orm

package
v1.0.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 30, 2026 License: Apache-2.0 Imports: 31 Imported by: 0

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 transaction carried in ctx, at the level the caller names — Serializable (conflict-retried; lock-free read-check-write) or ReadCommitted (one attempt; correctness from a lock). There is no default. 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 and TxOptions.
  • 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

View Source
const DefaultTxMaxAttempts = 7

DefaultTxMaxAttempts is the attempt budget Serializable and RepeatableRead carry: 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

View Source
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

View Source
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

View Source
var ErrInvalidTxOptions = errors.New("orm: invalid transaction options")

ErrInvalidTxOptions is returned by a transaction opener handed a policy that does not say what it means: none at all, an unset (or unknown) isolation level, an unset attempt budget, or a retry budget on an opener that cannot retry. The error names the call site. Nothing is begun.

View Source
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

func AfterCommit(ctx context.Context, fn func(context.Context)) error

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

func ConstraintName(err error) string

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

func DecodeCursor(token string) (string, error)

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 Detached added in v1.0.0

func Detached(ctx context.Context) context.Context

Detached returns a ctx whose ORM and database/sql calls run on the POOL, outside any transaction ctx carries — on every database. It keeps ctx's deadline, cancellation and values; it only hides the transactions.

// The ledger row commits on its own: it records the attempt even when
// the caller's transaction rolls back.
err := db.CreateLedgerEntry(orm.Detached(ctx), s.deps.DB, entry)

Every resolution the carried transaction drives honours it: generated delegates, pkg/crud, db.Bun() builders and its ExecContext/QueryContext, the raw Exec/Query/QueryRow passthroughs, SQLConn and DBTX, InTx (false), Client.InTx (false) and AfterCommit (ErrNoTransaction). Each statement is its own autocommit transaction on a pooled connection. RunTx with the detached ctx opens a NEW transaction, which the ctx it hands fn carries as usual, so a detached multi-statement write is `client.RunTx(orm.Detached(ctx), fn)`.

It is the statement-sized sibling of BeginDetached: the same "never joins" rule, without a transaction for the caller to end. An explicit handle is not affected — a statement run on a *Tx (tx.Bun(), tx.Exec) is that transaction's whatever ctx it is given.

When to use it

For a write that must survive the caller's rollback: an audit record, a write or idempotency ledger, an attempt counter, a failure log. Those are facts about the attempt, and the attempt happened whether or not its work commits.

When not to

  • Anything that must roll back with the caller. A detached write is committed before the caller's transaction ends, and stays committed if it rolls back or RunTx retries it — so under RunTx it runs once PER ATTEMPT. Make it idempotent, or record it with AfterCommit if it should exist only when the work committed.
  • Reading the caller's uncommitted writes. A detached read sees only committed rows.
  • Touching a row the caller's transaction has written or locked. The detached statement runs on a second connection and waits for the caller's lock, while the caller waits for it to return: a deadlock postgres cannot see. The same holds when the pool has no connection to spare — a pool of one deadlocks on the first detached call inside a transaction.

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

func EncodeCursor(id string) string

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:

  1. ADD new column with desired schema
  2. Migrate data from old column to new column (using DataMigration)
  3. 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 InTx added in v0.1.46

func InTx(ctx context.Context) bool

InTx reports whether ctx carries an active transaction — one opened by RunTx (or joined through Tx.RunTx) whose attempt has not yet committed or rolled back. A ctx captured inside RunTx and used after it returned reports false, exactly as its queries then run on the pool.

It answers for the innermost transaction, whatever its database — the same one AfterCommit attaches to. Use it where code must behave differently inside a caller's transaction: refuse to take a lock that needs its own transaction (BeginDetached would run on a SECOND connection, and a row the caller's transaction holds would deadlock it against itself), or decide whether a side effect has to be deferred.

func InlineValues added in v1.0.0

func InlineValues(ctx context.Context) context.Context

InlineValues returns ctx with parameter binding off: every statement a Client's bun handle runs with it is sent exactly as bun formats it, values in the text. It is the escape hatch for a statement binding mishandles; to keep a single value in the text, use Literal.

