db

package
v0.1.16 Latest Latest
Warning

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

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

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrUnavailable = errors.New("database unavailable")

ErrUnavailable is returned when the database cannot SERVE the query (as opposed to rejecting it): the queryTimeout elapsed, the circuit breaker is open, the connection failed, or PostgreSQL reported that it cannot take the work (connection_exception, insufficient_resources, shutting down). Handlers map it to 503 + Retry-After — "retry later", not "your request is broken". See isUnavailableCause for the exact classification and what is deliberately excluded.

Functions

func FileReferenceViolation

func FileReferenceViolation(err error) (column string, ok bool)

FileReferenceViolation reports whether err is a foreign_key_violation on a `file` field (FILES-LINK-S1): a write whose file_id references no row of the tenant's files table. It returns the violating column (the file field's name, parsed from the error Detail) so handlers can answer the write with the SAME field-addressed 422 shape the declarative validator uses — a bad file reference is field-level input validation to the client, even though the guard is the real FK. A delete blocked by a file FK (the other direction) is NOT this case — that stays the generic 409 (ForeignKeyViolation).

func ForeignKeyViolation

func ForeignKeyViolation(err error) (msg string, ok bool)

ForeignKeyViolation reports whether err is a Postgres foreign_key_violation (SQLSTATE 23503) and, if so, returns a safe, human-readable message naming the related resource (parsed from the error Detail, never raw SQL). Handlers map this to 409 Conflict — a delete blocked by a RESTRICT FK, or a write referencing a row that does not exist — instead of leaking a masked 500. ok is false for any other error. The resource names exposed are the tenant's OWN schema resources, not engine internals.

func IsBadInput

func IsBadInput(err error) bool

IsBadInput reports whether err is a client-supplied value that Postgres could not parse or store for a column type. These are caller errors, so handlers answer 400 instead of leaking a 500. See badInputCodes for the exact set and why it is wider than the 22P02 it started as.

func IsMissingTenant

func IsMissingTenant(err error) bool

IsMissingTenant reports whether err is a "schema/relation does not exist" Postgres error (SQLSTATE 42P01 undefined_table or 3F000 invalid_schema_name). In this schema-per-tenant design that means the tenant is not provisioned, so handlers answer 400 "invalid tenant" rather than leaking a 500 SQL error.

func IsUnavailable

func IsUnavailable(err error) bool

