From 515ad9a1dd4011f7bb3687fd119c447283f4e51b Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Tue, 24 Mar 2026 08:46:52 +0100 Subject: [PATCH] docs(03): capture phase context --- .../03-postgresql-support/03-CONTEXT.md | 127 ++++++++++++++++++ .../03-DISCUSSION-LOG.md | 87 ++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 .planning/phases/03-postgresql-support/03-CONTEXT.md create mode 100644 .planning/phases/03-postgresql-support/03-DISCUSSION-LOG.md diff --git a/.planning/phases/03-postgresql-support/03-CONTEXT.md b/.planning/phases/03-postgresql-support/03-CONTEXT.md new file mode 100644 index 0000000..1316439 --- /dev/null +++ b/.planning/phases/03-postgresql-support/03-CONTEXT.md @@ -0,0 +1,127 @@ +# Phase 3: PostgreSQL Support - Context + +**Gathered:** 2026-03-24 +**Status:** Ready for planning + + +## Phase Boundary + +Add PostgreSQL as an alternative database backend alongside SQLite. Users with PostgreSQL infrastructure can point DiunDashboard at a Postgres database via `DATABASE_URL` and the dashboard works identically to the SQLite deployment. Existing SQLite users upgrade without data loss. + + + + +## Implementation Decisions + +### PostgreSQL driver interface +- **D-01:** Use `pgx/v5/stdlib` as the database/sql adapter — matches SQLiteStore's `*sql.DB` pattern so PostgresStore has the same constructor signature (`*sql.DB` in, Store out) +- **D-02:** Do NOT use pgx native interface directly — keeping both stores on `database/sql` means the Store interface stays unchanged and `NewServer(store Store, ...)` works identically + +### SQL dialect handling +- **D-03:** Each store implementation has its own raw SQL — no runtime dialect switching, no query builder, no shared SQL templates +- **D-04:** PostgreSQL-specific syntax differences handled in PostgresStore methods: + - `SERIAL` instead of `INTEGER PRIMARY KEY AUTOINCREMENT` for tags.id + - `$1, $2, $3` positional params instead of `?` placeholders + - `NOW()` or `CURRENT_TIMESTAMP` instead of `datetime('now')` for acknowledged_at + - `ON CONFLICT ... DO UPDATE SET` syntax is compatible (PostgreSQL 9.5+) + - `INSERT ... ON CONFLICT DO UPDATE` for UPSERT (same pattern, different param style) + - `INSERT ... ON CONFLICT` for tag assignments instead of `INSERT OR REPLACE` + +### Connection pooling +- **D-05:** PostgresStore does NOT use a mutex — PostgreSQL handles concurrent writes natively +- **D-06:** Use `database/sql` default pool settings with sensible overrides: `MaxOpenConns(25)`, `MaxIdleConns(5)`, `ConnMaxLifetime(5 * time.Minute)` — appropriate for a low-traffic self-hosted dashboard + +### Database selection logic (main.go) +- **D-07:** `DATABASE_URL` env var present → PostgreSQL; absent → SQLite with `DB_PATH` (already decided in STATE.md) +- **D-08:** No separate `DB_DRIVER` variable — the presence of `DATABASE_URL` is the switch +- **D-09:** Startup log clearly indicates which backend is active: `"Using PostgreSQL database"` vs `"Using SQLite database at {path}"` + +### Migration structure +- **D-10:** Separate migration directories: `migrations/sqlite/` (exists) and `migrations/postgres/` (new) +- **D-11:** PostgreSQL baseline migration `0001_initial_schema.up.sql` creates the same 3 tables with PostgreSQL-native types +- **D-12:** `RunMigrations` becomes dialect-aware or split into `RunSQLiteMigrations`/`RunPostgresMigrations` — researcher should determine best approach +- **D-13:** PostgreSQL migrations embedded via separate `//go:embed migrations/postgres` directive + +### Docker Compose integration +- **D-14:** Use Docker Compose profiles — `docker compose --profile postgres up` activates the postgres service +- **D-15:** Default compose (no profile) remains SQLite-only for simple deploys +- **D-16:** Compose file includes a `postgres` service with health check, and the app service gets `DATABASE_URL` when the profile is active + +### Testing strategy +- **D-17:** PostgresStore integration tests use a `//go:build postgres` build tag — they only run when a PostgreSQL instance is available +- **D-18:** CI can optionally run `-tags postgres` with a postgres service container; SQLite tests always run +- **D-19:** Test helper `NewTestPostgresServer()` creates a test database and runs migrations, similar to `NewTestServer()` for SQLite + +### Claude's Discretion +- Exact PostgreSQL connection pool tuning beyond the defaults in D-06 +- Whether to split RunMigrations into two functions or use a dialect parameter +- Error message formatting for PostgreSQL connection failures +- Whether to add a health check endpoint that verifies database connectivity + + + + +## Canonical References + +**Downstream agents MUST read these before planning or implementing.** + +### Store interface and patterns +- `pkg/diunwebhook/store.go` — Store interface definition (9 methods that PostgresStore must implement) +- `pkg/diunwebhook/sqlite_store.go` — Reference implementation with exact SQL operations to port +- `pkg/diunwebhook/migrate.go` — Current migration runner (SQLite-only, needs PostgreSQL support) + +### Schema +- `pkg/diunwebhook/migrations/sqlite/0001_initial_schema.up.sql` — Baseline schema to translate to PostgreSQL dialect + +### Wiring +- `cmd/diunwebhook/main.go` — Current startup wiring (SQLite-only, needs DATABASE_URL branching) +- `pkg/diunwebhook/export_test.go` — Test server helpers (pattern for NewTestPostgresServer) + +### Deployment +- `Dockerfile` — Current build (may need postgres client libs or build tag) +- `compose.yml` — Production compose (needs postgres profile) +- `compose.dev.yml` — Dev compose (needs postgres profile for local dev) + + + + +## Existing Code Insights + +### Reusable Assets +- `Store` interface in `store.go`: PostgresStore implements the same 9 methods — no handler changes needed +- `SQLiteStore` in `sqlite_store.go`: Reference for all SQL operations — port each method to PostgreSQL dialect +- `RunMigrations` in `migrate.go`: Pattern for migration runner with `embed.FS` + `iofs` + `golang-migrate` +- `NewTestServer()` in `export_test.go`: Pattern for test helper — clone for PostgreSQL variant + +### Established Patterns +- `database/sql` as the DB abstraction layer — PostgresStore follows the same pattern +- `sync.Mutex` for SQLite write serialization — NOT needed for PostgreSQL (native concurrent writes) +- `//go:embed` for migration files — same pattern for `migrations/postgres/` +- Constructor returns concrete type implementing Store: `NewSQLiteStore(*sql.DB) *SQLiteStore` → `NewPostgresStore(*sql.DB) *PostgresStore` + +### Integration Points +- `main.go` line 24: `sql.Open("sqlite", dbPath)` — add conditional for `sql.Open("pgx", databaseURL)` +- `main.go` line 29: `diun.RunMigrations(db)` — needs to call the right migration runner +- `main.go` line 33: `diun.NewSQLiteStore(db)` — needs to call `diun.NewPostgresStore(db)` when using PostgreSQL +- `Dockerfile` Stage 2: May need `CGO_ENABLED=0` to remain — verify pgx/v5/stdlib is pure Go + + + + +## Specific Ideas + +No specific requirements — open to standard approaches. The core constraint is functional parity: every operation that works on SQLite must work identically on PostgreSQL. + + + + +## Deferred Ideas + +None — discussion stayed within phase scope. + + + +--- + +*Phase: 03-postgresql-support* +*Context gathered: 2026-03-24 via auto mode* diff --git a/.planning/phases/03-postgresql-support/03-DISCUSSION-LOG.md b/.planning/phases/03-postgresql-support/03-DISCUSSION-LOG.md new file mode 100644 index 0000000..88fabd5 --- /dev/null +++ b/.planning/phases/03-postgresql-support/03-DISCUSSION-LOG.md @@ -0,0 +1,87 @@ +# Phase 3: PostgreSQL Support - Discussion Log + +> **Audit trail only.** Do not use as input to planning, research, or execution agents. +> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered. + +**Date:** 2026-03-24 +**Phase:** 03-postgresql-support +**Areas discussed:** PostgreSQL driver interface, SQL dialect handling, Connection pooling, Docker Compose integration, Testing strategy +**Mode:** Auto (all selections made by Claude using recommended defaults) + +--- + +## PostgreSQL Driver Interface + +| Option | Description | Selected | +|--------|-------------|----------| +| pgx/v5/stdlib (database/sql adapter) | Matches SQLiteStore's *sql.DB pattern, Store interface unchanged | ✓ | +| pgx native interface | More features (COPY, batch), but different API from SQLiteStore | | +| lib/pq | Legacy driver, less maintained | | + +**User's choice:** [auto] pgx/v5/stdlib — recommended for consistency with existing database/sql pattern +**Notes:** Keeping both stores on database/sql means identical constructor signatures and no Store interface changes. + +--- + +## SQL Dialect Handling + +| Option | Description | Selected | +|--------|-------------|----------| +| Separate SQL per store | Each store has its own raw SQL, no shared templates | ✓ | +| Runtime dialect switching | Single store with if/else for dialect differences | | +| Query builder (squirrel/goqu) | Abstract SQL differences behind builder API | | + +**User's choice:** [auto] Separate SQL per store — recommended per project constraint (no ORM/query builder) +**Notes:** PROJECT.md explicitly states "No ORM or query builder — raw SQL per store implementation." + +--- + +## Connection Pooling + +| Option | Description | Selected | +|--------|-------------|----------| +| Standard pool defaults | MaxOpenConns(25), MaxIdleConns(5), ConnMaxLifetime(5m) | ✓ | +| Minimal single-connection | Match SQLite's MaxOpenConns(1) | | +| Configurable via env vars | Let users tune pool settings | | + +**User's choice:** [auto] Standard pool defaults — appropriate for low-traffic self-hosted dashboard +**Notes:** PostgreSQL handles concurrent writes natively, so no mutex needed unlike SQLiteStore. + +--- + +## Docker Compose Integration + +| Option | Description | Selected | +|--------|-------------|----------| +| Docker Compose profiles | `--profile postgres` activates postgres service | ✓ | +| Separate compose file | compose.postgres.yml alongside compose.yml | | +| Always include postgres | Postgres service always defined, user enables via DATABASE_URL | | + +**User's choice:** [auto] Docker Compose profiles — keeps simple deploys unchanged, opt-in for postgres +**Notes:** ROADMAP success criterion #4 states "optional postgres service profile." + +--- + +## Testing Strategy + +| Option | Description | Selected | +|--------|-------------|----------| +| Build tag `//go:build postgres` | Tests only run when postgres available | ✓ | +| Testcontainers (auto-start postgres) | No external dependency needed | | +| Mock store for postgres tests | No real postgres needed, but less confidence | | + +**User's choice:** [auto] Build tag — simplest approach, CI optionally runs with `-tags postgres` +**Notes:** Matches existing test pattern where SQLite tests always run. PostgreSQL tests are additive. + +--- + +## Claude's Discretion + +- Exact PostgreSQL connection pool tuning beyond defaults +- RunMigrations split strategy (two functions vs dialect parameter) +- Error message formatting for connection failures +- Health check endpoint (optional) + +## Deferred Ideas + +None — discussion stayed within phase scope.