func IsCheckViolation

func IsCheckViolation(err error) bool

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

func IsUniqueViolation(err error) bool

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

func JSON(v any) driver.Valuer

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.

It is also how a `?` argument says "this is a JSON document". A slice passed through Arg binds as a postgres ARRAY, never as JSON, so a value a json/jsonb operand needs is wrapped explicitly — and bound, as a text parameter postgres parses with jsonb's input function:

q.Where("labels @> ?", orm.JSON([]string{"urgent"}))

func ListDialects

func ListDialects() []string

ListDialects returns a list of all registered dialect names.

func MarshalJSONBList

func MarshalJSONBList[T proto.Message](list []T) (string, error)

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

func MarshalJSONBMap(m any) (string, error)

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

func MarshalJSONBMessage[T proto.Message](m T) (string, error)

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

func MarshalJSONBScalars(list any) (string, error)

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

func NewQueryError(query string, err error) error

NewQueryError creates a new QueryError

func NewSchemaError

func NewSchemaError(table, message string, err error) error

NewSchemaError creates a new SchemaError

func NewTransactionError

func NewTransactionError(operation string, err error) error

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

func ScanJSON(dst any) sql.Scanner

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

func UnmarshalJSONBList[T proto.Message](s string, dst *[]T) error

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

func UnmarshalJSONBMap(s string, dst any) error

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

func UnmarshalJSONBMessage[T proto.Message](s string, dst *T) error

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

func UnmarshalJSONBScalars(s string, dst any) error

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

func ValidateIdentifier(name string) error

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

func ValidateOrderBy(clause string, allowedColumns []string) error

ValidateOrderBy validates a comma-separated ORDER BY clause against a column allowlist.

Two layers:

  1. Shape: only identifier characters (letters, digits, underscores) in column names, with an optional ASC/DESC direction token.
  2. 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 ArgListValue added in v1.0.0

type ArgListValue struct {
	// contains filtered or unexported fields
}

ArgListValue is a list of values bound one parameter each. See ArgList.

func ArgList added in v1.0.0

func ArgList(values any) ArgListValue

ArgList is bun.In with every element bound, integers included:

q.Where("id IN (?)", orm.ArgList(ids))

The statement's text then varies only with the number of elements. A value that is not a slice or array renders as bun.In renders it.

func (ArgListValue) AppendQuery added in v1.0.0

func (l ArgListValue) AppendQuery(gen schema.QueryGen, b []byte) ([]byte, error)

AppendQuery implements schema.QueryAppender.

type ArgValue added in v1.0.0

type ArgValue struct {
	// contains filtered or unexported fields
}

ArgValue is a `?` argument a binding render sends as a bind parameter whatever its type. See Arg.

func Arg added in v1.0.0

func Arg(v any) ArgValue

Arg marks v to be sent as a bind parameter. Statements a Client runs bind their string, time, []byte and JSON values on their own, and every column value of a model; an integer or float `?` argument is the one value bun formats without consulting the dialect, so on a hot statement wrap it:

db.Bun().NewSelect().Model(&rows).Where("seq > ?", orm.Arg(seq))

Slices are postgres arrays

A Go slice or array — or a pointer to one — passed to Arg binds as a postgres ARRAY, the way pgx encodes a []T and the way a model's `,array` column binds:

q.Where("kind = ANY(?)", orm.Arg(kinds))      // kinds []string → text[]
q.Where("tags @> ?", orm.Arg([]string{"a"}))  // array containment

The exceptions keep their own encoding: []byte (and any slice of a byte kind, json.RawMessage included) stays bytea or JSON exactly as bun writes it, and a value that renders itself (a schema.QueryAppender or a driver.Valuer) is left to do so. A nil slice is NULL.

JSON is never inferred from a slice: say it with JSON(v), which binds v's json.Marshal text (`doc @> ?`, orm.JSON(filter)). For an IN list of one parameter per element, use ArgList.

A bare slice passed as a `?` argument WITHOUT Arg is bun's, not forge's: bun writes it as JSON. Wrap it — Arg for an array, JSON for a document.

