diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index db19275..749843e 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -23,7 +23,7 @@ Requirements for this milestone. Each maps to roadmap phases. ### Database - [x] **DB-01**: PostgreSQL is supported as an alternative to SQLite via pgx v5 driver -- [ ] **DB-02**: Database backend is selected via DATABASE_URL env var (present = PostgreSQL, absent = SQLite with DB_PATH) +- [x] **DB-02**: Database backend is selected via DATABASE_URL env var (present = PostgreSQL, absent = SQLite with DB_PATH) - [x] **DB-03**: Existing SQLite users can upgrade without data loss (baseline migration represents current schema) ### Bulk Actions @@ -99,7 +99,7 @@ Which phases cover which requirements. Updated during roadmap creation. | REFAC-02 | Phase 2 | Complete | | REFAC-03 | Phase 2 | Complete | | DB-01 | Phase 3 | Complete | -| DB-02 | Phase 3 | Pending | +| DB-02 | Phase 3 | Complete | | DB-03 | Phase 3 | Complete | | BULK-01 | Phase 4 | Pending | | BULK-02 | Phase 4 | Pending | diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index a1298ec..31a9d90 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -62,7 +62,7 @@ Plans: Plans: - [x] 03-01-PLAN.md — Create PostgresStore (9 Store methods), PostgreSQL migration files, rename RunMigrations to RunSQLiteMigrations, add RunPostgresMigrations -- [ ] 03-02-PLAN.md — Wire DATABASE_URL branching in main.go, fix cross-dialect UNIQUE detection, add Docker Compose postgres profiles, create build-tagged test helper +- [x] 03-02-PLAN.md — Wire DATABASE_URL branching in main.go, fix cross-dialect UNIQUE detection, add Docker Compose postgres profiles, create build-tagged test helper ### Phase 4: UX Improvements **Goal**: Users can manage a large list of updates efficiently — dismissing many at once, finding specific images quickly, and seeing new arrivals without manual refreshes diff --git a/.planning/STATE.md b/.planning/STATE.md index 1f10426..1b125e1 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -2,14 +2,14 @@ gsd_state_version: 1.0 milestone: v1.0 milestone_name: milestone -status: Ready to execute -stopped_at: Completed 03-01-PLAN.md -last_updated: "2026-03-24T08:10:37.889Z" +status: Phase complete — ready for verification +stopped_at: Completed 03-02-PLAN.md +last_updated: "2026-03-24T08:14:20.006Z" progress: total_phases: 4 - completed_phases: 2 + completed_phases: 3 total_plans: 6 - completed_plans: 5 + completed_plans: 6 --- # Project State @@ -51,6 +51,7 @@ Plan: 2 of 2 | Phase 02-backend-refactor P01 | 7min | 2 tasks | 7 files | | Phase 02-backend-refactor P02 | 3min | 2 tasks | 4 files | | Phase 03-postgresql-support P01 | 3min | 2 tasks | 7 files | +| Phase 03-postgresql-support P02 | 2min | 2 tasks | 5 files | ## Accumulated Context @@ -73,6 +74,9 @@ Recent decisions affecting current work: - [Phase 02-backend-refactor]: NewTestServer pattern: each test gets its own in-memory SQLite DB (RunMigrations + NewSQLiteStore + NewServer) - eliminates shared global state between tests - [Phase 03-postgresql-support]: PostgresStore uses *sql.DB via pgx/v5/stdlib adapter with no mutex; TEXT timestamps match SQLiteStore scan logic - [Phase 03-postgresql-support]: CreateTag uses RETURNING id in PostgresStore (pgx does not support LastInsertId); AssignTag uses ON CONFLICT DO UPDATE +- [Phase 03-postgresql-support]: DATABASE_URL presence-check activates PostgreSQL; absent falls back to SQLite — simpler UX than a separate DB_DRIVER var +- [Phase 03-postgresql-support]: postgres Docker service uses profiles: [postgres] with required: false depends_on — default compose up unchanged, SQLite only +- [Phase 03-postgresql-support]: UNIQUE constraint detection uses strings.ToLower for case-insensitive matching across SQLite (uppercase UNIQUE) and PostgreSQL (lowercase unique) ### Pending Todos @@ -85,6 +89,6 @@ None yet. ## Session Continuity -Last session: 2026-03-24T08:10:37.887Z -Stopped at: Completed 03-01-PLAN.md +Last session: 2026-03-24T08:14:20.004Z +Stopped at: Completed 03-02-PLAN.md Resume file: None diff --git a/.planning/phases/03-postgresql-support/03-02-SUMMARY.md b/.planning/phases/03-postgresql-support/03-02-SUMMARY.md new file mode 100644 index 0000000..a7198d1 --- /dev/null +++ b/.planning/phases/03-postgresql-support/03-02-SUMMARY.md @@ -0,0 +1,88 @@ +--- +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