Guidelines for database interactions using raw SQL and idiomatic Go patterns
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.
database/sql directly. Do NOT introduce or use any ORM.github.com/lib/pq (imported in internal/db/db.go).$1, $2 placeholders.*sql.Tx when multiple operations must complete together or fail (atomicity).internal/models/. Repository functions should focus on data retrieval and persistence.| 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 |
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;`
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;`
When implementing writes:
UpsertWeapon calling upsertManySlot), it MUST accept a *sql.Tx.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()
defer rows.Close() immediately after a Query call.rows.Scan() AND after the loop with rows.Err().*sql.DB for read-only operations and *sql.Tx for multi-step write operations.