# Technology Stack **Project:** DiunDashboard — PostgreSQL milestone **Researched:** 2026-03-23 **Scope:** Adding PostgreSQL support alongside existing SQLite to a Go 1.26 backend --- ## Recommended Stack ### PostgreSQL Driver | Technology | Version | Purpose | Why | |------------|---------|---------|-----| | `github.com/jackc/pgx/v5/stdlib` | v5.9.1 | PostgreSQL `database/sql` driver | The de-facto standard Go PostgreSQL driver. Pure Go. 7,328+ importers. The `stdlib` adapter makes it a drop-in for the existing `*sql.DB` code path. Native pgx interface not needed — this project uses `database/sql` already and has no PostgreSQL-specific features (no LISTEN/NOTIFY, no COPY). | **Confidence:** HIGH — Verified via pkg.go.dev (v5.9.1, published 2026-03-22). pgx v5 is the clear community standard; lib/pq is officially in maintenance-only mode. **Do NOT use:** - `github.com/lib/pq` — maintenance-only since 2021; pgx is the successor recommended by the postgres ecosystem. - Native pgx interface (`pgx.Connect`, `pgxpool.New`) — overkill here; this project only needs standard queries and the existing `*sql.DB` pattern should be preserved for consistency. ### Database Migration Tool | Technology | Version | Purpose | Why | |------------|---------|---------|-----| | `github.com/golang-migrate/migrate/v4` | v4.19.1 | Schema migrations for both SQLite and PostgreSQL | Supports both `database/sqlite` (uses `modernc.org/sqlite` — pure Go, no CGO) and `database/pgx/v5` (uses pgx v5). Both drivers are maintained. The existing inline `CREATE TABLE IF NOT EXISTS` + silent `ALTER TABLE` approach does not scale to dual-database support; a proper migration tool is required. | **Confidence:** HIGH — Verified via pkg.go.dev. The `database/sqlite` sub-package explicitly uses `modernc.org/sqlite` (pure Go), matching the project's no-CGO constraint. The `database/pgx/v5` sub-package uses pgx v5. **Drivers to import:** ```go // For SQLite migrations (pure Go, no CGO — matches existing constraint) _ "github.com/golang-migrate/migrate/v4/database/sqlite" // For PostgreSQL migrations (via pgx v5) _ "github.com/golang-migrate/migrate/v4/database/pgx/v5" // Migration source (embedded files) _ "github.com/golang-migrate/migrate/v4/source/iofs" ``` **Do NOT use:** - `pressly/goose` — Its SQLite dialect documentation does not confirm pure-Go driver support; CGO status is ambiguous. golang-migrate explicitly documents use of `modernc.org/sqlite`. Goose is a fine tool but the CGO uncertainty is a disqualifier for this project. - `database/sqlite3` variant of golang-migrate — Uses `mattn/go-sqlite3` which requires CGO. Use `database/sqlite` (no `3`) instead. ### SQLite Driver (Existing — Retain) | Technology | Version | Purpose | Why | |------------|---------|---------|-----| | `modernc.org/sqlite` | v1.47.0 | Pure-Go SQLite driver | Already in use; must be retained for no-CGO cross-compilation. Current version in go.mod is v1.46.1 — upgrade to v1.47.0 (released 2026-03-17) for latest SQLite 3.51.3 and bug fixes. | **Confidence:** HIGH — Verified via pkg.go.dev versions tab. --- ## SQL Dialect Abstraction ### The Problem The existing codebase has four SQLite-specific SQL constructs that break on PostgreSQL: | Location | SQLite syntax | PostgreSQL equivalent | |----------|--------------|----------------------| | `InitDB` — tags table | `INTEGER PRIMARY KEY AUTOINCREMENT` | `INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY` | | `UpdateEvent` | `INSERT OR REPLACE INTO updates VALUES (?,...)` | `INSERT INTO updates (...) ON CONFLICT (image) DO UPDATE SET ...` | | `DismissHandler` | `UPDATE ... SET acknowledged_at = datetime('now')` | `UPDATE ... SET acknowledged_at = NOW()` | | `TagAssignmentHandler` | `INSERT OR REPLACE INTO tag_assignments` | `INSERT INTO tag_assignments ... ON CONFLICT (image) DO UPDATE SET tag_id = ...` | | All handlers | `?` positional placeholders | `$1, $2, ...` positional placeholders | ### Recommended Pattern: Storage Interface Extract a `Store` interface in `pkg/diunwebhook/`. Implement it twice: once for SQLite (`sqliteStore`), once for PostgreSQL (`postgresStore`). Both implementations use `database/sql` and raw SQL, but with dialect-appropriate queries. ```go // pkg/diunwebhook/store.go type Store interface { InitSchema() error UpdateEvent(event DiunEvent) error GetUpdates() (map[string]UpdateEntry, error) DismissUpdate(image string) error GetTags() ([]Tag, error) CreateTag(name string) (Tag, error) DeleteTag(id int) error AssignTag(image string, tagID int) error UnassignTag(image string) error } ``` This is a standard Go pattern: define a narrow interface, swap implementations via factory function. The `sync.Mutex` moves into each store implementation (SQLite store keeps `SetMaxOpenConns(1)` + mutex; PostgreSQL store can use a connection pool without a global mutex). **Do NOT use:** - ORM (GORM, ent, sqlc, etc.) — The query set is small and known. An ORM adds a dependency with its own dialect quirks and opaque query generation. Raw SQL with an interface is simpler, easier to test, and matches the existing project style. - `database/sql` query builder libraries (squirrel, etc.) — Same reasoning; the schema is simple enough that explicit SQL per dialect is more readable and maintainable. --- ## Configuration ### New Environment Variable | Variable | Purpose | Default | |----------|---------|---------| | `DATABASE_URL` | PostgreSQL connection string (triggers PostgreSQL mode when set) | — (unset = SQLite mode) | | `DB_PATH` | SQLite file path (existing) | `./diun.db` | **Selection logic:** If `DATABASE_URL` is set, use PostgreSQL. Otherwise, use SQLite with `DB_PATH`. This is the simplest signal — no new `DB_DRIVER` variable needed. **PostgreSQL connection string format:** ``` postgres://user:password@host:5432/dbname?sslmode=disable ``` --- ## Migration File Structure ``` migrations/ 001_initial_schema.up.sql 001_initial_schema.down.sql 002_add_acknowledged_at.up.sql 002_add_acknowledged_at.down.sql ``` Each migration file should be valid for **both** SQLite and PostgreSQL — this is achievable for the current schema since: - `AUTOINCREMENT` can become `INTEGER PRIMARY KEY` (SQLite auto-assigns rowid regardless of keyword; PostgreSQL uses `SERIAL` — requires separate dialect files or a compatibility shim). **Revised recommendation:** Use **separate migration directories per dialect** when DDL diverges significantly: ``` migrations/ sqlite/ 001_initial_schema.up.sql 002_add_acknowledged_at.up.sql postgres/ 001_initial_schema.up.sql 002_add_acknowledged_at.up.sql ``` This is more explicit than trying to share SQL across dialects. golang-migrate supports `iofs` (Go embed) as a source, so both directories can be embedded in the binary. --- ## Full Dependency Changes ```bash # Add PostgreSQL driver (via pgx v5 stdlib adapter) go get github.com/jackc/pgx/v5@v5.9.1 # Add migration tool with SQLite (pure Go) and pgx/v5 drivers go get github.com/golang-migrate/migrate/v4@v4.19.1 # Upgrade existing SQLite driver to current version go get modernc.org/sqlite@v1.47.0 ``` No other new dependencies are required. The existing `database/sql` usage throughout the codebase is preserved. --- ## Alternatives Considered | Category | Recommended | Alternative | Why Not | |----------|-------------|-------------|---------| | PostgreSQL driver | pgx/v5 stdlib | lib/pq | lib/pq is maintenance-only since 2021; pgx is the successor | | PostgreSQL driver | pgx/v5 stdlib | Native pgx interface | Project uses database/sql; stdlib adapter preserves consistency; no need for PostgreSQL-specific features | | Migration tool | golang-migrate | pressly/goose | Goose's SQLite CGO status unconfirmed; golang-migrate explicitly uses modernc.org/sqlite | | Migration tool | golang-migrate | Inline `CREATE TABLE IF NOT EXISTS` | Inline approach cannot handle dual-dialect schema differences or ordered version history | | Abstraction | Store interface | GORM / ent | Schema is 3 tables; ORM adds complexity without benefit; project already uses raw SQL | | Abstraction | Store interface | sqlc | Code generation adds a build step and CI dependency; not warranted for this scope | | Placeholder style | Per-dialect (`?` vs `$1`) | `sqlx` named params | Named params add a new library; explicit per-dialect SQL is clearer and matches project style | --- ## Sources - pgx v5.9.1: https://pkg.go.dev/github.com/jackc/pgx/v5@v5.9.1 — HIGH confidence - pgxpool: https://pkg.go.dev/github.com/jackc/pgx/v5/pgxpool — HIGH confidence - golang-migrate v4.19.1 sqlite driver (pure Go): https://pkg.go.dev/github.com/golang-migrate/migrate/v4/database/sqlite — HIGH confidence - golang-migrate v4 pgx/v5 driver: https://pkg.go.dev/github.com/golang-migrate/migrate/v4/database/pgx/v5 — HIGH confidence - golang-migrate v4 sqlite3 driver (CGO — avoid): https://pkg.go.dev/github.com/golang-migrate/migrate/v4/database/sqlite3 — HIGH confidence - modernc.org/sqlite v1.47.0: https://pkg.go.dev/modernc.org/sqlite?tab=versions — HIGH confidence - goose v3.27.0: https://pkg.go.dev/github.com/pressly/goose/v3 — MEDIUM confidence (SQLite CGO status not confirmed in official docs)