Files
DiunDashboard/.planning/phases/02-backend-refactor/02-RESEARCH.md

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_URL present activates PostgreSQL; absent falls back to SQLite with DB_PATH — no separate DB_DRIVER variable (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 Server lives in the same package as Store or 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

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(...) — never s.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_at column). 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 adds acknowledged_at to represent the already-run ad-hoc migration.
  • Calling m.Up() and treating ErrNoChange as fatal: Always check err != migrate.ErrNoChange before returning an error from RunMigrations.
  • Removing PRAGMA foreign_keys = ON during refactor: The SQLite connection setup must still run this pragma. Move it from InitDB into NewSQLiteStore or the connection-open step in main.go.
  • Replacing db.SetMaxOpenConns(1) with nothing: This setting prevents concurrent write contention in SQLite. It must be preserved on the *sql.DB instance passed to NewSQLiteStore.

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 by SQLiteStore.db field
  • var mu sync.Mutex (package-level): replaced by SQLiteStore.mu field
  • var webhookSecret string (package-level): replaced by Server.webhookSecret field
  • SetWebhookSecret() function: replaced by NewServer(store, secret) constructor
  • InitDB() function: replaced by RunMigrations() + NewSQLiteStore()
  • export_test.go calling InitDB(":memory:"): replaced by NewTestServer() constructor

Open Questions

  1. Migration 0001 vs 0001+0002 baseline

    • What we know: The current schema has acknowledged_at added via an ad-hoc migration after initial creation. Two approaches exist: (a) single 0001 migration that creates all tables including acknowledged_at from the start; (b) 0001 creates original schema, 0002 adds acknowledged_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 have acknowledged_at, and since CREATE TABLE IF NOT EXISTS is used, 0001 is a no-op for the table structures but creates the schema_migrations tracking table. However, golang-migrate does not re-run 0001 just because tables exist — it checks schema_migrations. On an existing DB with no schema_migrations table, golang-migrate will try to run 0001. If 0001 uses CREATE TABLE IF NOT EXISTS, it succeeds even when tables exist. This is the safe path.
  2. TagExists vs inline check in AssignTag

    • What we know: TagAssignmentHandler currently does a SELECT COUNT(*) before the INSERT. Some designs inline this into AssignTag and return an error code when the tag is missing.
    • What's unclear: Whether the not found vs internal error distinction in the handler is best expressed as a separate TagExists call or a sentinel error from AssignTag.
    • Recommendation: Keep TagExists as 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.

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-23
  • pkg.go.dev/github.com/golang-migrate/migrate/v4/database/sqlite — confirmed uses modernc.org/sqlite (pure Go, not CGO)
  • pkg.go.dev/github.com/golang-migrate/migrate/v4/source/iofsiofs.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)