Documentation
¶
Overview ¶
Package orm is forge's typed data-access layer: a postgres-pinned query builder over uptrace/bun, plus the schema-truth machinery that projects entity types out of the applied migrations.
The generated internal/db/<entity>_orm.go consumes it, so most projects reach for this package only through generated code. What a hand-written caller uses directly:
- Client the handle; Bun() is the raw engine seam for queries the generated CRUD cannot express.
- Client.RunTx a SERIALIZABLE, conflict-retried transaction carried in ctx: every query made with the ctx it hands fn — generated delegates, stores, pkg/crud, raw SQL — joins it. fn may run more than once; defer side effects with AfterCommit. See tx.go.
- ErrNoRows the missing-row sentinel. Match it with errors.Is and let pkg/svcerr map it to a clean NotFound — never leak SQL text.
- IsUniqueViolation, IsCheckViolation, ConstraintName the write-side twins of ErrNoRows: classify a constraint violation a service wrote itself into, so a lost UNIQUE race becomes AlreadyExists instead of a 500 carrying driver prose.
- <Entity>Columns the declared-column allowlist generated per entity, used to validate an order_by against a closed set.
- WhereILikeAny the multi-column case-insensitive search predicate behind generated search/query/q filters.
- NullTime the tolerant nullable-timestamp scanner.
Schema truth flows one way: db/migrations/*.up.sql is the source, and the entity types, ORM and CRUD wiring are PROJECTIONS of the applied schema (introspect.go / differ.go / migration.go). There is no entity annotation to declare — an entity is a table plus matching CRUD RPCs in a service proto.
Index ¶
- Constants
- Variables
- func AfterCommit(ctx context.Context, fn func(context.Context)) error
- func ConstraintName(err error) string
- func DecodeCursor(token string) (string, error)
- func EmptyIfNil[T any](s []T) []T
- func EncodeCursor(id string) string
- func GenerateAlterSQL(diff SchemaDiff, dialect Dialect, allowDestructive bool) ([]string, error)
- func GenerateCreateTableSQL(schema TableSchema) string
- func IsCheckViolation(err error) bool
- func IsUniqueViolation(err error) bool
- func JSON(v any) driver.Valuer
- func ListDialects() []string
- func MarshalJSONBList[T proto.Message](list []T) (string, error)
- func MarshalJSONBMap(m any) (string, error)
- func MarshalJSONBMessage[T proto.Message](m T) (string, error)
- func MarshalJSONBScalars(list any) (string, error)
- func NewQueryError(query string, err error) error
- func NewSchemaError(table, message string, err error) error
- func NewTransactionError(operation string, err error) error
- func RegisterDialect(dialect Dialect)
- func ScanJSON(dst any) sql.Scanner
- func UnmarshalJSONBList[T proto.Message](s string, dst *[]T) error
- func UnmarshalJSONBMap(s string, dst any) error
- func UnmarshalJSONBMessage[T proto.Message](s string, dst *T) error
- func UnmarshalJSONBScalars(s string, dst any) error
- func ValidateIdentifier(name string) error
- func ValidateOrderBy(clause string, allowedColumns []string) error
- func ValidateSchema(ctx context.Context, client *Client, schema TableSchema) error
- type Client
- func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error)
- func (c *Client) Bun() bun.IDB
- func (c *Client) BunDB() *bun.DB
- func (c *Client) Close() error
- func (c *Client) DB() *sql.DB
- func (c *Client) Dialect() Dialect
- func (c *Client) Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
- func (c *Client) Query(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
- func (c *Client) QueryRow(ctx context.Context, query string, args ...interface{}) *sql.Row
- func (c *Client) RunTransaction(ctx context.Context, fn func(ctx Context) error) error
- func (c *Client) RunTransactionWithOptions(ctx context.Context, opts *sql.TxOptions, fn func(ctx Context) error) error
- func (c *Client) RunTx(ctx context.Context, fn func(ctx context.Context) error) error
- func (c *Client) RunTxReadOnly(ctx context.Context, fn func(ctx context.Context) error) error
- func (c *Client) RunTxWithOptions(ctx context.Context, opts TxOptions, fn func(ctx context.Context) error) error
- type ColumnDiff
- type Context
- type Dialect
- type FieldSchema
- type FieldType
- type IndexSchema
- type Int64Array
- type IntrospectedColumn
- type IntrospectedIndex
- type Isolation
- type Migration
- type MigrationManager
- func (m *MigrationManager) AutoMigrate(ctx context.Context, schemas []TableSchema) error
- func (m *MigrationManager) GenerateMigration(ctx context.Context, schemas []TableSchema) (*Migration, error)
- func (m *MigrationManager) Migrate(ctx context.Context) error
- func (m *MigrationManager) MigrateTo(ctx context.Context, targetVersion string) error
- func (m *MigrationManager) PlanMigration(ctx context.Context, schemas []TableSchema) (string, error)
- func (m *MigrationManager) Register(migration *Migration) error
- func (m *MigrationManager) RegisterMany(migrations ...*Migration) error
- func (m *MigrationManager) RegisterPairedMigration(paired *PairedMigration) error
- func (m *MigrationManager) SetTableName(name string)
- func (m *MigrationManager) Status(ctx context.Context) ([]MigrationStatus, error)
- type MigrationStatus
- type NullTime
- type Operator
- type Order
- type PairedMigration
- type PostgresDialect
- func (d *PostgresDialect) DriverName() string
- func (d *PostgresDialect) IntrospectColumnsQuery(tableName string) string
- func (d *PostgresDialect) IntrospectIndexesQuery(tableName string) string
- func (d *PostgresDialect) ListTablesQuery() string
- func (d *PostgresDialect) MapFieldType(fieldType FieldType) string
- func (d *PostgresDialect) Name() string
- func (d *PostgresDialect) OnConflictClause(conflictColumn string, updateColumns []string) string
- func (d *PostgresDialect) ParseColumnType(dbType string) (FieldType, error)
- func (d *PostgresDialect) Placeholder(index int) string
- func (d *PostgresDialect) QuoteIdentifier(identifier string) string
- func (d *PostgresDialect) ScanColumn(rows *sql.Rows) (IntrospectedColumn, error)
- func (d *PostgresDialect) ScanIndex(rows *sql.Rows) (indexName, columnName string, isUnique bool, err error)
- func (d *PostgresDialect) SupportsReturning() bool
- func (d *PostgresDialect) TableExistsQuery(tableName string) string
- type QueryError
- type QueryOption
- func WhereEq(column string, value any) QueryOption
- func WhereGt(column string, value any) QueryOption
- func WhereGte(column string, value any) QueryOption
- func WhereILike(column string, value any) QueryOption
- func WhereILikeAny(columns []string, value any) QueryOption
- func WhereIn(column string, values any) QueryOption
- func WhereIsNotNull(column string) QueryOption
- func WhereIsNull(column string) QueryOption
- func WhereLike(column string, value any) QueryOption
- func WhereLt(column string, value any) QueryOption
- func WhereLte(column string, value any) QueryOption
- func WhereNotEq(column string, value any) QueryOption
- func WhereNotIn(column string, values any) QueryOption
- func WithLimit(limit int) QueryOption
- func WithOffset(offset int) QueryOption
- func WithOrderBy(clause string, order Order) QueryOption
- func WithWhere(column string, op Operator, value any) QueryOption
- type SchemaDiff
- type SchemaError
- type StringArray
- type TableSchema
- type TransactionError
- type Tx
- func (t *Tx) Bun() bun.IDB
- func (t *Tx) Commit() error
- func (t *Tx) Dialect() Dialect
- func (t *Tx) Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
- func (t *Tx) Query(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
- func (t *Tx) QueryRow(ctx context.Context, query string, args ...interface{}) *sql.Row
- func (t *Tx) Rollback() error
- func (t *Tx) RunTransaction(ctx context.Context, fn func(Context) error) error
- func (t *Tx) RunTx(ctx context.Context, fn func(ctx context.Context) error) error
- func (t *Tx) RunTxReadOnly(ctx context.Context, fn func(ctx context.Context) error) error
- func (t *Tx) RunTxWithOptions(ctx context.Context, _ TxOptions, fn func(ctx context.Context) error) error
- type TxOptions
- type UnknownFieldError
Constants ¶
const DefaultTxMaxAttempts = 7
DefaultTxMaxAttempts is the attempt budget a zero TxOptions gets: one try plus six retries.
Sized from reliant's measurement on a single hot row under SERIALIZABLE (5 writes per writer): with 3 retries the failure rate was 0% at 4 concurrent writers, 5% at 8 and 8.8% at 16; at 32 writers every write completed within one further attempt. Six retries covers 32-way contention with margin, and because a transaction that does not conflict never sleeps, the extra headroom costs nothing on the uncontended path.
Variables ¶
var ( // ErrDestructiveOperation is returned when attempting a destructive operation without permission ErrDestructiveOperation = errors.New("orm: destructive operation not allowed") // ErrUnsupportedOperation is returned when a dialect doesn't support an operation ErrUnsupportedOperation = errors.New("orm: operation not supported by dialect") // ErrIncompatibleTypes is returned when column types cannot be safely converted ErrIncompatibleTypes = errors.New("orm: incompatible column types") )
Diff errors
var ( // ErrNoRows is returned when a query returns no rows ErrNoRows = sql.ErrNoRows // ErrTxDone is returned when a transaction is already committed or rolled back ErrTxDone = sql.ErrTxDone // ErrConnDone is returned when the connection is already closed ErrConnDone = sql.ErrConnDone // ErrNoPrimaryKey is returned when attempting operations on a message without a primary key ErrNoPrimaryKey = errors.New("orm: message has no primary key defined") // ErrInvalidDialect is returned when an invalid or unregistered dialect is specified ErrInvalidDialect = errors.New("orm: invalid or unregistered dialect") // ErrNilContext is returned when a nil context is provided ErrNilContext = errors.New("orm: nil context provided") // ErrSchemaValidationFailed is returned when schema validation fails ErrSchemaValidationFailed = errors.New("orm: schema validation failed") )
Common ORM errors
var ErrNoTransaction = errors.New("orm: no active transaction in context")
ErrNoTransaction is returned by AfterCommit when ctx carries no active transaction to defer the callback to.
Functions ¶
func AfterCommit ¶
AfterCommit defers fn until the transaction carried by ctx commits.
fn runs exactly once, after the OUTERMOST commit: a RunTx that joined an enclosing transaction does not commit anything itself, so callbacks it registers wait for the owner. If the attempt rolls back — fn returned an error, or postgres aborted it and RunTx is retrying — the callbacks registered by that attempt are discarded; the retry registers its own.
This is the place for every effect fn must not perform itself because fn can run more than once: sending an email, publishing an event, enqueueing a job, calling another service.
fn receives the context RunTx was called with, which does not carry the finished transaction. It runs synchronously, in registration order, before RunTx returns. It cannot fail the transaction — that has already committed — so it returns nothing; handle or log its own errors.
With nested transactions on DIFFERENT databases, fn attaches to the innermost one and runs when that one commits.
Returns ErrNoTransaction when ctx carries no active transaction.
func ConstraintName ¶
ConstraintName returns the name of the schema object err violated, or "" if err is not a postgres error or postgres named none.
The name is safe to act on and safe to surface: it is an identifier the application wrote in its own migration, and postgres reports it as a structured field rather than as text inside the message. That is the distinction pkg/crud draws too — the constraint name crosses the wire, the driver's prose and the offending values do not.
Compare it against the GENERATED constant, never a literal: forge projects the applied schema's UNIQUE / CHECK / FOREIGN KEY names to db.<Entity>Constraint<Name>, beside the column constants. That is what makes renaming a constraint in a migration a compile error at every reader rather than a branch that silently stops matching.
A NOT NULL violation has no constraint to name (postgres reports the column instead), so this returns "" for one.
func DecodeCursor ¶
DecodeCursor decodes a page cursor token back into the primary key value. Returns an error if the token is empty or not valid base64url.
func EmptyIfNil ¶
func EmptyIfNil[T any](s []T) []T
EmptyIfNil returns an empty (non-nil) slice when s is nil, else s. Used at generated write sites so a nil repeated field binds as an empty array literal, not NULL, against `NOT NULL DEFAULT '{}'` columns.
func EncodeCursor ¶
EncodeCursor encodes a primary key value into an opaque page cursor token. The cursor is base64url-encoded (no padding) for safe use in query strings.
func GenerateAlterSQL ¶
func GenerateAlterSQL(diff SchemaDiff, dialect Dialect, allowDestructive bool) ([]string, error)
GenerateAlterSQL generates ALTER TABLE statements to apply the schema diff GenerateAlterSQL generates ALTER TABLE statements for the given schema diff.
Following the protobuf philosophy, this function ONLY generates ADD and DROP statements. Column modifications (type changes, constraint changes, etc.) are NOT supported.
To modify a column, use a multi-step migration with PairedMigration:
- ADD new column with desired schema
- Migrate data from old column to new column (using DataMigration)
- DROP old column (in a separate migration after verification)
This approach:
- Works on all databases (SQLite, PostgreSQL, MySQL, etc.)
- Forces explicit thinking about data transformations
- Prevents accidental data loss
- Follows protobuf's additive-only philosophy
func GenerateCreateTableSQL ¶
func GenerateCreateTableSQL(schema TableSchema) string
GenerateCreateTableSQL generates a CREATE TABLE statement
func IsCheckViolation ¶
IsCheckViolation reports whether err is a postgres CHECK-constraint violation (SQLSTATE 23514), including one wrapped with %w.
A CHECK is a declared domain invariant — invoices_not_overpaid, say — so violating it is the CALLER's bad input, not a server fault: map it to an invalid-argument sentinel, not an internal one. It is a separate predicate rather than a mode on IsUniqueViolation because the two demand different answers from a service; collapsing them would report "already exists" for a payment that merely exceeded the balance.
func IsUniqueViolation ¶
IsUniqueViolation reports whether err is a postgres unique-constraint violation (SQLSTATE 23505), including one wrapped with %w.
This is the predicate that turns a lost race into a clean AlreadyExists instead of a 500:
if err := s.db.CreateJob(ctx, job); err != nil {
if orm.IsUniqueViolation(err) {
return Job{}, ErrAlreadyExists
}
return Job{}, fmt.Errorf("create job: %w", err)
}
On a table with more than one UNIQUE, pair it with ConstraintName — "some unique failed" cannot tell "this estimate already sold a job" from "that job number is taken", and a service that guesses is wrong half the time.
func JSON ¶
JSON wraps a Go value for storage as a JSON column value. The generated ORM code uses it for repeated scalar entity fields (e.g. `repeated string tags`): the value is marshalled to JSON text on write, which both postgres (a jsonb column accepts an untyped text parameter) and sqlite (a TEXT-affinity column) store natively — one generated code path, two dialects.
A nil/empty slice round-trips as SQL NULL so the column stays cleanly nullable and scans back to the zero value.
func ListDialects ¶
func ListDialects() []string
ListDialects returns a list of all registered dialect names.
func MarshalJSONBList ¶
MarshalJSONBList renders a repeated proto message field as the JSON array text of a json/jsonb column. A nil or empty slice renders "[]", never "null", so a column with CHECK (jsonb_typeof(col) = 'array') holds a legal value for an empty list.
func MarshalJSONBMap ¶
MarshalJSONBMap renders a proto map field with SCALAR values as the JSON object text of a json/jsonb column. A nil or empty map renders "{}" — the object shape its column expects, never "null". Maps of messages or enums never come here: encoding/json cannot encode either correctly, and entity birth TODO-skips them rather than creating a column.
func MarshalJSONBMessage ¶
MarshalJSONBMessage renders one proto message as the text of a json/jsonb column. A nil message renders the message's ZERO document (every declared field at its zero value, per EmitUnpopulated), which is what proto3 says an absent message means and what the read path reconstructs — never SQL-invalid text, and never `null` where a NOT NULL column would reject it. Generated code storing into a NULLABLE column writes SQL NULL for a nil message instead and never calls this.
func MarshalJSONBScalars ¶
MarshalJSONBScalars renders a repeated SCALAR wire field ([]string, []int64, …) as the JSON array text of a json/jsonb column. A nil or empty slice renders "[]" for the same reason MarshalJSONBList does. Message slices never come here — they need protojson (see MarshalJSONBList).
func NewQueryError ¶
NewQueryError creates a new QueryError
func NewSchemaError ¶
NewSchemaError creates a new SchemaError
func NewTransactionError ¶
NewTransactionError creates a new TransactionError
func RegisterDialect ¶
func RegisterDialect(dialect Dialect)
RegisterDialect registers a dialect for use with the ORM. This should be called from init() functions in dialect packages.
func ScanJSON ¶
ScanJSON returns a sql.Scanner that unmarshals a JSON column into dst (a pointer, e.g. *[]string). NULL / empty values leave dst at its zero value. It accepts []byte (postgres jsonb) and string (sqlite TEXT) sources.
func UnmarshalJSONBList ¶
UnmarshalJSONBList parses a json/jsonb column's array text into a repeated proto message field. dst is the ADDRESS of the wire field (e.g. &m.LineItems, of type *[]*pb.OrderLineItem). Empty text and `null` leave dst untouched (an empty list). A document that is not a JSON array is an error — the repeated field cannot represent it, and appending nothing would drop the row's data silently.
func UnmarshalJSONBMap ¶
UnmarshalJSONBMap parses a json/jsonb column's object text into a proto map field with scalar values. dst is the ADDRESS of the field (e.g. &m.Labels, of type *map[string]string).
func UnmarshalJSONBMessage ¶
UnmarshalJSONBMessage parses a json/jsonb column's text into a singular proto message field. dst is the ADDRESS of the wire field (e.g. &m.Address, of type **pb.Address) so the message is allocated here only when the column holds one. Empty text and the JSON literal `null` leave dst untouched (nil) — that is how an absent message round-trips.
func UnmarshalJSONBScalars ¶
UnmarshalJSONBScalars parses a json/jsonb column's array text into a repeated SCALAR wire field. dst is the ADDRESS of the field (e.g. &m.Tags, of type *[]string). Empty text and `null` leave dst untouched.
func ValidateIdentifier ¶
ValidateIdentifier checks that a string is a safe SQL identifier (letters, digits, underscores only). Returns an error if the identifier contains characters that could be used for SQL injection.
func ValidateOrderBy ¶
ValidateOrderBy validates a comma-separated ORDER BY clause against a column allowlist.
Two layers:
- Shape: only identifier characters (letters, digits, underscores) in column names, with an optional ASC/DESC direction token.
- Allowlist: when allowedColumns is non-empty, every column must be one of the declared columns. Shape validation alone is NOT enough: an undeclared-but-identifier-shaped column (order_by=password_hash) reaches the database where it can be a silent ordering no-op.
Generated entity code exports its declared column list (db.<Entity>Columns) precisely so handlers can pass it here.
func ValidateSchema ¶
func ValidateSchema(ctx context.Context, client *Client, schema TableSchema) error
ValidateSchema validates that the database schema matches the expected schema. It uses IntrospectTable and CompareSchemas to check for differences.
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is the forge ORM handle. Post-Phase-2 it is a thin wrapper over a *bun.DB (uptrace/bun, postgres-pinned): generated CRUD ops reach the engine via Bun(); the kept schema-truth machinery (introspect/differ/ migration) still consults Dialect() and the raw Exec/Query/QueryRow seam. forge is postgres-only — the dialect argument is retained on the constructors for call-site compatibility and must be "postgres".
func NewClientWithDB ¶
NewClientWithDB wraps an existing *sql.DB into an ORM client. This is the seam the generated bootstrap/setup and pkg/testkit use: open a postgres *sql.DB, hand it here. dialectName must be "postgres".
func (*Client) BeginTx ¶
BeginTx starts a transaction. It is the low-level primitive: it never joins a ctx-carried transaction, never retries, and leaves Commit or Rollback to the caller. Prefer RunTx.
func (*Client) Bun ¶
Bun returns the client's bun.IDB. Every query run through it with a ctx carrying a RunTx transaction on this database runs inside that transaction: query builders via the client's connection resolver, and ExecContext/QueryContext/QueryRowContext via the ambientDB wrapper.
func (*Client) BunDB ¶
BunDB returns the concrete *bun.DB (for advanced callers that need connection-pool control or BeginTx with bun's transaction type). Query builders off it still join a ctx-carried transaction; its own ExecContext/QueryContext/QueryRowContext do NOT — use Bun() for those.
func (*Client) DB ¶
DB returns the underlying *sql.DB for advanced usage and for the kept schema-truth machinery's database/sql seam.
func (*Client) Dialect ¶
Dialect returns the SQL dialect (postgres). Consumed by the kept schema-truth machinery (introspect/differ/migration), not by the runtime CRUD engine.
func (*Client) Exec ¶
Exec runs a raw SQL statement (escape hatch). It goes straight to the underlying *sql.DB — or to the RunTx transaction ctx carries for it — NOT through bun's query formatter: callers write native postgres SQL with $1/$2 placeholders, and bun's `?`-rewriting must not touch it. (Generated code uses db.Bun()'s typed builders, which handle their own placeholders.)
func (*Client) Query ¶
Query runs a raw SQL query (escape hatch). See Exec for the raw-passthrough rationale.
func (*Client) RunTransaction ¶
RunTransaction executes fn within a transaction, committing on success and rolling back on error or panic. The transaction Context is passed to fn so generated ORM ops transparently use it.
Prefer RunTx for new code. RunTransaction is the handle-passing form and keeps its original semantics — the database's default isolation (or whatever opts asks for), one attempt, no retry — because its existing callers' closures were written for exactly that: re-running them on a serialization failure could repeat side effects they were never written to repeat. fn also receives only a handle, not a ctx carrying the transaction, so code called with the OUTER ctx and the plain client does not participate; use the handle, or call handle.RunTx to get such a ctx.
It does join a transaction ctx already carries for this database (from RunTx), handing fn a handle on that transaction, so legacy code composes inside a RunTx exactly as RunTx composes inside it.
func (*Client) RunTransactionWithOptions ¶
func (c *Client) RunTransactionWithOptions(ctx context.Context, opts *sql.TxOptions, fn func(ctx Context) error) error
RunTransactionWithOptions is RunTransaction with custom tx options. opts are ignored when it joins a ctx-carried transaction.
func (*Client) RunTx ¶
RunTx runs fn in a SERIALIZABLE transaction carried by the ctx fn receives, retrying on serialization failure and deadlock. fn MAY RUN MORE THAN ONCE; see the package documentation in tx.go and AfterCommit.
If ctx already carries an active transaction on this database, RunTx JOINS it: fn runs inside the enclosing transaction, and committing, rolling back and retrying stay with whoever opened it. That is what lets a service wrap its own work in RunTx and still compose into a caller's larger one.
fn's error is returned verbatim, so svcerr sentinels and domain errors survive. A transaction still conflicting after its last attempt returns a svcerr.Aborted error (the driver error is kept as its cause).
func (*Client) RunTxReadOnly ¶
RunTxReadOnly is RunTx for a transaction that only reads: SERIALIZABLE READ ONLY DEFERRABLE. See TxOptions.ReadOnly for why that matters.
func (*Client) RunTxWithOptions ¶
func (c *Client) RunTxWithOptions(ctx context.Context, opts TxOptions, fn func(ctx context.Context) error) error
RunTxWithOptions is RunTx with explicit options.
When ctx already carries an active transaction on this database, opts are IGNORED and fn joins it: the enclosing transaction exists and its mode cannot change mid-flight. That is safe in the direction that matters — a read-only call joining a read-write transaction just reads — and the reverse, a write joining a read-only transaction, is rejected by postgres at the write, which is exactly where the mistake is.
type ColumnDiff ¶
type ColumnDiff struct {
ColumnName string
OldType string
NewType FieldType
OldNotNull bool
NewNotNull bool
OldDefault sql.NullString
NewDefault string
}
ColumnDiff represents a difference in a column definition
type Context ¶
type Context interface {
// Bun returns the underlying bun.IDB. Generated CRUD functions build
// their queries on it (db.Bun().NewSelect()/NewInsert()/...). It is
// also the raw-SQL escape hatch: bun.IDB exposes NewRaw plus the
// IConn methods below.
Bun() bun.IDB
// Exec executes a query without returning any rows. Thin wrapper over
// bun's ExecContext — the raw-SQL path for user handlers.
Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
// Query executes a query that returns rows (raw-SQL path).
Query(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
// QueryRow executes a query expected to return at most one row
// (raw-SQL path).
QueryRow(ctx context.Context, query string, args ...interface{}) *sql.Row
// RunTransaction executes fn inside a transaction, committing when it
// returns nil and rolling back on error or panic. The Context handed
// to fn is the transactional one, so generated ORM ops inside it join
// the transaction transparently.
//
// This lives on the INTERFACE, not just on *Client, because the
// interface is what forge injects: the CRUD generator writes
// `DB orm.Context` into a service's Deps, and
// forgeconv-deps-are-interfaces requires every Deps field to be an
// interface. With the method only on the concrete type, an
// app that needed to make two writes atomic could not reach a
// transaction through the dependency forge itself wired — so every
// such app re-declared this exact method as a local interface and
// type-asserted the DB to it. That is forge's own seam, hand-copied
// per project.
//
// On *Tx the call JOINS the transaction already in progress rather
// than opening a nested one (this API has no savepoints). That is what
// lets a service be called standalone or from inside a larger
// transaction without knowing which it is in.
//
// Prefer RunTx for new code; see Client.RunTransaction for how the two
// relate.
RunTransaction(ctx context.Context, fn func(Context) error) error
// RunTx runs fn in a SERIALIZABLE transaction CARRIED BY the ctx fn
// receives, retrying on serialization failure and deadlock. Every query
// made with that ctx against this database — generated delegates,
// stores, pkg/crud, raw SQL — joins the transaction, with no handle to
// thread. fn may run more than once, so it must have no effect outside
// the database; defer those with AfterCommit. A ctx that already carries
// a transaction on this database is joined, not nested.
//
// On the interface for the same reason as RunTransaction: `DB
// orm.Context` is the handle forge injects, so `s.deps.DB.RunTx(…)` has
// to compile against it.
RunTx(ctx context.Context, fn func(ctx context.Context) error) error
// RunTxReadOnly is RunTx as SERIALIZABLE READ ONLY DEFERRABLE: no
// predicate locks, never aborted by a conflict. See TxOptions.ReadOnly.
RunTxReadOnly(ctx context.Context, fn func(ctx context.Context) error) error
// RunTxWithOptions is RunTx with explicit isolation, read-only mode and
// retry budget. Options are ignored when joining.
RunTxWithOptions(ctx context.Context, opts TxOptions, fn func(ctx context.Context) error) error
// Dialect returns the SQL dialect (postgres — forge is postgres-pinned).
// The raw-SQL escape hatch needs it: a hand-written handler that builds
// its own SQL string calls db.Dialect().Placeholder(i) for $N parameter
// markers and db.Dialect().QuoteIdentifier(name) for safe identifiers,
// instead of hardcoding postgres syntax. Available on both *Client and
// *Tx so raw SQL composes the same way inside and outside a transaction.
Dialect() Dialect
}
Context is the unified database handle the generated ORM layer and forge/pkg/crud operate against. It can represent either a direct connection (*Client) or a transaction (*Tx), so the same generated CRUD functions work inside and outside transactions.
Bun engine (epic Phase 2): the query/CRUD engine is uptrace/bun. The canonical accessor is Bun(), which returns a bun.IDB — generated ops build their SELECT/INSERT/UPDATE/DELETE on Bun's typed query builders off this handle. The raw escape hatch (Exec/Query/QueryRow) wraps Bun's IConn, preserved here so user-owned handlers can run hand-written SQL and so the kept schema-truth machinery (introspect/differ/ migration) keeps a database/sql seam.
type Dialect ¶
type Dialect interface {
// Name returns the name of the dialect (e.g., "postgres", "sqlite")
Name() string
// DriverName returns the name of the database driver to use with sql.Open
DriverName() string
// Placeholder returns the placeholder string for a given parameter index (0-based)
// PostgreSQL uses $1, $2, etc.
// SQLite and MySQL use ?
Placeholder(index int) string
// QuoteIdentifier quotes an identifier (table name, column name) for the dialect
QuoteIdentifier(identifier string) string
// MapFieldType maps an ORM FieldType to the dialect-specific SQL type
MapFieldType(fieldType FieldType) string
// SupportsReturning returns true if the dialect supports RETURNING clause
SupportsReturning() bool
// OnConflictClause returns the dialect-specific ON CONFLICT or equivalent clause
// for upserts. Takes the conflict column and update columns.
OnConflictClause(conflictColumn string, updateColumns []string) string
// TableExistsQuery returns a SQL query to check if a table exists
TableExistsQuery(tableName string) string
// ListTablesQuery returns a SQL query to list all tables in the current schema
ListTablesQuery() string
// IntrospectColumnsQuery returns a SQL query to introspect columns of a table
IntrospectColumnsQuery(tableName string) string
// IntrospectIndexesQuery returns a SQL query to introspect indexes of a table
IntrospectIndexesQuery(tableName string) string
// ParseColumnType converts a database-specific type string to FieldType
ParseColumnType(dbType string) (FieldType, error)
// ScanColumn scans a row from IntrospectColumnsQuery into an IntrospectedColumn
ScanColumn(rows *sql.Rows) (IntrospectedColumn, error)
// ScanIndex scans a row from IntrospectIndexesQuery into index information
ScanIndex(rows *sql.Rows) (indexName, columnName string, isUnique bool, err error)
}
Dialect defines the interface for database-specific implementations. Each supported database (PostgreSQL, SQLite, MySQL, etc.) should implement this interface.
func GetDialect ¶
GetDialect returns the dialect with the given name. Returns nil if the dialect is not registered.
type FieldSchema ¶
type FieldSchema struct {
Name string
Type FieldType
PrimaryKey bool
Unique bool
NotNull bool
DefaultValue string
}
FieldSchema represents a database field schema
type FieldType ¶
type FieldType string
FieldType represents a database field type
const ( TypeText FieldType = "TEXT" TypeVarchar FieldType = "VARCHAR" TypeInteger FieldType = "INTEGER" TypeBigInt FieldType = "BIGINT" TypeBoolean FieldType = "BOOLEAN" TypeTimestampTZ FieldType = "TIMESTAMPTZ" TypeJSONB FieldType = "JSONB" TypeBytea FieldType = "BYTEA" TypeSerial FieldType = "SERIAL" TypeBigSerial FieldType = "BIGSERIAL" // TypeReal stores a single-precision floating point value (IEEE 754 // 32-bit). Maps to PostgreSQL REAL and to SQLite REAL (which is 8-byte // internally — SQLite has no float32 column type). Used for proto // `float` / `google.protobuf.FloatValue` fields. TypeReal FieldType = "REAL" // TypeDoublePrecision stores a double-precision floating point value // (IEEE 754 64-bit). Maps to PostgreSQL DOUBLE PRECISION and to // SQLite REAL. Used for proto `double` / `google.protobuf.DoubleValue` // fields. Before TypeDoublePrecision landed, protoc-gen-forge emitted // orm.TypeText for double columns; the runtime Scan still worked // because Scan uses *float64, but anything that consults // TableSchema() (DDL introspection / contract sync) saw TEXT for // numeric columns. See orm-typetext-for-double in FORGE_BACKLOG. TypeDoublePrecision FieldType = "DOUBLE PRECISION" )
type IndexSchema ¶
IndexSchema represents a database index
type Int64Array ¶
type Int64Array []int64
Int64Array scans an integer-array column from either encoding.
type IntrospectedColumn ¶
type IntrospectedColumn struct {
Name string
Type FieldType
Nullable bool
DefaultValue *string
IsPrimaryKey bool
IsUnique bool
}
IntrospectedColumn represents a database column discovered through introspection
type IntrospectedIndex ¶
IntrospectedIndex represents a database index discovered through introspection
type Isolation ¶
type Isolation int
Isolation selects a transaction's isolation level.
The zero value is IsolationDefault, which means SERIALIZABLE — deliberately, so the weaker choice is always the one spelled out at the call site, and a zero-valued TxOptions can never silently drop a guarantee.
const ( // IsolationDefault is SERIALIZABLE. Correct for anything that reads and // then writes based on what it read. IsolationDefault Isolation = iota // IsolationSerializable is the explicit spelling of the default, for a // call site where serializability is load-bearing and should be stated // rather than inherited. IsolationSerializable // IsolationRepeatableRead is postgres snapshot isolation: every // statement sees the snapshot taken at the transaction's first query, // and a write to a row changed since then fails with 40001 (retried). // It does NOT detect write skew — two transactions each reading what the // other writes — which SERIALIZABLE does. IsolationRepeatableRead // IsolationReadCommitted is postgres's own default. Each STATEMENT sees // rows committed before it began, so two reads in one transaction may // disagree and read-then-write has no protection against a lost update. // It takes no predicate locks and cannot raise 40001, which makes it a // strict win for a single-statement transaction and a correctness // decision — one that needs a reason at the call site — for anything // longer. IsolationReadCommitted )
type Migration ¶
type Migration struct {
// Version is a unique identifier for this migration (e.g., "20240101_001", "v1.0.0")
Version string
// Description is a human-readable description of the migration
Description string
// Up is the function to apply the migration.
//
// There is no Down. Forge rolls forward only: a reverse step written
// before the release ran cannot account for what the release wrote, so
// a bad migration is repaired by a NEW forward migration instead.
Up func(ctx context.Context, db Context) error
}
Migration represents a database migration
func NewSQLMigration ¶
NewSQLMigration creates a forward-only migration from a SQL string.
func NewSchemaCreateMigration ¶
func NewSchemaCreateMigration(version string, schemas ...TableSchema) *Migration
Helper to create a migration that generates schema from protobuf
type MigrationManager ¶
type MigrationManager struct {
// contains filtered or unexported fields
}
MigrationManager manages database migrations
func NewMigrationManager ¶
func NewMigrationManager(client *Client) *MigrationManager
NewMigrationManager creates a new migration manager
func (*MigrationManager) AutoMigrate ¶
func (m *MigrationManager) AutoMigrate(ctx context.Context, schemas []TableSchema) error
AutoMigrate is a convenience function that automatically generates and applies migrations for the provided schemas WARNING: This is a DANGEROUS operation that should ONLY be used in development environments. - It automatically modifies your database schema without explicit review - There is no rollback: forge rolls forward only - It may cause data loss if destructive operations are allowed - Production databases should use explicit migrations instead
Returns error early if schema diff, SQL generation, or database operations fail.
func (*MigrationManager) GenerateMigration ¶
func (m *MigrationManager) GenerateMigration(ctx context.Context, schemas []TableSchema) (*Migration, error)
GenerateMigration diffs the provided schemas against the database and generates ALTER statements Returns a Migration with the generated SQL. Returns error early if schema diff or SQL generation fails.
func (*MigrationManager) Migrate ¶
func (m *MigrationManager) Migrate(ctx context.Context) error
Migrate runs all pending migrations
func (*MigrationManager) MigrateTo ¶
func (m *MigrationManager) MigrateTo(ctx context.Context, targetVersion string) error
MigrateTo migrates to a specific version
func (*MigrationManager) PlanMigration ¶
func (m *MigrationManager) PlanMigration(ctx context.Context, schemas []TableSchema) (string, error)
PlanMigration is a dry-run function that returns the SQL that would be executed without executing it Returns the SQL statements as a string. Returns error early if schema diff or SQL generation fails.
func (*MigrationManager) Register ¶
func (m *MigrationManager) Register(migration *Migration) error
Register registers a migration
func (*MigrationManager) RegisterMany ¶
func (m *MigrationManager) RegisterMany(migrations ...*Migration) error
RegisterMany registers multiple migrations
func (*MigrationManager) RegisterPairedMigration ¶
func (m *MigrationManager) RegisterPairedMigration(paired *PairedMigration) error
RegisterPairedMigration registers a paired migration that combines schema changes with optional data migrations Schema changes are executed first, then data migrations, all in the same transaction. Returns error early if version conflicts are detected or if migrations are invalid.
func (*MigrationManager) SetTableName ¶
func (m *MigrationManager) SetTableName(name string)
SetTableName sets the name of the migrations tracking table
func (*MigrationManager) Status ¶
func (m *MigrationManager) Status(ctx context.Context) ([]MigrationStatus, error)
Status returns the status of all migrations
type MigrationStatus ¶
MigrationStatus represents the status of a migration
type NullTime ¶
NullTime is a nullable timestamp scanner that tolerates every representation forge's supported engines hand back for a timestamp column:
- time.Time (Postgres via pgx/stdlib; SQLite when the declared type is one the driver recognizes),
- string / []byte (SQLite for declared types like TIMESTAMPTZ, which mattn/go-sqlite3 does NOT auto-convert — it only recognizes "timestamp", "datetime" and "date" verbatim),
- nil (NULL column).
database/sql's own sql.NullTime rejects the string forms, which made generated entity scans fail at runtime against the SQLite test harness. Generated *_orm.go scan code uses this type instead.
type Operator ¶
type Operator string
Operator represents a SQL comparison operator used by WithWhere and the Where* convenience helpers. Postgres semantics (ILIKE is native).
const ( Eq Operator = "=" NotEq Operator = "!=" GreaterThan Operator = ">" GreaterThanOrEq Operator = ">=" LessThan Operator = "<" LessThanOrEq Operator = "<=" Like Operator = "LIKE" ILike Operator = "ILIKE" In Operator = "IN" NotIn Operator = "NOT IN" IsNull Operator = "IS NULL" IsNotNull Operator = "IS NOT NULL" )
type PairedMigration ¶
type PairedMigration struct {
// Version is a unique identifier for this migration (e.g., "20240101_001", "v1.0.0")
Version string
// Description is a human-readable description of the migration
Description string
// SchemaChanges are the proto-based table schemas to migrate to
SchemaChanges []TableSchema
// DataMigration is an optional raw SQL migration to run after schema changes
// This is executed in the same transaction as the schema changes
DataMigration *Migration
}
PairedMigration represents a migration that combines schema changes with optional data migrations
type PostgresDialect ¶
type PostgresDialect struct{}
PostgresDialect implements the Dialect interface for PostgreSQL
func (*PostgresDialect) DriverName ¶
func (d *PostgresDialect) DriverName() string
func (*PostgresDialect) IntrospectColumnsQuery ¶
func (d *PostgresDialect) IntrospectColumnsQuery(tableName string) string
IntrospectColumnsQuery returns a query to introspect columns of a table
func (*PostgresDialect) IntrospectIndexesQuery ¶
func (d *PostgresDialect) IntrospectIndexesQuery(tableName string) string
IntrospectIndexesQuery returns a query to introspect indexes of a table
func (*PostgresDialect) ListTablesQuery ¶
func (d *PostgresDialect) ListTablesQuery() string
ListTablesQuery returns a query to list all tables in the public schema
func (*PostgresDialect) MapFieldType ¶
func (d *PostgresDialect) MapFieldType(fieldType FieldType) string
func (*PostgresDialect) Name ¶
func (d *PostgresDialect) Name() string
func (*PostgresDialect) OnConflictClause ¶
func (d *PostgresDialect) OnConflictClause(conflictColumn string, updateColumns []string) string
func (*PostgresDialect) ParseColumnType ¶
func (d *PostgresDialect) ParseColumnType(dbType string) (FieldType, error)
ParseColumnType converts a PostgreSQL type to FieldType
func (*PostgresDialect) Placeholder ¶
func (d *PostgresDialect) Placeholder(index int) string
func (*PostgresDialect) QuoteIdentifier ¶
func (d *PostgresDialect) QuoteIdentifier(identifier string) string
func (*PostgresDialect) ScanColumn ¶
func (d *PostgresDialect) ScanColumn(rows *sql.Rows) (IntrospectedColumn, error)
ScanColumn scans a row from IntrospectColumnsQuery into an IntrospectedColumn
func (*PostgresDialect) ScanIndex ¶
func (d *PostgresDialect) ScanIndex(rows *sql.Rows) (indexName, columnName string, isUnique bool, err error)
ScanIndex scans a row from IntrospectIndexesQuery into index information
func (*PostgresDialect) SupportsReturning ¶
func (d *PostgresDialect) SupportsReturning() bool
func (*PostgresDialect) TableExistsQuery ¶
func (d *PostgresDialect) TableExistsQuery(tableName string) string
TableExistsQuery returns a query to check if a table exists in PostgreSQL
type QueryError ¶
QueryError represents an error that occurred during a query execution
func (*QueryError) Error ¶
func (e *QueryError) Error() string
func (*QueryError) Unwrap ¶
func (e *QueryError) Unwrap() error
type QueryOption ¶
type QueryOption func(*bun.SelectQuery)
QueryOption is a composable mutation of a Bun SELECT query. The generated List/Count/Get ops and forge/pkg/crud build their filters, ordering, and pagination as a slice of these and apply them to a *bun.SelectQuery.
Phase-2 note: pre-Bun this was func(*QueryBuilder) over forge's hand-rolled builder. The signature now targets *bun.SelectQuery — the engine is Bun, and there is no hand-rolled builder left.
func WhereEq ¶
func WhereEq(column string, value any) QueryOption
func WhereGt ¶
func WhereGt(column string, value any) QueryOption
func WhereGte ¶
func WhereGte(column string, value any) QueryOption
func WhereILike ¶
func WhereILike(column string, value any) QueryOption
func WhereILikeAny ¶
func WhereILikeAny(columns []string, value any) QueryOption
WhereILikeAny matches value case-insensitively against ANY of the given columns: (c1 ILIKE ? OR c2 ILIKE ? ...), AND-ed with the other WHERE clauses. This is the canonical mapping for a `search` filter field: it spans the entity's declared string columns instead of inventing a phantom `search` column.
The OR group is grouped with WhereGroup so it composes correctly with surrounding AND clauses (soft-delete, pagination cursor).
func WhereIn ¶
func WhereIn(column string, values any) QueryOption
func WhereIsNotNull ¶
func WhereIsNotNull(column string) QueryOption
func WhereIsNull ¶
func WhereIsNull(column string) QueryOption
func WhereLike ¶
func WhereLike(column string, value any) QueryOption
func WhereLt ¶
func WhereLt(column string, value any) QueryOption
func WhereLte ¶
func WhereLte(column string, value any) QueryOption
func WhereNotEq ¶
func WhereNotEq(column string, value any) QueryOption
func WhereNotIn ¶
func WhereNotIn(column string, values any) QueryOption
func WithLimit ¶
func WithLimit(limit int) QueryOption
WithLimit sets the LIMIT. The option keeps an int parameter (what callers and generated code pass); bun >= 1.3 takes int64, so convert at the call.
func WithOrderBy ¶
func WithOrderBy(clause string, order Order) QueryOption
WithOrderBy adds an ORDER BY clause. The clause may carry one or more comma-separated columns (validate user input with ValidateOrderBy first). order applies to the whole clause.
type SchemaDiff ¶
type SchemaDiff struct {
TableName string
MissingColumns []FieldSchema // Columns in proto but not in DB
ExtraColumns []string // Columns in DB but not in proto
ModifiedColumns []ColumnDiff // Columns with type/constraint changes
MissingIndexes []IndexSchema // Indexes in proto but not in DB
ExtraIndexes []string // Indexes in DB but not in proto
}
SchemaDiff represents all differences between expected and actual schema for a table
func CompareSchemas ¶
func CompareSchemas(expected, actual TableSchema) (SchemaDiff, error)
CompareSchemas compares expected schema against actual schema and returns differences
func DiffDatabase ¶
func DiffDatabase(ctx context.Context, db Context, dialect Dialect, expectedSchemas []TableSchema) ([]SchemaDiff, error)
DiffDatabase compares expected schemas against the actual database schema
func (*SchemaDiff) HasChanges ¶
func (d *SchemaDiff) HasChanges() bool
HasChanges returns true if there are any differences between schemas
func (*SchemaDiff) IsDestructive ¶
func (d *SchemaDiff) IsDestructive() bool
IsDestructive returns true if the diff contains destructive changes
type SchemaError ¶
SchemaError represents a schema-related error
func (*SchemaError) Error ¶
func (e *SchemaError) Error() string
func (*SchemaError) Unwrap ¶
func (e *SchemaError) Unwrap() error
type StringArray ¶
type StringArray []string
StringArray scans a string-array column from either encoding.
type TableSchema ¶
type TableSchema struct {
Name string
Fields []FieldSchema
Indexes []IndexSchema
}
TableSchema represents a database table schema
func IntrospectAllTables ¶
IntrospectAllTables retrieves schemas for all user tables in the database. It returns a slice of TableSchema objects, one for each table. Returns NewSchemaError if the query fails or any table introspection fails.
func IntrospectTable ¶
func IntrospectTable(ctx context.Context, db Context, dialect Dialect, tableName string) (TableSchema, error)
IntrospectTable retrieves the actual schema of a table from the database. It returns detailed information about columns and indexes. Returns NewSchemaError if the table doesn't exist or any operation fails.
type TransactionError ¶
TransactionError represents an error that occurred during a transaction
func (*TransactionError) Error ¶
func (e *TransactionError) Error() string
func (*TransactionError) Unwrap ¶
func (e *TransactionError) Unwrap() error
type Tx ¶
type Tx struct {
// contains filtered or unexported fields
}
Tx wraps a bun transaction as an orm.Context, so the same generated CRUD functions run transparently inside a transaction.
func (*Tx) Commit ¶
Commit commits the transaction, then runs the callbacks AfterCommit registered against it, in order. A failed commit runs none.
func (*Tx) Dialect ¶
Dialect returns the SQL dialect (postgres), so raw-SQL handlers running inside a transaction get the same Placeholder()/QuoteIdentifier() seam as on *Client. Carried from the parent Client at BeginTx time.
func (*Tx) Exec ¶
Exec runs a raw SQL statement within the transaction. Like Client.Exec it bypasses bun's query formatter (native $1/$2 placeholders) by going to the embedded *sql.Tx.
func (*Tx) RunTransaction ¶
RunTransaction on a Tx JOINS the transaction already in progress: it runs fn with this same *Tx rather than opening a nested one, which this API cannot express (there are no savepoints here). Commit and rollback stay the outermost caller's decision — an error from fn propagates and the owner of the transaction rolls it back.
Without this, an interactor that wraps its work in RunTransaction could not be composed into another interactor that had already opened one.
func (*Tx) RunTx ¶
RunTx on a *Tx runs fn inside THIS transaction, with a ctx that carries it, so code written against the plain client (generated delegates, stores, pkg/crud) participates. It never begins, commits, retries or rolls back: the owner of the *Tx does. This is how code holding an explicit handle from RunTransaction calls a service written with RunTx.
func (*Tx) RunTxReadOnly ¶
RunTxReadOnly on a *Tx is a join; see RunTx. The read-only request is ignored because the transaction's mode is already fixed.
type TxOptions ¶
type TxOptions struct {
// Isolation selects the isolation level. Zero value = SERIALIZABLE.
Isolation Isolation
// ReadOnly runs the transaction READ ONLY, and under SERIALIZABLE also
// DEFERRABLE.
//
// That second half is the point. A SERIALIZABLE read takes predicate
// locks over whatever it touched (the whole relation, for a read no index
// serves), and those locks are what make two writers on unrelated rows
// abort each other. A SERIALIZABLE READ ONLY DEFERRABLE transaction takes
// NO predicate locks and can never fail with 40001: it waits at its first
// statement for a snapshot guaranteed free of later conflicts, then runs
// outside the serialization graph entirely — neither suffering conflicts
// nor causing them for concurrent writers.
//
// The cost is that wait, which can briefly delay a read on a busy
// database. A write inside a read-only transaction is rejected by
// postgres outright, so a mislabelled path fails loudly.
ReadOnly bool
// MaxAttempts bounds how many times fn runs. Zero (or negative) means the
// default, DefaultTxMaxAttempts; 1 disables retry — useful for measuring
// contention rather than surviving it, never for a production writer.
MaxAttempts int
}
TxOptions configures RunTxWithOptions. The zero value is a read-write SERIALIZABLE transaction with the default retry budget — what RunTx uses.
type UnknownFieldError ¶
type UnknownFieldError struct {
Field string
}
UnknownFieldError is returned by the generated Update<Entity>Masked helpers when an update_mask path names a column that is not in the entity's updatable set — unknown columns, the primary key, and immutable bookkeeping columns (created_at, deleted_at) all qualify. pkg/crud maps it to CodeInvalidArgument with a clean, SQL-free message naming the path.
func (*UnknownFieldError) Error ¶
func (e *UnknownFieldError) Error() string