Outside a binding render Arg renders exactly as the bound statement reads.

func (ArgValue) AppendQuery added in v1.0.0

func (a ArgValue) AppendQuery(gen schema.QueryGen, b []byte) ([]byte, error)

AppendQuery implements schema.QueryAppender.

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 NewClient

func NewClient(dialectName, dsn string) (*Client, error)

NewClient opens a new ORM client on jackc/pgx, the driver forge's servers open their pool with (serverkit.OpenDB). dialectName must be "postgres".

func NewClientWithDB

func NewClientWithDB(db *sql.DB, dialectName string) (*Client, error)

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) BeginDetached added in v0.1.46

func (c *Client) BeginDetached(ctx context.Context, policy TxPolicy) (*Tx, error)

BeginDetached opens a transaction that the CALLER owns and ends: it stays open across function returns until Commit or Rollback, which is what a closure-scoped RunTx cannot express. The canonical use is a transaction-scoped lock held for the length of a unit of work whose shape is not a single closure:

func LockRun(ctx context.Context, db orm.Context, key int64) (release func(), err error) {
    if orm.InTx(ctx) {
        return nil, errors.New("LockRun needs its own transaction; call it outside RunTx")
    }
    tx, err := db.BeginDetached(ctx, orm.ReadCommitted)
    if err != nil {
        return nil, err
    }
    if _, err := tx.Exec(ctx, "SELECT pg_advisory_xact_lock($1)", key); err != nil {
        _ = tx.Rollback()
        return nil, err
    }
    return func() { _ = tx.Rollback() }, nil // releasing the lock = ending the tx
}

It is "detached" because it never joins the transaction ctx carries: it always begins a NEW one on its own pooled connection, at opts' isolation (READ ONLY, and DEFERRABLE under SERIALIZABLE, when opts.ReadOnly). Inside a caller's RunTx that is a second, independent transaction — its commit survives the caller's rollback, and it blocks on any row the caller's transaction has locked. Check InTx first when that is not what you mean. For a statement (or a closure) that only needs to run outside the caller's transaction, with no transaction of its own to hold open, use Detached(ctx) instead.

What the caller takes on, compared with RunTx:

  • Ending it. Every path must Commit or Rollback; a leaked transaction pins a pooled connection and its locks until ctx ends. Rollback after Commit is a harmless sql.ErrTxDone, so `defer tx.Rollback()` is safe.
  • Its lifetime is bounded by ctx. database/sql rolls the transaction back when ctx is cancelled, which is usually the right release for a request-scoped lock. Pass context.WithoutCancel(ctx) to outlive it.
  • No retry. opts.MaxAttempts must be 1 — orm.ReadCommitted, or orm.Serializable.WithMaxAttempts(1) — and anything else is rejected with ErrInvalidTxOptions: a serialization failure is returned to the caller, who decides whether to run the unit again, and a call site that asked for retries would be promising something nobody performs.

Statements run through the returned *Tx (its Exec/Query/QueryRow and Bun()); tx.Join(ctx, fn) hands fn a ctx that carries it, so generated stores and pkg/crud join it like any RunTx. AfterCommit callbacks registered in that ctx run on Commit.

It is also the low-level primitive: the one way to begin a transaction the caller ends. (Client.BeginTx, which took *sql.TxOptions and so could silently inherit the database's default level, is gone.)

func (*Client) Bun

func (c *Client) Bun() bun.IDB

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

func (c *Client) BunDB() *bun.DB

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) Close

func (c *Client) Close() error

Close closes the database connection.

func (*Client) DB

func (c *Client) DB() *sql.DB

DB returns the underlying *sql.DB for advanced usage and for the kept schema-truth machinery's database/sql seam.

func (*Client) DBTX added in v1.0.0

func (c *Client) DBTX() SQLConn

DBTX returns a SQLConn that resolves the transaction on every call: each method runs on SQLConn(ctx) for the ctx it is given. Construct sqlc's Queries over it once (pgdb.New(client.DBTX())) and every query joins the RunTx transaction its ctx carries, like the ORM's own.

