# 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. ```go // 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. ```go // 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: ```go // 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. ```go 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. ```go // 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.