IsUnavailable reports whether err signals an unreachable database. It matches both the classified sentinel (errors wrapped by this package's classify) AND a RAW unclassified cause (a bare pgconn/net error a CUSTOM handler got from its own SQL on the tenant tx) — CONSUMER-PATH-S1 (ENG-10): custom handlers never pass through classify, so with the sentinel-only check a stopped PostgreSQL surfaced as 500 "internal error" on every custom route while the generated routes answered the honest 503. Error-path only — never on the hot path.

func NewPool

func NewPool(ctx context.Context, connStr string) (*pgxpool.Pool, error)

func UndefinedColumnField

func UndefinedColumnField(err error) (field string, ok bool)

UndefinedColumnField reports whether err is a Postgres undefined_column (SQLSTATE 42703) and, if so, returns the column name parsed from the message. On the write path this means the client sent a field that neither the schema nor the live table knows: handlers map it to 422 unknown_field instead of a masked 500. The DB stays the source of truth for the writable column set (the schema can grow columns at runtime — see the no-whitelist NOTE in codegen.BuildRouter), so this classification happens on the error, never as a pre-write whitelist.

func UniqueViolationField

func UniqueViolationField(err error) (field string, ok bool)

UniqueViolationField reports whether err is a Postgres unique_violation (SQLSTATE 23505) and, if so, returns the offending column name parsed from the error Detail (falling back to the constraint name). Handlers map this to 409 without leaking raw SQL. ok is false for any other error.

Types

type TenantDB

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

TenantDB wraps a pgxpool and enforces per-tenant schema isolation via SET LOCAL search_path.

func NewTenantDB

func NewTenantDB(pool *pgxpool.Pool) *TenantDB

func (*TenantDB) ExecRowsTenant

func (tdb *TenantDB) ExecRowsTenant(ctx context.Context, schemaName, query string, args ...any) ([]map[string]any, error)

ExecRowsTenant runs a write query with RETURNING * within a tenant schema, reads all returned rows into memory, and commits the transaction. UUID columns ([16]byte) are converted to hyphenated UUID strings.

func (*TenantDB) ExecRowsTenantEmit

func (tdb *TenantDB) ExecRowsTenantEmit(ctx context.Context, schemaName, query string, emit func(ctx context.Context, tx pgx.Tx, rows []map[string]any) error, args ...any) ([]map[string]any, error)

ExecRowsTenantEmit is ExecRowsTenant plus a same-transaction emit step (CRUD-EMIT-V1): after the write's RETURNING rows are read and BEFORE commit, it calls emit(ctx, tx, rows) on the SAME pgx.Tx. If emit returns an error the transaction rolls back, so the business write and its emitted event are atomic — either both commit or neither does. emit may be nil (then this is exactly ExecRowsTenant). db stays decoupled from pkg/outbox: the caller supplies a closure that enqueues using the handed-in tx.

func (*TenantDB) ExecTenant

func (tdb *TenantDB) ExecTenant(ctx context.Context, schemaName, query string, args ...any) (int64, error)

ExecTenant executes an INSERT/UPDATE/DELETE within a transaction scoped to schemaName. Returns the number of rows affected. The transaction is committed on success.

func (*TenantDB) ExecTenantEmit

func (tdb *TenantDB) ExecTenantEmit(ctx context.Context, schemaName, query string, emit func(ctx context.Context, tx pgx.Tx, affected int64) error, args ...any) (int64, error)

ExecTenantEmit is ExecTenant plus a same-transaction emit step (CRUD-EMIT-V1). After the write executes and BEFORE commit it calls emit(ctx, tx, affected) on the SAME tx — the affected-row count lets the caller skip emission when nothing changed (e.g. a DELETE that matched no row). An emit error rolls the write back. emit may be nil (then this is exactly ExecTenant).

func (*TenantDB) IncludeListJSON

func (tdb *TenantDB) IncludeListJSON(ctx context.Context, pgSchema, query string, args ...any) (data []byte, n int64, err error)

IncludeListJSON runs a RELATIONS-V1 list-with-embeds query (built by query.BuildListInclude) inside the tenant search_path and scans its single row of (data text, n bigint): data is the JSON array of nested rows (the engine writes it straight to the client, no Go round-trip), n the row count for has_next. The query references multiple tables unqualified — the tenant search_path (set by QueryTenant) resolves them, so this path supports embeds across the tenant's tables without per-table qualification.

func (*TenantDB) IncludeOneJSON

func (tdb *TenantDB) IncludeOneJSON(ctx context.Context, pgSchema, query string, args ...any) (data []byte, found bool, err error)

IncludeOneJSON runs a RELATIONS-V1 get-by-id-with-embeds query (built by query.BuildGetInclude) and scans its single JSON object column. found is false when no row matched (→ 404), so an embed never resurrects a row the base WHERE (including the row-level RBAC condition) excluded.

func (*TenantDB) QueryDirect

func (tdb *TenantDB) QueryDirect(ctx context.Context, pgSchema, tableName, query string, args ...any) (pgx.Rows, error)

QueryDirect runs a SELECT using a schema-qualified table name — no transaction, no SET LOCAL. One roundtrip instead of four. Use for read-only list/get handlers. tableName must be the unqualified resource name (e.g. "tasks"); pgSchema the tenant schema (e.g. "tenant_acme"). Both are validated before use.

func (*TenantDB) QueryScalarDirect

func (tdb *TenantDB) QueryScalarDirect(ctx context.Context, pgSchema, tableName, query string, args ...any) (int64, error)

QueryScalarDirect runs a single-int64 query (e.g. COUNT(*)) against the schema-qualified table, mirroring QueryDirect (no transaction, no SET LOCAL) — used by the REST list's opt-in ?count=true total, which shares the list's QueryDirect path.

func (*TenantDB) QueryScalarTenant

func (tdb *TenantDB) QueryScalarTenant(ctx context.Context, schemaName, query string, args ...any) (int64, error)

QueryScalarTenant runs a query that returns a single int64 value (e.g. COUNT(*)) within the tenant schema. Returns 0 if no rows are returned.

func (*TenantDB) QueryTenant

func (tdb *TenantDB) QueryTenant(ctx context.Context, schemaName, query string, args ...any) (pgx.Rows, error)

QueryTenant executes a SELECT within a transaction scoped to schemaName. The transaction is rolled back automatically when the returned rows are closed.

func (*TenantDB) WithTenantTx

func (tdb *TenantDB) WithTenantTx(ctx context.Context, schemaName string, fn func(ctx context.Context, tx pgx.Tx) error) error

WithTenantTx opens ONE transaction scoped to schemaName (search_path set once via SET LOCAL, exactly like the single-statement helpers), runs fn against that tx, and COMMITS iff fn returns nil — otherwise it ROLLS BACK. It is the atomic multi-resource primitive (G4): a caller runs several writes through the one tx and they are all-or-nothing. fn must use ONLY the handed-in tx (never the pool) so every statement shares the tenant search_path and the atomic boundary; a panic or error in fn leaves nothing partially committed (the deferred Rollback runs). The tenant cannot be crossed — the search_path is fixed for the transaction's life.

Jump to

Keyboard shortcuts

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