Files
DiunDashboard/.planning/phases/03-postgresql-support/03-02-PLAN.md

16 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
03-postgresql-support 02 execute 2
03-01
cmd/diunwebhook/main.go
pkg/diunwebhook/diunwebhook.go
pkg/diunwebhook/postgres_test.go
pkg/diunwebhook/export_test.go
compose.yml
compose.dev.yml
true
DB-01
DB-02
DB-03
truths artifacts key_links
Setting DATABASE_URL starts the app using PostgreSQL; omitting it falls back to SQLite with DB_PATH
Startup log clearly indicates which backend is active
Docker Compose with --profile postgres activates a PostgreSQL service
Default docker compose (no profile) remains SQLite-only
Duplicate tag creation returns 409 on both SQLite and PostgreSQL
Existing SQLite users can upgrade to this version with zero configuration changes and no data loss
path provides contains
cmd/diunwebhook/main.go DATABASE_URL branching logic DATABASE_URL
path provides contains
compose.yml Production compose with postgres profile profiles:
path provides contains
compose.dev.yml Dev compose with postgres profile profiles:
path provides contains
pkg/diunwebhook/postgres_test.go Build-tagged PostgreSQL integration test helper go:build postgres
path provides contains
pkg/diunwebhook/diunwebhook.go Case-insensitive UNIQUE constraint detection strings.ToLower
from to via pattern
cmd/diunwebhook/main.go pkg/diunwebhook/postgres_store.go diun.NewPostgresStore(db) NewPostgresStore
from to via pattern
cmd/diunwebhook/main.go pkg/diunwebhook/migrate.go diun.RunPostgresMigrations(db) RunPostgresMigrations
from to via pattern
cmd/diunwebhook/main.go pgx/v5/stdlib blank import for driver registration _ "github.com/jackc/pgx/v5/stdlib"
Wire PostgresStore into the application and deployment infrastructure.

Purpose: Connects the PostgresStore (built in Plan 01) to the startup path, adds Docker Compose profiles for PostgreSQL deployments, creates build-tagged integration test helpers, and fixes the UNIQUE constraint detection to work across both database backends. Also updates all call sites that still reference the old RunMigrations name (renamed to RunSQLiteMigrations in Plan 01). Output: Updated main.go with DATABASE_URL branching, compose files with postgres profiles, build-tagged test helper, cross-dialect error handling fix.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/03-postgresql-support/03-CONTEXT.md @.planning/phases/03-postgresql-support/03-RESEARCH.md @.planning/phases/03-postgresql-support/03-01-SUMMARY.md

@cmd/diunwebhook/main.go @pkg/diunwebhook/diunwebhook.go @pkg/diunwebhook/export_test.go @compose.yml @compose.dev.yml

From pkg/diunwebhook/postgres_store.go: ```go func NewPostgresStore(db *sql.DB) *PostgresStore ```

From pkg/diunwebhook/migrate.go:

func RunSQLiteMigrations(db *sql.DB) error
func RunPostgresMigrations(db *sql.DB) error

From pkg/diunwebhook/store.go:

type Store interface { ... } // 9 methods

From pkg/diunwebhook/diunwebhook.go:

func NewServer(store Store, webhookSecret string) *Server
Task 1: Wire DATABASE_URL branching in main.go, update call sites, and fix cross-dialect UNIQUE detection - cmd/diunwebhook/main.go (current SQLite-only startup to rewrite with branching) - pkg/diunwebhook/diunwebhook.go (TagsHandler - UNIQUE detection to fix) - pkg/diunwebhook/export_test.go (calls RunMigrations - must rename to RunSQLiteMigrations) - pkg/diunwebhook/postgres_store.go (verify NewPostgresStore exists from Plan 01) - pkg/diunwebhook/migrate.go (verify RunSQLiteMigrations and RunPostgresMigrations exist from Plan 01) cmd/diunwebhook/main.go, pkg/diunwebhook/diunwebhook.go, pkg/diunwebhook/export_test.go **1. Rewrite `cmd/diunwebhook/main.go`** with DATABASE_URL branching per D-07, D-08, D-09.

Replace the current database setup block with DATABASE_URL branching. The full main function should:

package main

import (
    "context"
    "database/sql"
    "errors"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    diun "awesomeProject/pkg/diunwebhook"
    _ "github.com/jackc/pgx/v5/stdlib"
    _ "modernc.org/sqlite"
)

func main() {
    databaseURL := os.Getenv("DATABASE_URL")
    var store diun.Store
    if databaseURL != "" {
        db, err := sql.Open("pgx", databaseURL)
        if err != nil {
            log.Fatalf("sql.Open postgres: %v", err)
        }
        if err := diun.RunPostgresMigrations(db); err != nil {
            log.Fatalf("RunPostgresMigrations: %v", err)
        }
        store = diun.NewPostgresStore(db)
        log.Println("Using PostgreSQL database")
    } else {
        dbPath := os.Getenv("DB_PATH")
        if dbPath == "" {
            dbPath = "./diun.db"
        }
        db, err := sql.Open("sqlite", dbPath)
        if err != nil {
            log.Fatalf("sql.Open sqlite: %v", err)
        }
        if err := diun.RunSQLiteMigrations(db); err != nil {
            log.Fatalf("RunSQLiteMigrations: %v", err)
        }
        store = diun.NewSQLiteStore(db)
        log.Printf("Using SQLite database at %s", dbPath)
    }

    // ... rest of main unchanged (secret, server, mux, httpSrv, graceful shutdown)
}