func (*Client) Dialect

func (c *Client) Dialect() 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

func (c *Client) Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error)

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) InTx added in v1.0.0

func (c *Client) InTx(ctx context.Context) bool

InTx reports whether ctx carries an active transaction on c's pool — the one c's queries, SQLConn and DBTX would join. Unlike the package-level InTx, which answers for the innermost transaction on any database, this is the check to make before joining instead of beginning: a transaction on another pool is no reason not to open one on this one.

func (*Client) Query

func (c *Client) Query(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)

Query runs a raw SQL query (escape hatch). See Exec for the raw-passthrough rationale.

func (*Client) QueryRow

func (c *Client) QueryRow(ctx context.Context, query string, args ...interface{}) *sql.Row

QueryRow runs a raw SQL query returning at most one row (escape hatch).

func (*Client) RunTransaction

func (c *Client) RunTransaction(ctx context.Context, policy TxPolicy, fn func(ctx Context) error) error

RunTransaction executes fn within a transaction at opts' isolation, 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: fn 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.Join to get such a ctx.

opts mean exactly what they mean to RunTx: orm.ReadCommitted runs fn once, orm.Serializable re-runs it on 40001 / 40P01, so fn must then have no effect outside the database. There is no default; an unset level is rejected with ErrInvalidTxOptions.

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) RunTx

func (c *Client) RunTx(ctx context.Context, policy TxPolicy, fn func(ctx context.Context) error) error

RunTx runs fn in a transaction at opts' isolation, carried by the ctx fn receives, running it up to opts.MaxAttempts times: a 40001 / 40P01 failure rolls the attempt back and re-runs fn from the top. With orm.Serializable fn MAY RUN MORE THAN ONCE; see the package documentation in tx.go and AfterCommit. opts has no default — see TxOptions — and an unset level is rejected with ErrInvalidTxOptions before anything begins.

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. opts are still validated — a call site that names no level is wrong whether or not it happens to join — and otherwise IGNORED: 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.

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

func (c *Client) RunTxReadOnly(ctx context.Context, fn func(ctx context.Context) error) error

RunTxReadOnly is RunTx for a transaction that only reads: SERIALIZABLE READ ONLY DEFERRABLE, orm.Serializable.WithReadOnly(). See TxOptions.ReadOnly for why that matters.

It needs no level argument because a read-only transaction has one right answer: postgres rejects every write in it, so the lost update the level choice guards against cannot happen, and SERIALIZABLE READ ONLY DEFERRABLE is the strongest snapshot at no conflict cost.

func (*Client) SQLConn added in v1.0.0

func (c *Client) SQLConn(ctx context.Context) SQLConn

SQLConn returns the database/sql executor ctx resolves to on c's pool: the *sql.Tx of the active transaction ctx carries for this database (from RunTx, or a BeginTx'd *Tx's RunTx), else the *sql.DB itself. Statements run on it exactly as generated delegates would run with the same ctx. A statement prepared on it inside a transaction is bound to that transaction, as *sql.Tx.PrepareContext is.

