Files
DiunDashboard/.planning/research/STACK.md

9.1 KiB

Technology Stack

Project: DiunDashboard — PostgreSQL milestone Researched: 2026-03-23 Scope: Adding PostgreSQL support alongside existing SQLite to a Go 1.26 backend


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:

// 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

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.

// 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

# 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