docs: complete project research
This commit is contained in:
185
.planning/research/STACK.md
Normal file
185
.planning/research/STACK.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user