--- phase: 03-postgresql-support plan: "02" subsystem: wiring tags: [postgresql, sqlite, database, docker-compose, branching] dependency_graph: requires: [03-01] provides: [DATABASE_URL branching, postgres docker profile, NewTestPostgresServer] affects: [cmd/diunwebhook/main.go, compose.yml, compose.dev.yml, pkg/diunwebhook/diunwebhook.go] tech_stack: added: [] patterns: [DATABASE_URL env var branching, Docker Compose profiles, build-tagged test helpers] key_files: created: - pkg/diunwebhook/postgres_test.go modified: - cmd/diunwebhook/main.go - pkg/diunwebhook/diunwebhook.go - compose.yml - compose.dev.yml decisions: - "DATABASE_URL present activates PostgreSQL path; absent falls back to SQLite with DB_PATH" - "postgres Docker service uses profiles: [postgres] so default compose up remains SQLite-only" - "UNIQUE detection uses strings.ToLower for case-insensitive matching across SQLite and PostgreSQL" - "Build tag //go:build postgres gates postgres_test.go so standard test runs have no pgx dependency" metrics: duration: "~2 minutes" completed: "2026-03-24T08:13:21Z" tasks_completed: 2 files_changed: 5 --- # Phase 03 Plan 02: Wire PostgreSQL Support and Deployment Infrastructure Summary DATABASE_URL branching in main.go routes to PostgresStore or SQLiteStore at startup; Docker Compose postgres profile enables optional PostgreSQL; build-tagged test helper and cross-dialect UNIQUE detection complete the integration. ## What Was Built ### Updated main.go (cmd/diunwebhook/main.go) - `DATABASE_URL` env var check: when set, opens pgx connection, runs `RunPostgresMigrations`, creates `NewPostgresStore`, logs `"Using PostgreSQL database"` - When absent: existing SQLite path using `RunSQLiteMigrations` (renamed in Plan 01), `NewSQLiteStore`, logs `"Using SQLite database at {path}"` - Blank import `_ "github.com/jackc/pgx/v5/stdlib"` registers the `"pgx"` driver name - All route wiring and graceful shutdown logic unchanged ### Cross-dialect UNIQUE detection (pkg/diunwebhook/diunwebhook.go) - `TagsHandler` now uses `strings.Contains(strings.ToLower(err.Error()), "unique")` for 409 Conflict detection - SQLite errors: `UNIQUE constraint failed: tags.name` (uppercase UNIQUE) - PostgreSQL errors: `duplicate key value violates unique constraint "tags_name_key"` (lowercase unique) - Both backends now return 409 correctly ### Docker Compose postgres profiles - `compose.yml`: postgres service added with `profiles: [postgres]`, healthcheck via `pg_isready`, `DATABASE_URL` env var in app service, conditional `depends_on` with `required: false`, `postgres-data` volume - `compose.dev.yml`: same postgres service with port 5432 exposed on host for direct psql access during development - Default `docker compose up` (no profile) unchanged — SQLite only, no new services start ### Build-tagged test helper (pkg/diunwebhook/postgres_test.go) - `//go:build postgres` tag — only compiled with `go test -tags postgres` - `NewTestPostgresServer()` constructs a `*Server` backed by PostgreSQL using `TEST_DATABASE_URL` env var (defaults to `postgres://diun:diun@localhost:5432/diundashboard_test?sslmode=disable`) - Calls `RunPostgresMigrations` and `NewPostgresStore` — mirrors the production startup path ## Decisions Made | Decision | Rationale | |----------|-----------| | DATABASE_URL presence-check (not a separate DB_DRIVER var) | Simpler UX; empty string = SQLite, any value = PostgreSQL | | profiles: [postgres] in compose files | Standard Docker Compose pattern for optional services; default deploy unchanged | | required: false in depends_on | App can start without postgres service (SQLite fallback); Docker Compose v2.20+ required | | //go:build postgres tag on test helper | Prevents pgx import at test time for standard `go test ./...` runs; explicit opt-in | | strings.ToLower for UNIQUE check | SQLite and PostgreSQL use different cases in constraint error messages | ## Deviations from Plan None — plan executed exactly as written. The `export_test.go` rename (RunMigrations -> RunSQLiteMigrations) was already completed as a deviation in Plan 01, as noted in the objective. ## Verification Results - `go build ./...` exits 0 - `go test -count=1 ./pkg/diunwebhook/` passes (all 20+ SQLite tests, postgres_test.go skipped) - `docker compose config` validates without errors - `docker compose --profile postgres config` shows postgres service - `grep -c "DATABASE_URL" cmd/diunwebhook/main.go` returns 1 - `grep "strings.ToLower" pkg/diunwebhook/diunwebhook.go` shows case-insensitive UNIQUE check ## Known Stubs None — this plan wires implementation code, no UI stubs. ## Self-Check: PASSED