Resolve it per use, with the ctx the statement runs with; a value kept past the transaction's end is a finished *sql.Tx. Use DBTX for a value to hold.

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. opts are required, exactly as for RunTx.
	RunTransaction(ctx context.Context, policy TxPolicy, fn func(Context) error) error

	// RunTx runs fn in a transaction at opts' isolation, CARRIED BY the ctx
	// fn receives, re-running fn on 40001 / 40P01 up to opts.MaxAttempts.
	// Every query made with that ctx against this database — generated
	// delegates, stores, pkg/crud, raw SQL — joins the transaction, with no
	// handle to thread. A ctx that already carries a transaction on this
	// database is joined, not nested.
	//
	// opts has no default: name orm.Serializable (read-check-write with no
	// locks; fn may run more than once, so defer effects outside the
	// database with AfterCommit) or orm.ReadCommitted (correctness from a
	// lock or a single statement; one attempt). See TxOptions.
	//
	// 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, policy TxPolicy, 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.
	// It takes no level: a transaction that cannot write has one right one.
	RunTxReadOnly(ctx context.Context, fn func(ctx context.Context) error) error

	// BeginDetached opens a NEW transaction the caller owns and must end
	// with Commit or Rollback — one that outlives the call that opened it,
	// such as a transaction-scoped advisory lock released later. It never
	// joins the transaction ctx carries (on *Tx it opens a sibling on the
	// same database) and never retries, so opts.MaxAttempts must be 1
	// (orm.ReadCommitted, orm.Serializable.WithMaxAttempts(1)). See
	// Client.BeginDetached.
	//
	// On the interface for the same reason as RunTx: code written against
	// the injected `DB orm.Context` must not need the concrete *Client to
	// hold a transaction across calls.
	BeginDetached(ctx context.Context, policy TxPolicy) (*Tx, 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

func GetDialect(name string) (Dialect, error)

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

type IndexSchema struct {
	Name   string
	Fields []string
	Unique bool
}

IndexSchema represents a database index

type Int64Array

type Int64Array []int64

Int64Array scans an integer-array column from either encoding.

func (*Int64Array) Scan

func (a *Int64Array) Scan(src any) error

Scan implements sql.Scanner.

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

type IntrospectedIndex struct {
	Name     string
	Columns  []string
	IsUnique bool
}

IntrospectedIndex represents a database index discovered through introspection

type Isolation

type Isolation int

Isolation is a transaction's isolation level, and the policy every opener takes: RunTx(ctx, orm.Serializable, fn), BeginDetached(ctx, orm.ReadCommitted). Each level carries the attempt budget it needs — Serializable and RepeatableRead are retried on 40001 / 40P01 up to DefaultTxMaxAttempts, ReadCommitted runs once — and WithMaxAttempts / WithReadOnly refine it into a TxOptions.

It has no default. The zero value is UNSET, and every opener rejects it: SERIALIZABLE and READ COMMITTED make opposite correctness arguments (the database proves serializability, versus a lock or a single statement does), so whichever one a blank value silently meant would be wrong for the code written for the other.

const (
	// Serializable is SERIALIZABLE, retried on 40001 / 40P01 up to
	// DefaultTxMaxAttempts. Correct for read-check-write with NO locks: the
	// database aborts the loser of a race and the retry re-reads committed
	// state. fn may run more than once. Do not take a lock inside it
	// (forgeconv-tx-lock-under-serializable): the snapshot predates the lock
	// wait, so the waiter wakes into stale data and fails 40001.
	Serializable Isolation = iota + 1

	// RepeatableRead is REPEATABLE READ (snapshot isolation), retried on
	// 40001 / 40P01 up to DefaultTxMaxAttempts. Every statement sees the
	// snapshot taken at the first one; a write to a row changed since fails
	// with 40001. It does NOT detect write skew — two transactions each
	// reading what the other writes — which SERIALIZABLE does.
	RepeatableRead

	// ReadCommitted is READ COMMITTED, one attempt. Each STATEMENT sees the
	// rows committed before it began, so read-then-write is protected from a
	// lost update only by a lock it takes (SELECT … FOR UPDATE,
	// pg_advisory_xact_lock) — or by being one statement. It takes no
	// predicate locks and never raises 40001, so it has nothing to retry
	// unless it can deadlock; then say so with WithMaxAttempts.
	ReadCommitted
)

func (Isolation) String

func (i Isolation) String() string

String names the isolation level as postgres spells it.

func (Isolation) WithMaxAttempts added in v1.0.0

func (i Isolation) WithMaxAttempts(n int) TxOptions

WithMaxAttempts returns the level with the attempt budget n (1 = no retry).

func (Isolation) WithReadOnly added in v1.0.0

func (i Isolation) WithReadOnly() TxOptions

WithReadOnly returns the level as a READ ONLY transaction (DEFERRABLE under SERIALIZABLE; see TxOptions.ReadOnly).

type LiteralValue added in v1.0.0

type LiteralValue struct {
	// contains filtered or unexported fields
}

LiteralValue is a value always written into the SQL text. See Literal.

func Literal added in v1.0.0

func Literal(v any) LiteralValue

Literal keeps v in the statement's text instead of binding it. Use it for a value the planner must see while planning — one a partial index's predicate is proved from — since a prepared statement's generic plan cannot see a parameter's value.

func (LiteralValue) AppendQuery added in v1.0.0

func (l LiteralValue) AppendQuery(gen schema.QueryGen, b []byte) ([]byte, error)

AppendQuery implements schema.QueryAppender.

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

func NewSQLMigration(version, description, upSQL string) *Migration

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

Status returns the status of all migrations

type MigrationStatus

type MigrationStatus struct {
	Version     string
	Description string
	Applied     bool
	AppliedAt   time.Time
}

MigrationStatus represents the status of a migration

type NullTime

type NullTime struct {
	Time  time.Time
	Valid bool
}

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.

func (*NullTime) Scan

func (n *NullTime) Scan(value any) error

Scan implements sql.Scanner.

func (NullTime) Value

func (n NullTime) Value() (driver.Value, error)

Value implements driver.Valuer so NullTime round-trips on writes too.

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 Order

type Order string

Order represents sort direction.

const (
	Asc  Order = "ASC"
	Desc Order = "DESC"
)

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

type QueryError struct {
	Query string
	Err   error
}

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 WithOffset

func WithOffset(offset int) QueryOption

WithOffset sets the OFFSET.

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.

func WithWhere

func WithWhere(column string, op Operator, value any) QueryOption

WithWhere adds a WHERE clause. Identifiers are bound via Bun's `?` placeholders with bun.Ident, so column names are safely quoted; the value goes through Arg (IN lists through ArgList), so it is sent as a bind parameter whatever its type (bind.go).

type SQLConn added in v1.0.0

type SQLConn interface {
	ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
	PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
	QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
	QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}

SQLConn is the executor surface *sql.DB and *sql.Tx share. It is the shape sqlc's generated DBTX interface names, so either value — and DBTX's — satisfies a sqlc Queries directly.

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

type SchemaError struct {
	Table   string
	Message string
	Err     error
}

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.

func (*StringArray) Scan

func (a *StringArray) Scan(src any) error

Scan implements sql.Scanner.

type TableSchema

type TableSchema struct {
	Name    string
	Fields  []FieldSchema
	Indexes []IndexSchema
}

TableSchema represents a database table schema

func IntrospectAllTables

func IntrospectAllTables(ctx context.Context, db Context, dialect Dialect) ([]TableSchema, error)

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

type TransactionError struct {
	Operation string
	Err       error
}

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) BeginDetached added in v0.1.46

