28 KiB
Phase 2: Backend Refactor - Research
Researched: 2026-03-23 Domain: Go interface extraction, dependency injection, golang-migrate with modernc.org/sqlite Confidence: HIGH
Summary
Phase 2 replaces three package-level globals (db, mu, webhookSecret) with a Server struct that holds a Store interface. HTTP handlers become methods on Server. SQL is extracted from handlers into named Store methods with a concrete SQLiteStore implementation. Schema management moves to versioned SQL migration files run by golang-migrate/v4 at startup via embed.FS.
The change is purely structural. No API contracts, no HTTP status codes, no SQL query semantics change. The test suite must pass before the phase is complete. Tests currently rely on export_test.go helpers (UpdatesReset, GetUpdatesMap, ResetTags, ResetWebhookSecret) that call package-level functions directly — these must be redesigned to work against the new Server/Store seam.
The critical library constraint is that golang-migrate/v4/database/sqlite (not database/sqlite3) uses modernc.org/sqlite — the same pure-Go driver already in use. This is the only migration path that avoids introducing CGO.
Primary recommendation: Extract a Store interface with one method per logical operation, implement SQLiteStore backed by *sql.DB, replace globals with a Server struct holding Store and webhookSecret, move all DDL to embedded SQL files under migrations/sqlite/, run migrations on startup via golang-migrate/v4.
<user_constraints>
User Constraints (from CONTEXT.md)
No CONTEXT.md exists for this phase. Constraints are drawn from CLAUDE.md and STATE.md decisions.
Locked Decisions (from STATE.md Accumulated Context)
- Backend refactor must be behavior-neutral — all existing tests must pass before PostgreSQL is introduced
- No ORM or query builder — raw SQL per store implementation; 8 operations across 3 tables is too small to justify a dependency
DATABASE_URLpresent activates PostgreSQL; absent falls back to SQLite withDB_PATH— no separateDB_DRIVERvariable (deferred to Phase 3; Store interface must accommodate it)
Claude's Discretion
- Internal file layout within
pkg/diunwebhook/and new sub-packages (e.g.,store/) - Migration file naming convention within the chosen scheme
- Whether
Serverlives in the same package asStoreor a separate one
Deferred Ideas (OUT OF SCOPE for Phase 2)
- PostgreSQL implementation of
Store(Phase 3) - Any new API endpoints or behavioral changes
- DATABASE_URL env var routing (Phase 3) </user_constraints>
<phase_requirements>
Phase Requirements
| ID | Description | Research Support |
|---|---|---|
| REFAC-01 | Database operations are behind a Store interface with separate SQLite and PostgreSQL implementations | Store interface design, SQLiteStore struct with *sql.DB, method inventory below |
| REFAC-02 | Package-level global state (db, mu, webhookSecret) is replaced with a Server struct that holds dependencies | Server struct pattern, handler-as-method pattern, export_test.go redesign |
| REFAC-03 | Schema migrations use golang-migrate with separate migration directories per dialect (sqlite/, postgres/) | golang-migrate v4.19.1, database/sqlite sub-package uses modernc.org/sqlite, iofs embed.FS source |
| </phase_requirements> |
Standard Stack
Core
| Library | Version | Purpose | Why Standard |
|---|---|---|---|
github.com/golang-migrate/migrate/v4 |
v4.19.1 | Versioned schema migrations | De-facto standard in Go; supports multiple DB drivers; iofs source enables single-binary deploy |
github.com/golang-migrate/migrate/v4/database/sqlite |
v4.19.1 (same module) | golang-migrate driver for modernc.org/sqlite | Only non-CGO sqlite driver in golang-migrate; uses pure-Go modernc.org/sqlite |
github.com/golang-migrate/migrate/v4/source/iofs |
v4.19.1 (same module) | Read migrations from embed.FS | Keeps migrations bundled in the binary — required for single-binary Docker deploy |
Note on sqlite sub-package: Use database/sqlite (NOT database/sqlite3). The sqlite3 sub-package requires CGO via mattn/go-sqlite3, which violates the project's no-CGO constraint. Verified against pkg.go.dev documentation.
Supporting (already in go.mod — no new additions for the Store/Server pattern)
| Library | Version | Purpose | When to Use |
|---|---|---|---|
modernc.org/sqlite |
v1.46.1 (current) | Pure-Go SQLite driver | Already present; imported as _ "modernc.org/sqlite" for side-effect registration |
Go stdlib sync |
— | sync.Mutex inside SQLiteStore |
Mutex moves from package-level to a field on SQLiteStore |
Go stdlib embed |
— | //go:embed for migration files |
Embed SQL files into compiled binary |
Alternatives Considered
| Instead of | Could Use | Tradeoff |
|---|---|---|
golang-migrate iofs source |
Raw DDL in InitDB (current) |
Current approach blocks versioned migrations and PostgreSQL parity; golang-migrate handles ordering, locking, and checksums |
database/sqlite sub-package |
database/sqlite3 |
sqlite3 requires CGO — forbidden by project constraint |
Handler methods on Server |
Function closures over Server |
Methods are idiomatic Go, simpler to test, consistent with net/http handler signature func(w, r) via thin wrapper |
Installation (new dependencies only):
go get github.com/golang-migrate/migrate/v4@v4.19.1
go get github.com/golang-migrate/migrate/v4/database/sqlite
go get github.com/golang-migrate/migrate/v4/source/iofs
Version verification: v4.19.1 confirmed via Go module proxy (proxy.golang.org) on 2026-03-23. Published 2025-11-29.
Architecture Patterns
Recommended Project Structure
pkg/diunwebhook/
├── diunwebhook.go # Types (DiunEvent, UpdateEntry, Tag), Server struct, handler methods
├── store.go # Store interface definition
├── sqlite_store.go # SQLiteStore — concrete implementation
├── migrate.go # RunMigrations() using golang-migrate + iofs
├── export_test.go # Test-only helpers (redesigned for Server/Store)
├── diunwebhook_test.go # Handler tests (unchanged HTTP assertions)
└── migrations/
└── sqlite/
├── 0001_initial_schema.up.sql
├── 0001_initial_schema.down.sql
└── 0002_add_acknowledged_at.up.sql # baseline migration for existing acknowledged_at column
cmd/diunwebhook/
└── main.go # Constructs SQLiteStore, calls RunMigrations, builds Server, registers routes
Why keep everything in pkg/diunwebhook/: CLAUDE.md says "No barrel files; single source file" — this phase is allowed to split into multiple files within the same package to keep things navigable, but a new sub-package is not required. All existing import paths (awesomeProject/pkg/diunwebhook) stay valid.
Pattern 1: Store Interface
What: A Go interface that names every persistence operation the HTTP handlers need. One method per logical operation. No *sql.DB in the interface — callers never see the database type.
When to use: Always, for all DB access from handlers.
// store.go
type Store interface {
UpsertEvent(event DiunEvent) error
GetUpdates() (map[string]UpdateEntry, error)
AcknowledgeUpdate(image string) (found bool, err error)
ListTags() ([]Tag, error)
CreateTag(name string) (Tag, error)
DeleteTag(id int) (found bool, err error)
AssignTag(image string, tagID int) error
UnassignTag(image string) error
TagExists(id int) (bool, error)
}
Method count: 9 methods covering all current SQL operations across updates, tags, and tag_assignments. Each method maps 1:1 to a logical DB operation that currently appears inline in a handler or in UpdateEvent/GetUpdates.
Pattern 2: SQLiteStore
What: Concrete struct holding *sql.DB and sync.Mutex. Implements every method on Store. All SQL currently in handlers moves here.
// sqlite_store.go
type SQLiteStore struct {
db *sql.DB
mu sync.Mutex
}
func NewSQLiteStore(db *sql.DB) *SQLiteStore {
return &SQLiteStore{db: db}
}
func (s *SQLiteStore) UpsertEvent(event DiunEvent) error {
s.mu.Lock()
defer s.mu.Unlock()
_, err := s.db.Exec(`INSERT INTO updates (...) ON CONFLICT ...`, ...)
return err
}
Key: The mutex moves from a package global var mu sync.Mutex to a SQLiteStore field. This enables parallel tests (each test gets its own SQLiteStore with its own in-memory DB).
Pattern 3: Server Struct
What: Holds the Store interface and webhookSecret. Handler methods hang off Server. main.go constructs it and registers routes.
// diunwebhook.go
type Server struct {
store Store
webhookSecret string
}
func NewServer(store Store, webhookSecret string) *Server {
return &Server{store: store, webhookSecret: webhookSecret}
}
func (s *Server) WebhookHandler(w http.ResponseWriter, r *http.Request) { ... }
func (s *Server) UpdatesHandler(w http.ResponseWriter, r *http.Request) { ... }
// ... etc
Route registration in main.go:
srv := diun.NewServer(store, secret)
mux.HandleFunc("/webhook", srv.WebhookHandler)
mux.HandleFunc("/api/updates/", srv.DismissHandler)
// ...
Pattern 4: RunMigrations with embed.FS
What: RunMigrations(db *sql.DB, dialect string) uses golang-migrate/v4 to apply versioned SQL files embedded in the binary. Called from main.go before routes are registered.
// migrate.go
import (
"embed"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/sqlite"
"github.com/golang-migrate/migrate/v4/source/iofs"
_ "modernc.org/sqlite"
)
//go:embed migrations/sqlite
var sqliteMigrations embed.FS
func RunMigrations(db *sql.DB) error {
src, err := iofs.New(sqliteMigrations, "migrations/sqlite")
if err != nil {
return err
}
driver, err := sqlite.WithInstance(db, &sqlite.Config{})
if err != nil {
return err
}
m, err := migrate.NewWithInstance("iofs", src, "sqlite", driver)
if err != nil {
return err
}
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
return err
}
return nil
}
CRITICAL: migrate.ErrNoChange is not an error — it means all migrations already applied. Must not treat it as failure.
Pattern 5: export_test.go Redesign
What: The current export_test.go calls package-level functions (InitDB, db.Exec). After the refactor, these globals are gone. Test helpers must construct a Server backed by a SQLiteStore using an in-memory DB.
// export_test.go — new design
package diunwebhook
// TestServer constructs a Server with a fresh in-memory SQLiteStore.
// Used by test files to get a clean server per test.
func NewTestServer() (*Server, error) {
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
return nil, err
}
if err := RunMigrations(db); err != nil {
return nil, err
}
store := NewSQLiteStore(db)
return NewServer(store, ""), nil
}
Tests that previously called diun.UpdatesReset() will call diun.NewTestServer() at the start of each test and operate on the returned server instance. Handler tests pass srv.WebhookHandler instead of diun.WebhookHandler.
Impact on test signatures: All test functions that currently call package-level handler functions will receive the server as a local variable. TestMain simplifies (no global reset needed — each test owns its DB).
Anti-Patterns to Avoid
- Direct SQL in handlers: After REFAC-01, handlers must call
s.store.SomeMethod(...)— nevers.store.(*SQLiteStore).db.Exec(...). The interface hides the DB type. - Single migration file containing all schema:
InitDB's current DDL represents TWO logical migrations (initial schema +acknowledged_atcolumn). These must become two separate numbered files so existing databases do not re-apply the already-applied column addition. Baseline migration (file 0001) represents the state of existing databases; file 0002 addsacknowledged_atto represent the already-run ad-hoc migration. - Calling
m.Up()and treatingErrNoChangeas fatal: Always checkerr != migrate.ErrNoChangebefore returning an error fromRunMigrations. - Removing
PRAGMA foreign_keys = ONduring refactor: The SQLite connection setup must still run this pragma. Move it fromInitDBintoNewSQLiteStoreor the connection-open step inmain.go. - Replacing
db.SetMaxOpenConns(1)with nothing: This setting prevents concurrent write contention in SQLite. It must be preserved on the*sql.DBinstance passed toNewSQLiteStore.
Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---|---|---|---|
| Versioned schema migration | Custom migration runner with version table | golang-migrate/v4 |
Migration ordering, dirty-state detection, locking, and ErrNoChange handling already solved |
| Embedding SQL files in binary | Copying SQL into string constants | Go embed.FS + iofs source |
Single-binary deploy; embed handles file reading at compile time |
| Migration down-file generation | Omitting .down.sql files |
Create stub down files | golang-migrate requires down files exist even if empty to resolve migration history |
Key insight: The migration machinery looks simple but has multiple edge cases (dirty state after failed migration, concurrent migration race, no-change idempotency). golang-migrate handles all of these.
Common Pitfalls
Pitfall 1: Wrong sqlite sub-package (CGO contamination)
What goes wrong: Developer imports github.com/golang-migrate/migrate/v4/database/sqlite3 (the one with the 3) — this pulls in mattn/go-sqlite3 which requires CGO. The build succeeds on developer machines with a C compiler but fails in Alpine/cross-compilation.
Why it happens: The two sub-packages have nearly identical names. The sqlite3 one appears first in search results.
How to avoid: Always import database/sqlite (no 3). Verify with go mod graph | grep sqlite.
Warning signs: Build output mentions gcc or cgo; go build fails with "cgo: C compiler not found".
Pitfall 2: ErrNoChange treated as fatal
What goes wrong: RunMigrations returns an error when the database is already at the latest migration version, causing every startup after the first to crash.
Why it happens: m.Up() returns migrate.ErrNoChange (a non-nil error) when no new migrations exist.
How to avoid: if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) { return err }.
Warning signs: App starts successfully once, crashes with "no change" on every subsequent start.
Pitfall 3: PRAGMA foreign_keys lost during refactor
What goes wrong: The pragma is in InitDB which is being deleted. If it is not moved to the connection-open step, foreign key cascades silently stop working. The TestDeleteTagHandler_CascadesAssignment test catches this — but only if the pragma is active.
Why it happens: Refactor focuses on interface extraction and forgets the SQLite-specific connection setup.
How to avoid: Set PRAGMA foreign_keys = ON immediately after sql.Open and before any queries, inside NewSQLiteStore or via sql.DB.Exec in main.go.
Pitfall 4: Migration baseline mismatch with existing databases
What goes wrong: Migration file 0001 creates the acknowledged_at column, but existing databases already have it (from the current ad-hoc migration). golang-migrate fails with "column already exists".
Why it happens: The baseline migration (0001) must represent the schema of new databases, while the ad-hoc migration (ALTER TABLE updates ADD COLUMN acknowledged_at TEXT) already ran on all existing ones.
How to avoid: Two migration files: 0001_initial_schema.up.sql creates all tables including acknowledged_at (for fresh databases). 0002_acknowledged_at.up.sql is a no-op or empty migration for existing databases that already ran the ALTER TABLE. Actually: since golang-migrate tracks which migrations have run, running 0001 on a new database creates the full schema; it is never run on an existing database that has already been opened by the old binary. The schema_migrations table created by golang-migrate tracks this. The safe approach: 0001 creates all three tables with acknowledged_at included from the start. Old databases that pre-exist migration tracking will need to have golang-migrate's schema_migrations table bootstrapped, but since CREATE TABLE IF NOT EXISTS is used, existing tables are not re-created.
Warning signs: Integration test with a pre-seeded SQLite file fails; startup error "table already exists" or "duplicate column name".
Pitfall 5: export_test.go still references deleted globals
What goes wrong: After removing var db, var mu, var webhookSecret, the export_test.go that calls db.Exec(...) or InitDB(":memory:") directly fails to compile.
Why it happens: export_test.go provides internal access that previously relied on the globals.
How to avoid: Rewrite export_test.go to use NewTestServer() (a test-only constructor that returns a fresh *Server with in-memory DB). All test helpers become methods on *Server or use the public Store interface.
Pitfall 6: INSERT OR REPLACE in TagAssignmentHandler
What goes wrong: The current handler uses INSERT OR REPLACE INTO tag_assignments — this is correct for SQLite but differs from the ON CONFLICT DO UPDATE pattern used in UpdateEvent. The AssignTag Store method should preserve the working behavior, not silently change semantics.
Why it happens: Developer unifies syntax without checking that both approaches are semantically identical for the tag_assignments table.
How to avoid: Keep INSERT OR REPLACE in SQLiteStore.AssignTag (it is correct — tag_assignments has image as PRIMARY KEY so REPLACE works). Document the intent.
Code Examples
Store interface (verified pattern)
// Source: project-derived from current diunwebhook.go SQL operations audit
type Store interface {
UpsertEvent(event DiunEvent) error
GetUpdates() (map[string]UpdateEntry, error)
AcknowledgeUpdate(image string) (found bool, err error)
ListTags() ([]Tag, error)
CreateTag(name string) (Tag, error)
DeleteTag(id int) (found bool, err error)
AssignTag(image string, tagID int) error
UnassignTag(image string) error
TagExists(id int) (bool, error)
}
golang-migrate with embed.FS + modernc/sqlite (verified against pkg.go.dev)
// Source: pkg.go.dev/github.com/golang-migrate/migrate/v4/source/iofs
//go:embed migrations/sqlite
var sqliteMigrations embed.FS
func RunMigrations(db *sql.DB) error {
src, err := iofs.New(sqliteMigrations, "migrations/sqlite")
if err != nil {
return err
}
driver, err := sqlitemigrate.WithInstance(db, &sqlitemigrate.Config{})
if err != nil {
return err
}
m, err := migrate.NewWithInstance("iofs", src, "sqlite", driver)
if err != nil {
return err
}
if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
return err
}
return nil
}
Migration file naming convention
migrations/sqlite/
0001_initial_schema.up.sql -- CREATE TABLE IF NOT EXISTS updates, tags, tag_assignments
0001_initial_schema.down.sql -- DROP TABLE tag_assignments; DROP TABLE tags; DROP TABLE updates
0002_acknowledged_at.up.sql -- (empty or no-op: column exists in 0001 baseline)
0002_acknowledged_at.down.sql -- (empty)
Note on 0002: The current InitDB has an ad-hoc ALTER TABLE updates ADD COLUMN acknowledged_at TEXT. Since 0001 will include acknowledged_at in the CREATE TABLE, file 0002 documents the migration history for databases that were created before this field existed but does not need to run anything — it can contain only a comment. Alternatively, since this is a greenfield migration setup, 0001 can simply include acknowledged_at from the start, making 0002 unnecessary. Single-file baseline (0001 only) is simpler and correct.
Handler method on Server (verified pattern for net/http)
// Source: project CLAUDE.md conventions + stdlib net/http
func (s *Server) WebhookHandler(w http.ResponseWriter, r *http.Request) {
if s.webhookSecret != "" {
auth := r.Header.Get("Authorization")
if subtle.ConstantTimeCompare([]byte(auth), []byte(s.webhookSecret)) != 1 {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
}
if r.Method != http.MethodPost { ... }
// ...
if err := s.store.UpsertEvent(event); err != nil {
log.Printf("WebhookHandler: failed to store event: %v", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
}
SQL Operations Inventory
All current SQL in diunwebhook.go that must move into SQLiteStore methods:
| Current location | Operation | Store method |
|---|---|---|
UpdateEvent() |
UPSERT into updates |
UpsertEvent |
GetUpdates() |
SELECT updates JOIN tags | GetUpdates |
DismissHandler |
UPDATE acknowledged_at |
AcknowledgeUpdate |
TagsHandler GET |
SELECT from tags |
ListTags |
TagsHandler POST |
INSERT into tags |
CreateTag |
TagByIDHandler DELETE |
DELETE from tags |
DeleteTag |
TagAssignmentHandler PUT (check) |
SELECT COUNT from tags |
TagExists |
TagAssignmentHandler PUT (assign) |
INSERT OR REPLACE into tag_assignments |
AssignTag |
TagAssignmentHandler DELETE |
DELETE from tag_assignments |
UnassignTag |
Total: 9 Store methods. All inline SQL moves to SQLiteStore. Handlers call s.store.X(...) only.
State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|---|---|---|---|
| Ad-hoc DDL in application code | Versioned migration files | golang-migrate has been standard since ~2017 | Migration history tracked; dirty-state recovery available |
| Package-level globals for DB | Struct-held dependencies | Standard Go since Go 1.0; best practice since ~2016 | Enables parallel tests, multiple instances |
| CGO SQLite drivers | Pure-Go modernc.org/sqlite |
~2020 | No C toolchain needed; Alpine-friendly |
Deprecated/outdated patterns in this codebase:
var db *sql.DB(package-level): replaced bySQLiteStore.dbfieldvar mu sync.Mutex(package-level): replaced bySQLiteStore.mufieldvar webhookSecret string(package-level): replaced byServer.webhookSecretfieldSetWebhookSecret()function: replaced byNewServer(store, secret)constructorInitDB()function: replaced byRunMigrations()+NewSQLiteStore()export_test.gocallingInitDB(":memory:"): replaced byNewTestServer()constructor
Open Questions
-
Migration 0001 vs 0001+0002 baseline
- What we know: The current schema has
acknowledged_atadded via an ad-hoc migration after initial creation. Two approaches exist: (a) single 0001 migration that creates all tables includingacknowledged_atfrom the start; (b) 0001 creates original schema, 0002 addsacknowledged_at. - What's unclear: Whether any existing deployed databases lack
acknowledged_at. The code has_, _ = db.Exec("ALTER TABLE ... ADD COLUMN acknowledged_at TEXT")which silently ignores errors — meaning every database that ran this code has the column. - Recommendation: Use a single 0001 migration with the full current schema (including
acknowledged_at). Since this is the first time golang-migrate is introduced, all databases are either: (a) new — get full schema from 0001; (b) existing — already haveacknowledged_at, and sinceCREATE TABLE IF NOT EXISTSis used, 0001 is a no-op for the table structures but creates theschema_migrationstracking table. However, golang-migrate does not re-run 0001 just because tables exist — it checksschema_migrations. On an existing DB with noschema_migrationstable, golang-migrate will try to run 0001. If 0001 usesCREATE TABLE IF NOT EXISTS, it succeeds even when tables exist. This is the safe path.
- What we know: The current schema has
-
TagExistsvs inline check inAssignTag- What we know:
TagAssignmentHandlercurrently does aSELECT COUNT(*)before the INSERT. Some designs inline this intoAssignTagand return an error code when the tag is missing. - What's unclear: Whether the
not foundvsinternal errordistinction in the handler is best expressed as a separateTagExistscall or a sentinel error fromAssignTag. - Recommendation: Keep
TagExistsas a separate method matching the current two-step pattern. This keeps the Store methods simple and the handler logic readable. A future refactor can merge them.
- What we know:
Environment Availability
Step 2.6: SKIPPED — this phase is code/configuration-only. All changes are within the Go module already present. No new external services, CLIs, or runtimes are required beyond the existing Go 1.26 toolchain.
Project Constraints (from CLAUDE.md)
The planner MUST verify all generated plans comply with these directives:
| Directive | Source | Applies To |
|---|---|---|
No CGO — use modernc.org/sqlite only |
CLAUDE.md Constraints | golang-migrate sub-package selection |
Pure Go SQLite driver (modernc.org/sqlite) registered as "sqlite" |
CLAUDE.md Key Dependencies | sql.Open("sqlite", path) — never "sqlite3" |
| No ORM or query builder | STATE.md Decisions | All SQLiteStore methods use raw database/sql |
go vet runs in CI; gofmt enforced |
CLAUDE.md Code Style | All new Go files must be gofmt-compliant |
Handler naming pattern: <Noun>Handler |
CLAUDE.md Naming Patterns | Handler methods on Server keep existing names |
Test functions: Test<FunctionName>_<Scenario> |
CLAUDE.md Naming Patterns | New test functions follow this convention |
No barrel files; logic in diunwebhook.go |
CLAUDE.md Module Design | New files within package are fine; no new packages required |
Error messages lowercase: "internal error", "not found" |
CLAUDE.md Error Handling | Handler error strings must not change |
log.Printf with handler name prefix on errors |
CLAUDE.md Logging | e.g., "WebhookHandler: failed to store event: %v" |
| Single-container Docker deploy | CLAUDE.md Deployment | Migrations must run at startup from embedded files — no external migration tool |
| Backward compatible — existing SQLite users upgrade without data loss | CLAUDE.md Constraints | Migration 0001 must use CREATE TABLE IF NOT EXISTS |
Sources
Primary (HIGH confidence)
pkg.go.dev/github.com/golang-migrate/migrate/v4— version v4.19.1 confirmed via Go module proxy on 2026-03-23pkg.go.dev/github.com/golang-migrate/migrate/v4/database/sqlite— confirmed usesmodernc.org/sqlite(pure Go, not CGO)pkg.go.dev/github.com/golang-migrate/migrate/v4/source/iofs—iofs.New(fsys, path)API signature verified- Project source:
pkg/diunwebhook/diunwebhook.go— complete SQL operations inventory derived from direct code read
Secondary (MEDIUM confidence)
github.com/golang-migrate/migrate/blob/master/database/sqlite/README.md— confirms modernc.org/sqlite driver and pure-Go status
Tertiary (LOW confidence)
- WebSearch results on Go Store interface patterns — general patterns verified against known stdlib conventions; no single authoritative source
Metadata
Confidence breakdown:
- Standard stack: HIGH — golang-migrate version confirmed from Go proxy; sqlite sub-package driver verified from pkg.go.dev
- Architecture (Store interface, Server struct): HIGH — derived directly from auditing current source code; all 9 operations enumerated
- Migration design: HIGH — iofs API verified; ErrNoChange behavior documented in pkg.go.dev
- Pitfalls: HIGH — CGO pitfall verified by checking sqlite vs sqlite3 sub-packages; other pitfalls derived from code analysis
Research date: 2026-03-23 Valid until: 2026-09-23 (golang-migrate is stable; modernc.org/sqlite API is stable)