data-access-patterns
Guidelines for database interactions using raw SQL and idiomatic Go patterns
SKILL.md
Full skill instructions
Data Access Patterns Skill
Use this skill when writing or modifying database access code in internal/models/.
This project uses explicit SQL over ORMs for performance, readability, and fine-grained control.
Core Principles
- No ORM: Use
database/sqldirectly. Do NOT introduce or use any ORM. - PostgreSQL Driver: The project uses
github.com/lib/pq(imported ininternal/db/db.go). - Raw SQL: Write explicit, readable SQL queries. Use PostgreSQL-specific features (JSONB, ON CONFLICT) and
$1, $2placeholders. - Transactional Integrity: Use
*sql.Txwhen multiple operations must complete together or fail (atomicity). - Separation of Concerns: Keep database models and access logic in
internal/models/. Repository functions should focus on data retrieval and persistence.
Repository Function Naming
| Pattern | Purpose | Example Signature |
|---|---|---|
Get[Entity]ById | Retrieve a single entity | GetWeaponById(db *sql.DB, id string) (*Weapon, error) |
Get[Entities]By[Field] | Filtered retrieval | GetTraderOffersByItemID(db *sql.DB, itemID string) ([]TraderOffer, error) |
Upsert[Entity] | Insert or update one record | UpsertWeapon(tx *sql.Tx, weapon Weapon) error |
UpsertMany[Entity] | Batch insert/update | UpsertManyWeapon(tx *sql.Tx, weapons []Weapon) error |
Purge[Entity] | Clean up records | PurgeOptimumBuilds(db *sql.DB) error |
Writing Efficient Queries
JSONB for Complex Trees
When fetching an entity with nested children (e.g., a weapon with many slots), use jsonb_agg and jsonb_build_object to minimize round-trips and simplify Go-side scanning.
// Example: Single query to fetch weapon and all its slots
query := `
SELECT w.name,
w.item_id,
jsonb_agg(jsonb_build_object(
'slot_id', ws.slot_id,
'name', ws.name
)) as slots
FROM weapons w
JOIN slots ws ON w.item_id = ws.item_id
WHERE w.item_id = $1
GROUP BY w.name, w.item_id;`
Idempotent Writes (ON CONFLICT)
Prefer ON CONFLICT for upsert operations to ensure idempotency and handle existing records gracefully.
query := `
INSERT INTO weapons (item_id, name)
VALUES ($1, $2)
ON CONFLICT (item_id) DO UPDATE SET
name = EXCLUDED.name;`
Transaction Management Checklist
When implementing writes:
- Composite Writes: If a function calls multiple other write functions (e.g.,
UpsertWeaponcallingupsertManySlot), it MUST accept a*sql.Tx. - Top-Level Orchestration: Start the transaction at the highest possible level (usually in the importer or service layer).
- Defer Rollback: Defer a rollback immediately after starting a transaction to prevent leaks on error.
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback() // Safe: does nothing if committed
if err := UpsertManyWeapon(tx, weapons); err != nil {
return err
}
return tx.Commit()
Developer Best Practices
- ✅ Explicit Scanning: Scan rows into structs carefully; match types exactly with the DB schema.
- ✅ Resource Management:
defer rows.Close()immediately after aQuerycall. - ✅ Strict Errors: Check errors after
rows.Scan()AND after the loop withrows.Err(). - ✅ Clean Signatures: Pass
*sql.DBfor read-only operations and*sql.Txfor multi-step write operations. - ❌ **Avoid SELECT ***: Explicitly list required columns to prevent breakage from schema changes.
- ❌ No Global DB: Pass the database handle as an argument.