func (t *Tx) BeginDetached(ctx context.Context, policy TxPolicy) (*Tx, error)

BeginDetached on a *Tx opens a NEW transaction on the same database, not a nested one: see Client.BeginDetached. It never joins t.

func (*Tx) Bun

func (t *Tx) Bun() bun.IDB

Bun returns the transaction as a bun.IDB. Queries built on it run in the transaction with their values bound, as on Client.Bun().

func (*Tx) Commit

func (t *Tx) Commit() error

Commit commits the transaction, then runs the callbacks AfterCommit registered against it, in order. A failed commit runs none.

func (*Tx) Dialect

func (t *Tx) Dialect() 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

func (t *Tx) Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error)

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) Join added in v1.0.0

func (t *Tx) Join(ctx context.Context, fn func(ctx context.Context) error) error

Join 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, and the transaction keeps the level it was begun at. This is how code holding an explicit handle — from BeginDetached, or RunTransaction — calls a service written with RunTx.

It takes no policy because it decides none: a level spelled at a join would claim an isolation the transaction may not have.

func (*Tx) Query

func (t *Tx) Query(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)

Query runs a raw SQL query within the transaction (raw passthrough).

func (*Tx) QueryRow

func (t *Tx) QueryRow(ctx context.Context, query string, args ...interface{}) *sql.Row

QueryRow runs a raw SQL query within the transaction (raw passthrough).

func (*Tx) Rollback

func (t *Tx) Rollback() error

