DB layer
Postgres-only DML over bun — no DDL, injection-safe by construction.
Source: db/. Postgres only. Brick performs no DDL — Frappe (or your migrations) owns the schema.
DB
bdb := db.New(bunDB)
bdb.RunInTx(ctx, nil, func(ctx context.Context, txDB *db.DB) error { ... })
bdb.InsertMap(ctx, table, body)
bdb.FindByFieldTableWhere(ctx, table, field, value, where)
bdb.UpdateByFieldTableWhere(ctx, table, field, value, patch, where)
bdb.DeleteByFieldTableWhere(ctx, table, field, value, where)
bdb.List(ctx, slice, opts) / bdb.ListMap(ctx, table, opts)
bdb.Count(ctx, ...)
bdb.Raw(...)DeleteWhere with an empty predicate is refused — scoped deletes always carry the lookup + guard filters.
Where
db.Where{Field: "team", Value: teamID, Operator: db.OpEq}Operators: eq ne gt gte lt lte in not_in contains starts_with ends_with is_null is_not_null, with Connector (AND/OR), Or, Not, nesting depth ≤ 8. Identifiers go through bun.Ident, values through ? / bun.In, LIKE patterns escaped with ESCAPE — injection-safe by construction.
Listing
type ListOptions struct {
Page, Limit int
Search string // ILIKE-OR over Searchable
Filter []Where
Sort string // "col" or "-col", allowlist-checked
Sortable []string
Searchable []string
}DefaultLimit = 50, MaxLimit = 500; the exec layer echoes effective page/limit. Sort outside the allowlist is rejected (400) in the resource layer and ignored as backup in db.
Fluent helper:
db.QueryFilter{}.Eq("team", teamID).Search("acme").OrderDesc("modified").Limit(25)
qf.EqIf(cond, "status", "open") // false-kept variant
qf.Options() // → ListOptions
qf.ApplyTo(&opts)Series counters
NextFrappeIntPK takes a tx advisory lock and computes MAX(name)+1 — the backing for naming_series autoname under concurrency.