Key changes:

  • Add blank import _ "github.com/jackc/pgx/v5/stdlib" to register "pgx" driver name
  • DATABASE_URL present -> sql.Open("pgx", databaseURL) -> RunPostgresMigrations -> NewPostgresStore
  • DATABASE_URL absent -> existing SQLite path with RunSQLiteMigrations (renamed from RunMigrations in Plan 01)
  • Log "Using PostgreSQL database" or "Using SQLite database at %s" per D-09
  • Keep all existing code after the store setup unchanged (secret, server, mux, httpSrv, shutdown)

2. Update pkg/diunwebhook/export_test.go to use the renamed function.

Change all occurrences of RunMigrations(db) to RunSQLiteMigrations(db) in export_test.go. This completes the rename that Plan 01 started in migrate.go.

3. Fix cross-dialect UNIQUE constraint detection in pkg/diunwebhook/diunwebhook.go.

In the TagsHandler method, change:

if strings.Contains(err.Error(), "UNIQUE") {

to:

if strings.Contains(strings.ToLower(err.Error()), "unique") {

Why: SQLite errors contain uppercase "UNIQUE" (e.g., UNIQUE constraint failed: tags.name). PostgreSQL/pgx errors contain lowercase "unique" (e.g., duplicate key value violates unique constraint "tags_name_key"). Case-insensitive matching ensures 409 Conflict is returned for both backends. cd /home/jean-luc-makiola/Development/projects/DiunDashboard && go build ./... && go test -v -count=1 ./pkg/diunwebhook/ 2>&1 | tail -5 <acceptance_criteria> - cmd/diunwebhook/main.go contains databaseURL := os.Getenv("DATABASE_URL") - cmd/diunwebhook/main.go contains sql.Open("pgx", databaseURL) - cmd/diunwebhook/main.go contains diun.RunPostgresMigrations(db) - cmd/diunwebhook/main.go contains diun.NewPostgresStore(db) - cmd/diunwebhook/main.go contains log.Println("Using PostgreSQL database") - cmd/diunwebhook/main.go contains log.Printf("Using SQLite database at %s", dbPath) - cmd/diunwebhook/main.go contains _ "github.com/jackc/pgx/v5/stdlib" - cmd/diunwebhook/main.go contains diun.RunSQLiteMigrations(db) (not RunMigrations) - pkg/diunwebhook/export_test.go contains RunSQLiteMigrations (not RunMigrations) - pkg/diunwebhook/diunwebhook.go contains strings.Contains(strings.ToLower(err.Error()), "unique") - pkg/diunwebhook/diunwebhook.go does NOT contain strings.Contains(err.Error(), "UNIQUE") (old pattern removed) - go build ./... exits 0 - go test -v -count=1 ./pkg/diunwebhook/ exits 0 (full test suite passes) </acceptance_criteria> main.go branches on DATABASE_URL to select PostgreSQL or SQLite. pgx/v5/stdlib is blank-imported to register the driver. Startup log identifies the active backend. export_test.go updated with RunSQLiteMigrations. UNIQUE detection is case-insensitive for cross-dialect compatibility. All existing tests pass.

Task 2: Add Docker Compose postgres profiles and build-tagged test helper - compose.yml (current production compose to add postgres profile) - compose.dev.yml (current dev compose to add postgres profile) - pkg/diunwebhook/export_test.go (pattern for NewTestPostgresServer) - Dockerfile (verify no changes needed -- pgx/v5 is pure Go, CGO_ENABLED=0 is fine) compose.yml, compose.dev.yml, pkg/diunwebhook/postgres_test.go **1. Update `compose.yml`** (production) to add postgres profile per D-14, D-15, D-16:
# Minimum Docker Compose v2.20 required for depends_on.required
services:
  app:
    image: gitea.jeanlucmakiola.de/makiolaj/diundashboard:latest
    ports:
      - "8080:8080"
    environment:
      - WEBHOOK_SECRET=${WEBHOOK_SECRET:-}
      - PORT=${PORT:-8080}
      - DB_PATH=/data/diun.db
      - DATABASE_URL=${DATABASE_URL:-}
    volumes:
      - diun-data:/data
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
        required: false

  postgres:
    image: postgres:17-alpine
    profiles:
      - postgres
    environment:
      POSTGRES_USER: ${POSTGRES_USER:-diun}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-diun}
      POSTGRES_DB: ${POSTGRES_DB:-diundashboard}
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-diun}"]
      interval: 5s
      timeout: 5s
      retries: 5
      start_period: 10s
    restart: unless-stopped

volumes:
  diun-data:
  postgres-data:

Default docker compose up still uses SQLite (DATABASE_URL is empty string, app falls back to DB_PATH). docker compose --profile postgres up starts the postgres service; user sets DATABASE_URL=postgres://diun:diun@postgres:5432/diundashboard?sslmode=disable in .env.

2. Update compose.dev.yml to add postgres profile for local development:

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - WEBHOOK_SECRET=${WEBHOOK_SECRET:-}
      - DATABASE_URL=${DATABASE_URL:-}
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
        required: false

  postgres:
    image: postgres:17-alpine
    profiles:
      - postgres
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: ${POSTGRES_USER:-diun}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-diun}
      POSTGRES_DB: ${POSTGRES_DB:-diundashboard}
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-diun}"]
      interval: 5s
      timeout: 5s
      retries: 5
      start_period: 10s
    restart: unless-stopped

volumes:
  postgres-data:

Dev compose exposes port 5432 on host for direct psql access during development.

3. Create pkg/diunwebhook/postgres_test.go with build tag per D-17, D-19:

//go:build postgres

package diunwebhook

import (
    "database/sql"
    "os"

    _ "github.com/jackc/pgx/v5/stdlib"
)

// NewTestPostgresServer constructs a Server backed by a PostgreSQL database.
// Requires a running PostgreSQL instance. Set TEST_DATABASE_URL to override
// the default connection string.
func NewTestPostgresServer() (*Server, error) {
    databaseURL := os.Getenv("TEST_DATABASE_URL")
    if databaseURL == "" {
        databaseURL = "postgres://diun:diun@localhost:5432/diundashboard_test?sslmode=disable"
    }
    db, err := sql.Open("pgx", databaseURL)
    if err != nil {
        return nil, err
    }
    if err := RunPostgresMigrations(db); err != nil {
        return nil, err
    }
    store := NewPostgresStore(db)
    return NewServer(store, ""), nil
}

This file is in the diunwebhook package (internal, same as export_test.go pattern). The //go:build postgres tag ensures it only compiles when explicitly requested with go test -tags postgres. Without the tag, go test ./pkg/diunwebhook/ skips this file entirely -- no pgx import, no PostgreSQL dependency. cd /home/jean-luc-makiola/Development/projects/DiunDashboard && go build ./... && go test -v -count=1 ./pkg/diunwebhook/ 2>&1 | tail -5 <acceptance_criteria> - compose.yml contains profiles: under the postgres service - compose.yml contains - postgres (profile name) - compose.yml contains postgres:17-alpine - compose.yml contains pg_isready - compose.yml contains required: false (conditional depends_on) - compose.yml contains DATABASE_URL=${DATABASE_URL:-} - compose.yml contains postgres-data: in volumes - compose.dev.yml contains profiles: under the postgres service - compose.dev.yml contains - postgres (profile name) - compose.dev.yml contains "5432:5432" (exposed for dev) - compose.dev.yml contains required: false - compose.dev.yml contains DATABASE_URL=${DATABASE_URL:-} - pkg/diunwebhook/postgres_test.go contains //go:build postgres - pkg/diunwebhook/postgres_test.go contains func NewTestPostgresServer() - pkg/diunwebhook/postgres_test.go contains sql.Open("pgx", databaseURL) - pkg/diunwebhook/postgres_test.go contains RunPostgresMigrations(db) - pkg/diunwebhook/postgres_test.go contains NewPostgresStore(db) - pkg/diunwebhook/postgres_test.go contains TEST_DATABASE_URL - go build ./... exits 0 (postgres_test.go is not compiled without build tag) - go test -v -count=1 ./pkg/diunwebhook/ exits 0 (full SQLite test suite passes, postgres_test.go skipped) </acceptance_criteria> Docker Compose files support optional PostgreSQL via profiles. Default deploy remains SQLite-only. Build-tagged test helper exists for PostgreSQL integration testing. Dockerfile needs no changes (pgx/v5 is pure Go).

1. `go build ./...` succeeds 2. `go test -v -count=1 ./pkg/diunwebhook/` passes (all existing SQLite tests) 3. `docker compose config` validates without errors 4. `docker compose --profile postgres config` shows postgres service 5. `grep -c "DATABASE_URL" cmd/diunwebhook/main.go` returns at least 1 6. `grep "strings.ToLower" pkg/diunwebhook/diunwebhook.go` shows case-insensitive UNIQUE check

<success_criteria>

  • DATABASE_URL present: app opens pgx connection, runs PostgreSQL migrations, creates PostgresStore, logs "Using PostgreSQL database"
  • DATABASE_URL absent: app opens sqlite connection, runs SQLite migrations, creates SQLiteStore, logs "Using SQLite database at {path}"
  • docker compose up (no profile) works with SQLite only
  • docker compose --profile postgres up starts PostgreSQL service with health check
  • Build-tagged test helper available for PostgreSQL integration tests
  • UNIQUE constraint detection works for both SQLite and PostgreSQL error messages
  • All existing SQLite tests continue to pass </success_criteria>
After completion, create `.planning/phases/03-postgresql-support/03-02-SUMMARY.md`