Documentation
¶
Overview ¶
Package database provides a high-performance query builder, struct scanner, lifecycle hooks, and explicit eager relationship loading for OniWorks.
Design principles:
- Query builder is lazy — nothing executes until a terminal method is called
- Reflection cache: struct field mapping is computed once per type, never per-request
- No lazy loading: relationships are never populated automatically (no hidden queries)
- Batch eager loading: db.Load(&users, "Posts") fires WHERE user_id IN (...) — no N+1
- Context-everywhere: all queries accept context.Context
Index ¶
- Variables
- func SetDefault(db *DB)
- type Builder
- func (b *Builder) All(dest any) error
- func (b *Builder) Avg(col string) (float64, error)
- func (b *Builder) Chunk(size int, fn func(batch []map[string]any) error) error
- func (b *Builder) ChunkByID(size int, fn func(batch []map[string]any) error) error
- func (b *Builder) Count() (int64, error)
- func (b *Builder) Ctx(ctx context.Context) *Builder
- func (b *Builder) CursorPaginate(col string, after any, perPage int, dest any) (any, error)
- func (b *Builder) Decrement(col string, by int) error
- func (b *Builder) Delete() error
- func (b *Builder) Exec() error
- func (b *Builder) Exists() (bool, error)
- func (b *Builder) First(dest any) error
- func (b *Builder) FirstOrCreateMap(match Map, extra Map, dest any) error
- func (b *Builder) ForceDelete() error
- func (b *Builder) GroupBy(cols ...string) *Builder
- func (b *Builder) GroupByRaw(expr string) *Builder
- func (b *Builder) Having(clause string, args ...any) *Builder
- func (b *Builder) Increment(col string, by int) error
- func (b *Builder) Insert(dest any) error
- func (b *Builder) InsertMany(src any) error
- func (b *Builder) InsertMap(data map[string]any) error
- func (b *Builder) InsertMaps(rows []map[string]any) error
- func (b *Builder) Join(clause string) *Builder
- func (b *Builder) LeftJoin(clause string) *Builder
- func (b *Builder) Limit(n int) *Builder
- func (b *Builder) Max(col string) (float64, error)
- func (b *Builder) Min(col string) (float64, error)
- func (b *Builder) Offset(n int) *Builder
- func (b *Builder) OnlyTrashed() *Builder
- func (b *Builder) OrWhere(clause string, args ...any) *Builder
- func (b *Builder) OrWhereGroup(fn func(*Builder)) *Builder
- func (b *Builder) OrderBy(clause string) *Builder
- func (b *Builder) OrderByRaw(clause string) *Builder
- func (b *Builder) Paginate(page, perPage int, dest any) (*Page[any], error)
- func (b *Builder) Pluck(col string, dest any) error
- func (b *Builder) Restore() error
- func (b *Builder) Save(dest any) error
- func (b *Builder) Scan(dest any) error
- func (b *Builder) Select(cols ...string) *Builder
- func (b *Builder) SelectRaw(expr string) *Builder
- func (b *Builder) SoftDelete() *Builder
- func (b *Builder) Sum(col string) (float64, error)
- func (b *Builder) ToSQL() (string, []any, error)
- func (b *Builder) Update(data Map) error
- func (b *Builder) UpdateOrCreateMap(match Map, values Map) error
- func (b *Builder) UpsertMap(data map[string]any, conflictCols []string, updateCols []string) error
- func (b *Builder) Where(clause string, args ...any) *Builder
- func (b *Builder) WhereGroup(fn func(*Builder)) *Builder
- func (b *Builder) WhereIn(col string, values ...any) *Builder
- func (b *Builder) WhereInSub(col string, sub *Builder) *Builder
- func (b *Builder) WhereNotIn(col string, values ...any) *Builder
- func (b *Builder) WhereNotNull(col string) *Builder
- func (b *Builder) WhereNull(col string) *Builder
- func (b *Builder) WhereRaw(clause string, args ...any) *Builder
- func (b *Builder) With(relations ...string) *Builder
- func (b *Builder) WithTrashed() *Builder
- type Config
- type DB
- func (db *DB) Close() error
- func (db *DB) Driver() Driver
- func (db *DB) Load(dest any, relations ...string) error
- func (db *DB) LoadContext(ctx context.Context, dest any, relations ...string) error
- func (db *DB) Ping(ctx context.Context) error
- func (db *DB) QuoteIdentifier(s string) string
- func (db *DB) Raw(sql string, args ...any) *Builder
- func (db *DB) SQLDB() *sql.DB
- func (db *DB) SetLogLevel(level slog.Level)
- func (db *DB) Table(table string) *Builder
- func (db *DB) Transaction(fn func(tx *DB) error) error
- func (db *DB) TransactionContext(ctx context.Context, fn func(tx *DB) error) error
- func (db *DB) WithLogger(l *slog.Logger) *DB
- type Driver
- type Grammar
- type Map
- type Page
Constants ¶
This section is empty.
Variables ¶
var ErrNotFound = fmt.Errorf("database: record not found")
ErrNotFound is returned by First when no matching row is found.
Functions ¶
func SetDefault ¶
func SetDefault(db *DB)
SetDefault sets the package-level default DB used by top-level Table() / Raw() calls.
Types ¶
type Builder ¶
type Builder struct {
// contains filtered or unexported fields
}
Builder is a lazy, fluent SQL query builder. Nothing is executed until a terminal method (First, All, Count, Insert, Update, etc.) is called.
func (*Builder) All ¶
All executes the query and scans all rows into dest (must be a pointer to a slice).
func (*Builder) Avg ¶
Avg executes AVG(col) over the current query and returns the result. Returns 0 when no rows match (SQL NULL). col is validated and quoted.
func (*Builder) Chunk ¶
Chunk iterates the query in LIMIT/OFFSET batches of size rows, invoking fn for each batch until the result set is exhausted or fn returns an error (which stops iteration and is returned). An ORDER BY is required — without a deterministic order, OFFSET pagination can skip or repeat rows.
Rows written while chunking runs can still shift offsets between batches; prefer ChunkByID when the table has a numeric id column.
func (*Builder) ChunkByID ¶
ChunkByID iterates the query in keyset batches — WHERE id > last ORDER BY id LIMIT size — which stays correct and fast even when rows are inserted or deleted mid-iteration. The table must have a column named "id" with a sortable, unique value; any ORDER BY set on the builder is replaced by "id ASC".
func (*Builder) CursorPaginate ¶
CursorPaginate performs single-column keyset pagination: WHERE col > after ORDER BY col ASC LIMIT perPage+1. dest must be a pointer to a slice (of structs or map[string]any) and receives at most perPage rows. When more rows exist, the col value of the last returned row is returned as nextCursor — pass it as after on the next call. nextCursor is nil on the final page. Pass after == nil for the first page.
col must be unique (or effectively unique) for stable pagination, and is validated and quoted like every other identifier.
next, err := db.Table("posts").CursorPaginate("id", nil, 20, &posts)
func (*Builder) Decrement ¶
Decrement atomically subtracts by from col for all rows matching the WHERE clause. It is Increment with a negated amount, so the generated SQL is "SET col = col + ?" with a negative bind value.
func (*Builder) Delete ¶
Delete removes rows matching the WHERE clause.
db.Table("users").Where("id = ?", id).Delete()
func (*Builder) Exec ¶
Exec executes a raw write query (INSERT, UPDATE, DELETE) that returns no rows. It requires a Raw() builder — calling Exec on a Table() builder returns an error rather than guessing at a statement (previously it fell back to an unguarded DELETE).
func (*Builder) Exists ¶
Exists reports whether any row matches the query. It issues SELECT EXISTS(SELECT 1 FROM ... WHERE ...) — on both Postgres and MySQL the engine can stop at the first matching row, which is much cheaper than COUNT(*) on large tables.
func (*Builder) First ¶
First executes the query and scans a single row into dest. Returns ErrNotFound if no row matches.
func (*Builder) FirstOrCreateMap ¶
FirstOrCreateMap finds the first row matching all match columns and scans it into dest. If no row matches, it inserts match merged with extra (extra wins on key collisions, timestamps stamped like InsertMap) and re-selects the row into dest so database defaults and generated ids are populated.
NOT atomic: a concurrent insert between the SELECT and the INSERT can create a duplicate (or make the INSERT fail on a unique index). For an atomic guarantee, add a unique index over the match columns and use UpsertMap instead.
func (*Builder) ForceDelete ¶
ForceDelete permanently removes rows even on a soft-delete builder.
db.Table("posts").SoftDelete().Where("id = ?", id).ForceDelete()
func (*Builder) GroupBy ¶
GroupBy adds a GROUP BY clause. Each argument must be a "[table.]column" reference and is validated and quoted to prevent SQL injection. Use GroupByRaw for trusted raw expressions.
func (*Builder) GroupByRaw ¶
GroupByRaw adds a raw, unescaped GROUP BY expression. The caller is responsible for safety — never pass user input directly.
func (*Builder) Having ¶
Having adds a HAVING clause. Repeated calls accumulate and are AND-joined, like Where. The clause is raw, unescaped SQL — the caller is responsible for ensuring it is safe. Never pass user input directly; use ? placeholders for values.
db.Table("orders").GroupBy("user_id").Having("COUNT(*) > ?", 5)
func (*Builder) Increment ¶
Increment atomically adds by to col for all rows matching the WHERE clause:
UPDATE t SET col = col + ? WHERE ...
The soft-delete scope is honored like Update. Like Delete/Update, it refuses to run without a WHERE clause — use WhereRaw("1=1") to hit all rows.
db.Table("posts").Where("id = ?", id).Increment("views", 1)
func (*Builder) Insert ¶
Insert inserts dest (a model pointer) and calls BeforeCreate / AfterCreate hooks. dest must be a struct pointer; for map[string]any use InsertMap instead.
func (*Builder) InsertMany ¶
InsertMany inserts a slice of structs (or struct pointers) in a single multi-row VALUES statement, reusing the same field metadata as Insert. Auto-increment primary keys are skipped like Insert, and created_at / updated_at time.Time fields are stamped on each element.
Unlike Insert, lifecycle hooks are NOT invoked and auto-increment IDs are NOT backfilled onto the structs (drivers only report the first inserted id) — use Insert per model when you need either.
func (*Builder) InsertMap ¶
InsertMap inserts a row from a plain map[string]any. Column order is sorted for determinism. Hooks are not called. "created_at" and "updated_at" are injected as UTC time.Time values when not already present in the map, so each driver formats the timestamp natively (MySQL DATETIME rejects RFC3339 strings in strict mode).
func (*Builder) InsertMaps ¶
InsertMaps inserts multiple rows in a single multi-row VALUES statement. All rows must have exactly the same keys — a mismatched row is an error rather than a guess (a union of keys would silently insert NULL/zero values for the missing columns). Column order is sorted for determinism. "created_at"/"updated_at" are stamped on every row like InsertMap when not already present. Hooks are not called. Inserting an empty slice is a no-op.
func (*Builder) Join ¶
Join adds an INNER JOIN. The clause is raw, unescaped SQL — the caller is responsible for ensuring it is safe. Never pass user input directly.
db.Table("posts").Join("users ON users.id = posts.user_id")
func (*Builder) LeftJoin ¶
LeftJoin adds a LEFT JOIN. The clause is raw, unescaped SQL — the caller is responsible for ensuring it is safe. Never pass user input directly.
func (*Builder) Max ¶
Max executes MAX(col) over the current query and returns the result. The column must be numeric — MAX over text or date columns cannot be represented as float64 and returns a conversion error. Returns 0 when no rows match (SQL NULL).
func (*Builder) Min ¶
Min executes MIN(col) over the current query and returns the result. The column must be numeric — MIN over text or date columns cannot be represented as float64 and returns a conversion error. Returns 0 when no rows match (SQL NULL).
func (*Builder) OnlyTrashed ¶
OnlyTrashed scopes the query to soft-deleted rows only (deleted_at IS NOT NULL). It implies SoftDelete() and takes precedence over WithTrashed().
db.Table("posts").OnlyTrashed().All(&trashed)
func (*Builder) OrWhereGroup ¶
OrWhereGroup adds a parenthesized group of conditions joined to the previous clauses with OR. See WhereGroup.
func (*Builder) OrderBy ¶
OrderBy adds an ORDER BY clause. The clause is a comma-separated list of "[table.]column [ASC|DESC] [NULLS FIRST|NULLS LAST]" terms; column identifiers are validated and quoted to prevent SQL injection, and the direction/NULLS keywords are checked against an allow-list. Anything outside that grammar (function calls, arithmetic, etc.) is rejected — use OrderByRaw for trusted raw expressions.
db.Table("users").OrderBy("created_at DESC").OrderBy("name ASC")
db.Table("messages").OrderBy("last_message_at DESC NULLS LAST")
func (*Builder) OrderByRaw ¶
OrderByRaw adds a raw, unescaped ORDER BY expression. The caller is responsible for safety — never pass user input directly.
func (*Builder) Paginate ¶
Paginate executes a COUNT and a SELECT with LIMIT/OFFSET and returns a Page. page is 1-based.
func (*Builder) Pluck ¶
Pluck retrieves a single column as a []T slice.
var emails []string
db.Table("users").Pluck("email", &emails)
func (*Builder) Restore ¶
Restore un-deletes soft-deleted rows matching the WHERE clause by setting deleted_at back to NULL. The statement is automatically scoped to trashed rows (deleted_at IS NOT NULL), so live rows are never touched. Like Delete/Update it refuses to run without a WHERE clause — use WhereRaw("1=1") to restore all trashed rows.
db.Table("posts").Where("id = ?", id).Restore()
func (*Builder) Save ¶
Save updates all fields of dest by primary key. Save always takes the update path, so it fires BeforeSave → BeforeUpdate before the UPDATE and AfterUpdate → AfterSave after it.
func (*Builder) Select ¶
Select specifies which columns to retrieve. Each argument must be a column reference — "col", "table.col", "col AS alias", "*", or "table.*" — and is validated and quoted to prevent SQL injection. For aggregate or computed expressions (e.g. "COUNT(*) AS n"), use SelectRaw instead.
db.Table("users").Select("id", "email").All(&users)
func (*Builder) SelectRaw ¶
SelectRaw adds a raw, unescaped SELECT expression. The caller is responsible for ensuring the expression is safe — never pass user input directly.
db.Table("posts").SelectRaw("COUNT(*) AS post_count")
func (*Builder) SoftDelete ¶
SoftDelete tells the builder this table uses soft deletes (deleted_at column). Automatically adds "deleted_at IS NULL" to WHERE unless WithTrashed() is called.
func (*Builder) Sum ¶
Sum executes SUM(col) over the current query and returns the result. Returns 0 when no rows match (SQL NULL). col is validated and quoted.
func (*Builder) ToSQL ¶
ToSQL returns the SELECT statement and bind arguments the builder would execute, with dialect-native placeholders, WITHOUT executing it and WITHOUT releasing the builder — it can be called mid-chain for debugging/logging and then followed by a terminal method.
q, args, _ := db.Table("users").Where("active = ?", true).ToSQL()
func (*Builder) Update ¶
Update updates specific columns for rows matching the WHERE clause.
db.Table("users").Where("id = ?", 1).Update(database.Map{"name": "Alice"})
On a SoftDelete() builder, soft-deleted rows are excluded from the UPDATE unless WithTrashed() is set.
func (*Builder) UpdateOrCreateMap ¶
UpdateOrCreateMap updates values on the first row matching all match columns, or inserts match merged with values when no row matches. The UPDATE stamps updated_at (when not supplied) and the INSERT stamps both timestamps, mirroring InsertMap.
NOT atomic: a concurrent writer between the existence check and the write can race. For an atomic guarantee, add a unique index over the match columns and use UpsertMap instead.
func (*Builder) UpsertMap ¶
UpsertMap inserts data, updating updateCols instead when a row already exists. Dialect handling:
- Postgres: INSERT ... ON CONFLICT (conflictCols) DO UPDATE SET col = EXCLUDED.col. conflictCols must name a unique index/constraint.
- MySQL: INSERT ... ON DUPLICATE KEY UPDATE col = VALUES(col). conflictCols are IGNORED — MySQL always resolves against whatever unique key the row collides with.
Every identifier (data keys, conflictCols, updateCols) is validated and quoted; updateCols must be a subset of data's keys since the update references the inserted values. created_at/updated_at are stamped like InsertMap when absent.
db.Table("settings").UpsertMap(
database.Map{"key": "theme", "value": "dark"},
[]string{"key"}, []string{"value", "updated_at"})
func (*Builder) Where ¶
Where adds a WHERE condition (AND-joined).
db.Table("users").Where("active = ?", true).Where("role = ?", "admin")
func (*Builder) WhereGroup ¶
WhereGroup adds a parenthesized group of conditions joined to the previous clauses with AND. fn receives a fresh builder whose Where/OrWhere/WhereIn/ etc. calls are collected and rendered inside the parentheses.
db.Table("users").
Where("active = ?", true).
WhereGroup(func(g *database.Builder) {
g.Where("role = ?", "admin").OrWhere("role = ?", "owner")
})
// WHERE (active = ?) AND ((role = ?) OR (role = ?))
func (*Builder) WhereInSub ¶
WhereInSub adds "col IN (SELECT ...)" using sub as the sub-query.
WhereInSub CONSUMES sub: the sub-builder is rendered immediately and released back to the pool, so it must not be used (or released) again after this call. Placeholders from the sub-query are merged into the outer query and normalized together at execution time.
sub := db.Table("orders").Select("user_id").Where("total > ?", 100)
db.Table("users").WhereInSub("id", sub).All(&users)
func (*Builder) WhereNotIn ¶
WhereNotIn adds a WHERE column NOT IN (...) clause.
func (*Builder) WhereNotNull ¶
WhereNotNull adds WHERE column IS NOT NULL.
func (*Builder) WhereRaw ¶
WhereRaw adds a raw WHERE clause without any quoting or escaping. Use this for complex conditions or when you need full control over the SQL.
db.Table("users").WhereRaw("1=1").Delete() // delete all rows
func (*Builder) With ¶
With eager-loads the named relationships after the main query executes.
Note: relations are loaded WITHOUT soft-delete scoping — soft-deleted related rows are included, because the related model's soft-delete configuration is not known at load time.
db.Table("users").With("Posts", "Role").All(&users)
func (*Builder) WithTrashed ¶
WithTrashed includes soft-deleted rows (does not add "deleted_at IS NULL").
type Config ¶
type Config struct {
Driver Driver
Host string
Port int
Name string
User string
Password string
SSLMode string
// Pool settings
MaxOpen int
MaxIdle int
MaxLifetime time.Duration
// PostgreSQL-specific: use pgxpool directly for best performance
PgxConfig *pgxpool.Config
}
Config holds all database connection parameters.
func DefaultConfig ¶
func DefaultConfig() Config
DefaultConfig returns a localhost PostgreSQL config suitable for development.
type DB ¶
type DB struct {
// contains filtered or unexported fields
}
DB is the central database handle. It wraps *sql.DB and provides the query builder API. Create one DB per database connection and share it.
func Default ¶
func Default() *DB
Default returns the package-level default DB, or panics if not set.
func (*DB) Load ¶
Load populates the named relationship(s) on a slice of model pointers. It fires exactly one batch query per relationship — never N individual queries.
db.Load(&users, "Posts") // SELECT * FROM posts WHERE user_id IN (1,2,3) db.Load(&users, "Posts", "Role") // two batch queries
func (*DB) LoadContext ¶
LoadContext is like Load but with context.
func (*DB) QuoteIdentifier ¶
QuoteIdentifier quotes a SQL identifier (table or column) for the active dialect, escaping any embedded quote characters. Use it when building a raw clause fragment that must include a caller-supplied identifier safely.
func (*DB) Raw ¶
Raw returns a Builder for a raw SQL query. Terminal methods Scan, All, and Count all work on Raw builders.
db.Raw("SELECT COUNT(*) FROM users WHERE active = ?", true).Scan(&count)
db.Raw("SELECT * FROM users WHERE role = ?", "admin").All(&users)
func (*DB) SQLDB ¶
SQLDB exposes the underlying *sql.DB for advanced use (migrations, raw exec, etc.).
func (*DB) SetLogLevel ¶
SetLogLevel sets the minimum log level for query logging (default: slog.LevelDebug).
func (*DB) Table ¶
Table returns a new Builder for the given table name.
db.Table("users").Where("active = ?", true).All(&users)
func (*DB) Transaction ¶
Transaction executes fn inside a database transaction. If fn returns nil, the transaction is committed; otherwise it is rolled back. Nested calls create savepoints so inner failures can be rolled back independently.
func (*DB) TransactionContext ¶
TransactionContext is like Transaction but accepts a context.
type Grammar ¶
type Grammar interface {
// Placeholder returns the n-th positional bind placeholder ($1, $2 for
// Postgres; ? for MySQL).
Placeholder(n int) string
// QuoteIdent quotes an identifier, doubling embedded quote characters.
QuoteIdent(s string) string
}
Grammar generates database-dialect-specific SQL fragments.
DDL generation (CREATE TABLE, ALTER TABLE, indexes) lives in the framework/migrations package; this interface only covers the fragments the query builder needs at runtime.