6.6 KiB
6.6 KiB
Phase 3: PostgreSQL Support - Context
Gathered: 2026-03-24 Status: Ready for planning
## Phase BoundaryAdd 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.
PostgreSQL driver interface
- D-01: Use
pgx/v5/stdlibas the database/sql adapter — matches SQLiteStore's*sql.DBpattern so PostgresStore has the same constructor signature (*sql.DBin, Store out) - D-02: Do NOT use pgx native interface directly — keeping both stores on
database/sqlmeans the Store interface stays unchanged andNewServer(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:
SERIALinstead ofINTEGER PRIMARY KEY AUTOINCREMENTfor tags.id$1, $2, $3positional params instead of?placeholdersNOW()orCURRENT_TIMESTAMPinstead ofdatetime('now')for acknowledged_atON CONFLICT ... DO UPDATE SETsyntax is compatible (PostgreSQL 9.5+)INSERT ... ON CONFLICT DO UPDATEfor UPSERT (same pattern, different param style)INSERT ... ON CONFLICTfor tag assignments instead ofINSERT OR REPLACE
Connection pooling
- D-05: PostgresStore does NOT use a mutex — PostgreSQL handles concurrent writes natively
- D-06: Use
database/sqldefault 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_URLenv var present → PostgreSQL; absent → SQLite withDB_PATH(already decided in STATE.md) - D-08: No separate
DB_DRIVERvariable — the presence ofDATABASE_URLis 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) andmigrations/postgres/(new) - D-11: PostgreSQL baseline migration
0001_initial_schema.up.sqlcreates the same 3 tables with PostgreSQL-native types - D-12:
RunMigrationsbecomes dialect-aware or split intoRunSQLiteMigrations/RunPostgresMigrations— researcher should determine best approach - D-13: PostgreSQL migrations embedded via separate
//go:embed migrations/postgresdirective
Docker Compose integration
- D-14: Use Docker Compose profiles —
docker compose --profile postgres upactivates the postgres service - D-15: Default compose (no profile) remains SQLite-only for simple deploys
- D-16: Compose file includes a
postgresservice with health check, and the app service getsDATABASE_URLwhen the profile is active
Testing strategy
- D-17: PostgresStore integration tests use a
//go:build postgresbuild tag — they only run when a PostgreSQL instance is available - D-18: CI can optionally run
-tags postgreswith a postgres service container; SQLite tests always run - D-19: Test helper
NewTestPostgresServer()creates a test database and runs migrations, similar toNewTestServer()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_refs>
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 portpkg/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)
</canonical_refs>
<code_context>
Existing Code Insights
Reusable Assets
Storeinterface instore.go: PostgresStore implements the same 9 methods — no handler changes neededSQLiteStoreinsqlite_store.go: Reference for all SQL operations — port each method to PostgreSQL dialectRunMigrationsinmigrate.go: Pattern for migration runner withembed.FS+iofs+golang-migrateNewTestServer()inexport_test.go: Pattern for test helper — clone for PostgreSQL variant
Established Patterns
database/sqlas the DB abstraction layer — PostgresStore follows the same patternsync.Mutexfor SQLite write serialization — NOT needed for PostgreSQL (native concurrent writes)//go:embedfor migration files — same pattern formigrations/postgres/- Constructor returns concrete type implementing Store:
NewSQLiteStore(*sql.DB) *SQLiteStore→NewPostgresStore(*sql.DB) *PostgresStore
Integration Points
main.goline 24:sql.Open("sqlite", dbPath)— add conditional forsql.Open("pgx", databaseURL)main.goline 29:diun.RunMigrations(db)— needs to call the right migration runnermain.goline 33:diun.NewSQLiteStore(db)— needs to calldiun.NewPostgresStore(db)when using PostgreSQLDockerfileStage 2: May needCGO_ENABLED=0to remain — verify pgx/v5/stdlib is pure Go
</code_context>
## Specific IdeasNo specific requirements — open to standard approaches. The core constraint is functional parity: every operation that works on SQLite must work identically on PostgreSQL.
## Deferred IdeasNone — discussion stayed within phase scope.
Phase: 03-postgresql-support Context gathered: 2026-03-24 via auto mode