Rollback rolls back the transaction and discards its after-commit callbacks.

func (*Tx) RunTransaction

func (t *Tx) RunTransaction(ctx context.Context, policy TxPolicy, fn func(Context) error) error

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. opts are validated, as everywhere, and otherwise ignored.

func (*Tx) RunTx

func (t *Tx) RunTx(ctx context.Context, policy TxPolicy, fn func(ctx context.Context) error) error

RunTx on a *Tx is a join: see Join. It exists so *Tx satisfies Context — code written against Context (a service's RunTx(ctx, orm.Serializable, …)) composes into a transaction it was handed. The policy is validated, as on Client.RunTx, and otherwise ignored. Holding a *Tx, call Join.

func (*Tx) RunTxReadOnly

func (t *Tx) RunTxReadOnly(ctx context.Context, fn func(ctx context.Context) error) error

RunTxReadOnly on a *Tx is a join; see Join. The read-only request is ignored because the transaction's mode is already fixed.

type TxOptions

type TxOptions struct {
	// Isolation selects the isolation level. Required.
	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: 1 is no retry, N re-runs fn
	// from the top on 40001 / 40P01 until N attempts have failed. Required
	// (at least 1). BeginDetached hands the transaction to the caller, so it
	// cannot retry and requires exactly 1.
	MaxAttempts int
}

TxOptions is a transaction's full policy: its isolation level, its access mode and how many times it may run. Derive one from a level rather than writing the struct out:

db.RunTx(ctx, orm.Serializable, fn)                     // retried up to DefaultTxMaxAttempts
db.RunTx(ctx, orm.ReadCommitted, fn)                    // one attempt
db.RunTx(ctx, orm.ReadCommitted.WithMaxAttempts(3), fn) // retried on deadlock (40P01)
tx, err := db.BeginDetached(ctx, orm.Serializable.WithReadOnly().WithMaxAttempts(1))

Every field that decides behaviour must be set: the zero value is rejected. The level and the retry budget are stated together on purpose: retry is what makes SERIALIZABLE correct (an aborted transaction that is not re-run is a failed request), and what READ COMMITTED needs almost never.

func ResolveDetachedTxPolicy added in v1.0.0

func ResolveDetachedTxPolicy(op string, p TxPolicy) (TxOptions, error)

ResolveDetachedTxPolicy is ResolveTxPolicy for an opener that hands the transaction to its caller, as BeginDetached does: such an opener cannot re-run the caller's work, so it also requires exactly one attempt.

func ResolveTxPolicy added in v1.0.0

func ResolveTxPolicy(op string, p TxPolicy) (TxOptions, error)

ResolveTxPolicy validates the policy a project's OWN transaction opener was handed — a func that takes an orm.TxPolicy and runs the transaction itself, such as a repository wrapper with a retry ladder of its own — and returns it as full options: level, access mode and attempt budget. It refuses what RunTx refuses (no policy, an unset or unknown level, no attempt budget), so the wrapper's callers name a level exactly as RunTx's do.

op names the opener for the error ("Repo.RunTx"), and the error names the line that called the opener — one frame above the caller of ResolveTxPolicy. Declare the opener with `//forge:tx-opener level-arg=<n>` so forgeconv-tx-lock-under-serializable judges its calls as it judges RunTx's.

func (TxOptions) String added in v1.0.0

func (o TxOptions) String() string

String describes the policy: "serializable, 7 attempts".

func (TxOptions) WithMaxAttempts added in v1.0.0

func (o TxOptions) WithMaxAttempts(n int) TxOptions

WithMaxAttempts returns o with the attempt budget n (1 = no retry).

func (TxOptions) WithReadOnly added in v1.0.0

func (o TxOptions) WithReadOnly() TxOptions

WithReadOnly returns o as a READ ONLY transaction (DEFERRABLE under SERIALIZABLE; see TxOptions.ReadOnly).

type TxPolicy added in v1.0.0

type TxPolicy interface {
	// contains filtered or unexported methods
}

TxPolicy is what a transaction opener takes: an Isolation constant, or a TxOptions refined from one. Only this package implements it.

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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL