← GophersCONTENT HISTORY

Update to Gophers

Snapshot Sep 30, 2026 · 23:14 UTC · version 0.1.0

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

Download comparison JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "description": "Use when writing, reviewing, or debugging Go code that talks to a SQL database (PostgreSQL, MySQL, MariaDB, SQLite). Covers library choice (database/sql, sqlx, sqlc, pgx, GORM trade-offs), parameterized queries, context propagation, NULL handling, scanning, transactions and isolation, connection pool tuning, and migration tooling. Apply when adding repository code, refactoring SQL, or auditing for missing rows.Close()/QueryContext.",
  "included_files": [
    {
      "relative_path": "agents/openai.yaml",
      "size_in_bytes": 197
    },
    {
      "relative_path": "references/anti-patterns.md",
      "size_in_bytes": 4912
    },
    {
      "relative_path": "references/library-tradeoffs.md",
      "size_in_bytes": 5194
    },
    {
      "relative_path": "references/scanning.md",
      "size_in_bytes": 5227
    },
    {
      "relative_path": "references/transactions.md",
      "size_in_bytes": 5549
    }
  ],
  "name": "go-database",
  "skill_md_contents": "---\nname: go-database\ndescription: \"Use when writing, reviewing, or debugging Go code that talks to a SQL database (PostgreSQL, MySQL, MariaDB, SQLite). Covers library choice (database/sql, sqlx, sqlc, pgx, GORM trade-offs), parameterized queries, context propagation, NULL handling, scanning, transactions and isolation, connection pool tuning, and migration tooling. Apply when adding repository code, refactoring SQL, or auditing for missing rows.Close()/QueryContext.\"\nlicense: MIT\ncompatibility: \"Designed for Claude Code or similar AI coding agents. Requires Go 1.21+. Library-agnostic: applies to database/sql, sqlx, sqlc, pgx.\"\nallowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*)\n---\n\n# Go Database\n\nGo's `database/sql` is a thin, driver-pluggable foundation. Most projects layer one of `sqlx`, `sqlc`, or `pgx` on top for ergonomics. ORMs (GORM, ent) trade SQL visibility for one less line of code — a bad trade in production.\n\n## Core Rules\n\n1. **SQL is the source of truth.** It is reviewed, version-controlled, and explained in code. Magic ORM queries are the opposite.\n2. **Always parameterize.** `$1`/`?` placeholders, never string concatenation. The driver handles escaping; you cannot.\n3. **Every I/O call takes `ctx`.** `QueryContext`, `ExecContext`, `GetContext`. No context = no timeout = a stuck handler.\n4. **Distinguish \"not found\" from \"error\".** `errors.Is(err, sql.ErrNoRows)` is a domain signal, not a failure.\n5. **Close rows.** `defer rows.Close()` immediately after `QueryContext`. Forgetting it leaks a pool connection.\n6. **Configure the pool.** Default `MaxOpenConns` is unlimited — a runaway request rate exhausts the DB.\n\n## Library Decision\n\n| Library | Best for | Struct scanning | Code-gen |\n|---|---|---|---|\n| `database/sql` | Minimal deps, multi-driver portability | Manual `Scan` | No |\n| `sqlx` | Sweetens `database/sql` ergonomics | `StructScan`, `Get`, `Select` | No |\n| `sqlc` | Type-safe queries derived from `.sql` files | Generated structs and funcs | Yes |\n| `pgx` (v5) | PostgreSQL-only, 30-50% faster, native types | `pgx.RowToStructByName` | No |\n| GORM / ent | **Avoid** in new code | Reflection | Yes |\n\n**Why not ORMs.**\n\n- Generated queries are unpredictable; N+1 problems are invisible at the call site.\n- Hooks (`BeforeCreate`, `AfterUpdate`) create implicit state machines.\n- Schema migrations entangle with application code.\n- Learning the ORM API is harder than learning SQL, and the abstraction leaks at every interesting query.\n\n> Read [references/library-tradeoffs.md](references/library-tradeoffs.md) when picking between sqlx, sqlc, and pgx for a new project.\n\n## Parameterized Queries\n\n```go\n// VERY BAD — SQL injection.\nq := fmt.Sprintf(\"SELECT * FROM users WHERE email = '%s'\", email)\n\n// Good — placeholder, driver-escaped.\nerr := db.GetContext(ctx, &u, \"SELECT id, email FROM users WHERE email = $1\", email)\n```\n\n### Dynamic `IN` clauses\n\n```go\nq, args, err := sqlx.In(\"SELECT * FROM users WHERE id IN (?)\", ids)\nif err != nil { return fmt.Errorf(\"expanding IN: %w\", err) }\nq = db.Rebind(q)                            // $1, $2, ... for Postgres\nerr = db.SelectContext(ctx, &users, q, args...)\n```\n\n### Dynamic column names\n\nPlaceholders cannot stand in for identifiers. Use an allowlist:\n\n```go\nallowed := map[string]bool{\"name\": true, \"email\": true, \"created_at\": true}\nif !allowed[sortCol] {\n    return fmt.Errorf(\"invalid sort column: %s\", sortCol)\n}\nq := fmt.Sprintf(\"SELECT id, name FROM users ORDER BY %s\", sortCol)\n```\n\n## Context Propagation\n\n```go\n// Bad — query runs to completion even if the client disconnected.\nrows, err := db.Query(\"SELECT ...\")\n\n// Good — driver cancels the query on ctx.Done().\nrows, err := db.QueryContext(ctx, \"SELECT ...\")\n```\n\nEvery I/O method takes `ctx` first. Pass the request context through service → repository.\n\n## Error Handling\n\n```go\nerr := r.db.GetContext(ctx, &u, \"SELECT ... WHERE id = $1\", id)\nswitch {\ncase errors.Is(err, sql.ErrNoRows):\n    return nil, ErrUserNotFound           // domain error\ncase err != nil:\n    return nil, fmt.Errorf(\"get user %s: %w\", id, err)\n}\n```\n\n### Always close rows\n\n```go\nrows, err := db.QueryContext(ctx, \"SELECT id, name FROM users\")\nif err != nil { return fmt.Errorf(\"query: %w\", err) }\ndefer rows.Close()\nfor rows.Next() {\n    var u User\n    if err := rows.Scan(&u.ID, &u.Name); err != nil { return fmt.Errorf(\"scan: %w\", err) }\n    users = append(users, u)\n}\nif err := rows.Err(); err != nil { return fmt.Errorf(\"iterate: %w\", err) }\n```\n\nThree error checks (`Query`, `Scan`, `rows.Err()`) — missing the third hides truncated iteration.\n\n## NULL Columns and Scanning\n\n```go\ntype User struct {\n    ID    string         `db:\"id\"`\n    Email string         `db:\"email\"`\n    Bio   *string        `db:\"bio\"`      // nullable → pointer\n    Login sql.NullTime   `db:\"last_login\"`\n}\n```\n\nPointer fields work cleanly with JSON marshaling and with sqlx `StructScan`. Use `sql.NullXxx` when you need to distinguish \"not set\" from \"zero value\" at the SQL layer.\n\n> Read [references/scanning.md](references/scanning.md) for sqlx tags, pgx `RowToStructByName`, and `sql.Null*` patterns.\n\n## Transactions and Isolation\n\nWrap related writes in `db.BeginTxx(ctx, &sql.TxOptions{Isolation: ...})`, rollback on every error path, commit only on success. Use `SELECT ... FOR UPDATE` when reading data you intend to modify — otherwise a concurrent writer races you. See [references/transactions.md](references/transactions.md) for isolation levels, retryable serialization errors, and the UnitOfWork pattern.\n\n## Connection Pool\n\n```go\ndb.SetMaxOpenConns(25)\ndb.SetMaxIdleConns(10)\ndb.SetConnMaxLifetime(5 * time.Minute)\ndb.SetConnMaxIdleTime(1 * time.Minute)\n```\n\n`MaxOpenConns` should be ≤ the DB server's `max_connections` divided by replica count, with headroom for migrations and other consumers.\n\n## Migrations\n\nDo **not** generate migration SQL with this skill. Schema design needs human judgment about indexes, foreign keys, and data volume.\n\nRecommended tools:\n\n- [golang-migrate](https://github.com/golang-migrate/migrate) — Go library + CLI.\n- [Atlas](https://atlasgo.io/) — declarative, supports diff and lint.\n- [Flyway](https://flywaydb.org/) — JVM, common in heterogeneous shops.\n\nRun migrations in CI/CD, not from application code at startup.\n\n## Avoid Hidden SQL Features\n\nTriggers, views, materialized views, stored procedures, row-level security — all create invisible state changes. The application code looks correct; debugging takes hours. Keep behavior in Go where it is testable and reviewable.\n\n## Anti-Patterns\n\n| Anti-pattern | Why it hurts | Do this instead |\n|---|---|---|\n| `fmt.Sprintf` building queries | SQL injection | Always `$1`/`?` placeholders |\n| `db.Query` (no context) | No timeout, no cancellation | `db.QueryContext(ctx, ...)` |\n| Forgetting `defer rows.Close()` | Connection leak; pool exhaustion | Defer immediately after `QueryContext` |\n| Missing `rows.Err()` check | Truncated iteration treated as success | Always check after the `for rows.Next()` loop |\n| `db.Query` for INSERT/UPDATE/DELETE | `*Rows` must be closed; easy to leak | Use `db.ExecContext` |\n| Returning raw `*sql.DB` from repos | Couples service to driver | Return domain types; keep `*sql.DB` private |\n| ORM with hooks for business logic | Magic side effects, untraceable bugs | Move logic into a service layer |\n| `MaxOpenConns(0)` (unlimited) | Stampede exhausts the DB | Cap below `pg_max_connections` |\n\n## Verification Checklist\n\n- [ ] No string-concatenated SQL\n- [ ] Every DB call uses a `*Context` method\n- [ ] Every `QueryContext` is followed by `defer rows.Close()`\n- [ ] Every iteration loop ends with `rows.Err()` check\n- [ ] `sql.ErrNoRows` is translated to a domain error at the repository boundary\n- [ ] Connection pool limits set; not relying on defaults\n- [ ] Transactions roll back on every error path; commit only on success\n- [ ] Migrations live in `migrations/`, not in Go code\n\n## References\n\n- [references/library-tradeoffs.md](references/library-tradeoffs.md) — sqlx vs sqlc vs pgx vs GORM with concrete examples\n- [references/transactions.md](references/transactions.md) — isolation levels, FOR UPDATE, retryable errors\n- [references/scanning.md](references/scanning.md) — sqlx, pgx, NULL handling, JSON columns\n- [references/anti-patterns.md](references/anti-patterns.md) — detailed walkthrough of each anti-pattern\n"
}

SHA-256 of public snapshot: 32abb8072e0930aadc610cc86345ce0d113f2eb018541cd141341c2b669fe897