Files
bible/rules/patterns/go-database/contract.md
2026-03-07 21:56:51 +03:00

4.5 KiB

Contract: Database Patterns (Go / MySQL / MariaDB)

Version: 1.5

MySQL Transaction Cursor Safety (CRITICAL)

Never execute SQL on the same transaction while iterating over a query result cursor.

This is the most common source of invalid connection and unexpected EOF driver panics.

Rule

Use a two-phase approach: read all rows first, close the cursor, then execute writes.

// WRONG — executes SQL inside rows.Next() loop on the same tx
rows, _ := tx.Query("SELECT id FROM machines")
for rows.Next() {
    var id string
    rows.Scan(&id)
    tx.Exec("UPDATE machines SET processed=1 WHERE id=?", id) // DEADLOCK / driver panic
}

// CORRECT — collect IDs first, then write
rows, _ := tx.Query("SELECT id FROM machines")
var ids []string
for rows.Next() {
    var id string
    rows.Scan(&id)
    ids = append(ids, id)
}
rows.Close() // explicit close before any write

for _, id := range ids {
    tx.Exec("UPDATE machines SET processed=1 WHERE id=?", id)
}

This applies to:

  • database/sql with manual transactions
  • GORM db.Raw().Scan() inside a db.Transaction() callback
  • Any loop that calls a repository method while a cursor is open

Soft Delete / Archive Pattern

Do not use hard deletes for user-visible records. Use an archive flag.

// Schema: is_active bool DEFAULT true
// "Delete" = set is_active = false
// Restore = set is_active = true

// All list queries must filter:
WHERE is_active = true
  • Never physically delete rows that have foreign key references or history.
  • Hard delete is only acceptable for orphaned/temporary data with no audit trail requirement.
  • Archive operations must be reversible from the UI.

GORM Virtual Fields

Use the correct tag based on whether the field should exist in the DB schema:

// Field computed at runtime, column must NOT exist in DB (excludes from migrations AND queries)
Count int `gorm:"-"`

// Field computed at query time via JOIN/SELECT, column must NOT be in migrations
// but IS populated from query results
DisplayName string `gorm:"-:migration"`
  • gorm:"-" — fully ignored: no migration, no read, no write.
  • gorm:"-:migration" — skip migration only; GORM will still read/write if the column exists.
  • Do not use gorm:"-" for JOIN-populated fields — the value will always be zero.

Fail-Fast DB Check on Startup

Always verify the database connection before starting the HTTP server.

sqlDB, err := db.DB()
if err != nil || sqlDB.Ping() != nil {
    log.Fatal("database unavailable, refusing to start")
}
// then: run migrations, then: start gin/http server

Never start serving traffic with an unverified DB connection. Fail loudly at boot.

N+1 Query Prevention

Use JOINs or batch IN queries. Never query inside a loop over rows from another query.

// WRONG
for _, pricelist := range pricelists {
    items, _ := repo.GetItems(pricelist.ID) // N queries
}

// CORRECT
items, _ := repo.GetItemsByPricelistIDs(ids) // 1 query with WHERE id IN (...)
// then group in Go

Backup Before Any DB Change

Any operation that changes persisted database state must have a fresh backup taken immediately before execution.

This applies to:

  • Go migrations
  • Manual SQL runbooks
  • Data backfills and repair scripts
  • Imports, bulk updates, and bulk deletes
  • Admin tools or one-off operator commands

Backup naming, storage, archive format, retention, and restore-readiness must follow the backup-management contract.

Rules:

  • No schema change or data mutation is allowed on a non-ephemeral database without a current backup.
  • "Small" or "safe" changes are not exceptions.
  • The operator must know how to restore from that backup before applying the change.
  • If a migration or script is intended for production/staging, the rollout instructions must state the backup step explicitly.
  • The backup taken before a migration must be triggered by the application's own backup mechanism, not by assuming mysql, mysqldump, or other DB client tools exist on the user's machine.

Migration Policy

  • Migrations are numbered sequentially and never modified after merge.
  • Trigger, take, and verify a fresh backup through the application-owned backup mechanism before applying migrations to any non-ephemeral database.
  • Each migration must be reversible where possible (document rollback in a comment).
  • Never rename a column in one migration step — add new, backfill, drop old across separate deploys.
  • Auto-apply migrations on startup is acceptable for internal tools; document if used.