Knowledge
Search knowledge... ⌘K
Knowledge · Guidelines · go
Type Safe Persistence
Enforce compile-time type safety for all database CRUD wrappers — no map[string]any, no string field names, no untyped values
Metadata
go go recommended
Procedures
Showing 3 of 5
- 1 Define typed Field descriptors for every database column
Every entity model MUST define a Fields struct where each member is a Field[T] carrying the column name and Go type. This enables compile-time checked filtering — Field[int64].GTE("string") won't compile. Field descriptors are defined ONCE per entity, next to the model struct.// Field[T] — typed column descriptor (compile-time safe) type Field[T any] struct{ Column string } func (f Field[T]) Eq(val T) Predicate { return Predicate{f.Column, OpEq, val} } func (f Field[T]) GTE(val T) Predicate { return Predicate{f.Column, OpGTE, val} } func (f Field[T]) LTE(val T) Predicate { return Predicate{f.Column, OpLTE, val} } func (f Field[T]) In(vals []T) Predicate { /* ... */ } // Per-entity Fields struct — compile-time column registry var PropertyFields = struct { PriceAmount Field[int64] TransactionType Field[string] IsPublished Field[bool] City Field[string] }{ PriceAmount: Field[int64]{Column: "price_amount"}, TransactionType: Field[string]{Column: "transaction_type"}, IsPublished: Field[bool]{Column: "is_published"}, City: Field[string]{Column: "city"}, } // COMPILES: Field[int64].GTE(int64) store.List(ctx, PropertyFields.PriceAmount.GTE(500000000)) // WON'T COMPILE: Field[int64].GTE(string) store.List(ctx, PropertyFields.PriceAmount.GTE("wrong type")) - 2 Use typed param structs for Insert and Update — never map[string]any
Each entity defines dedicated Insert and Update param structs with concrete field types. The Model interface methods return these typed structs, not map[string]any. This ensures column-value type alignment at compile time.
// Typed param structs — one per operation type PropertyInsert struct { Title string TransactionType string PropertyType string IsPublished bool PriceAmount int64 PriceCurrency string CoverImageID *string CoverImageURL *string } type PropertyUpdate struct { Title *string // nil = no change IsPublished *bool PriceAmount *int64 PriceCurrency *string } // Model interface — typed, not map[string]any type Model[I any, U any] interface { Table() string PrimaryKey() (column string, value any) InsertParams() I UpdateParams() U ScanRow(scanner RowScanner) error } - 3 Use struct tag or explicit column mapping for row scanning — never positional
Row scanning MUST map columns to struct fields by name, not by position. Use pgx RowToStructByName with `db` tags, or explicit named mapping in ScanRow. Positional scanning breaks silently when column order changes.
// CORRECT: Named field mapping with db tags type Property struct { ID string `db:"id"` Title string `db:"title"` PriceAmount int64 `db:"price_amount"` PriceCurrency string `db:"price_currency"` IsPublished bool `db:"is_published"` } // pgx named scanning — resilient to column reordering rows, _ := db.Query(ctx, "SELECT id, title, price_amount, ...") props, err := pgx.CollectRows(rows, pgx.RowToStructByName[Property]) // WRONG: Positional scanning — breaks if columns reorder // rows.Scan(&p.ID, &p.Title, &p.PriceAmount) // fragile
Tools
- pkg/crud— Generic typed CRUD Store[M] with Field[T] predicates
- pkg/types— Compound type contracts (Money, ImageRef) with typed expansion
- pkg/database— pgx pool wrapper for complex typed queries via pgx.CollectRows
- go vet / go build— Compile-time verification — Field[T] type mismatches caught here
References
- external go-jet typed column expressions
- external sqlc typed param structs
- external incident.io Partial[T] pattern
- external sq type-safe query builder
- external Andrew Pillar generic Store[M]
- external JPA @Embeddable + @AttributeOverride
- external EF Core 8 ComplexType
- external GORM embedded + embeddedPrefix