db

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Sep 3, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

Documentation

Overview

Package db owns database access. Every query runs inside a transaction whose scope is a type parameter: a repository that accepts Tx[Tenant] cannot be handed a Tx[System], and nothing can run outside a transaction. The tenant is applied with set_config(..., true) so Postgres row-level security enforces isolation; there is no Go-side tenant predicate anywhere.

This package is the only place in the program that writes a platformkit.* setting. scripts/check_gucs.sh keeps it that way, because those settings are USERSET: any statement could rewrite them, so the barrier is a grep and a re-read (see Run and RunSystem), not a database privilege.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrNoTenant is returned by Run when the context carries no tenant.
	ErrNoTenant = errors.New("db: no tenant in context")

	// ErrScopeMismatch is returned when a transaction cannot join the one
	// already open on the context: a system transaction inside a tenant one, a
	// tenant transaction inside a system one, or a second tenant inside the
	// first tenant's transaction.
	ErrScopeMismatch = errors.New("db: transaction scope mismatch")

	// ErrNoSystemToken is returned by RunSystem for a nil token. Only
	// kit/internal/syscap can produce one, so a nil token is the only forgery
	// Go allows and this is where it stops.
	ErrNoSystemToken = errors.New("db: system token was not minted by the kernel")

	// ErrScopeTampered is returned when a transaction ends under different
	// settings than the ones it opened with. See sealed.
	ErrScopeTampered = errors.New("db: the transaction's tenancy settings were rewritten inside it")
)

Functions

func Detached

func Detached(ctx context.Context) context.Context

Detached returns ctx with no transaction on it, so that a Run or a RunSystem below it opens its own instead of joining — or refusing to join — the one the caller already holds.

It exists for one shape of work: a request that has resolved a tenant, opened a tenant transaction to recognise its caller, and now has to touch a table that belongs to no tenant. Those are two transactions and they cannot be one, because a system transaction cannot widen a tenant one half way through.

The consequence is the thing to understand, and it is why this is spelled out rather than hidden: the detached transaction commits on its own, so its work survives a request that afterwards fails. A caller uses it for a control-plane write it means to keep — creating a tenant, recording a failed login — and for nothing else.

func Migrate

func Migrate(ctx context.Context, migrateURL string, fsys fs.FS) error

Migrate applies every pending migration in fsys, in filename order, against the single "schema_migrations" ledger. migrateURL connects as the owner role, which holds the DDL rights the application role deliberately lacks; the ledger and every object land in that connection's search_path.

func Now

func Now() time.Time

Now is the clock everything that writes a timestamp reads: UTC, truncated to the microsecond.

Both halves are about the database. UTC, because a time that depends on where the process runs cannot be compared across two replicas in two regions, and timestamptz stores an instant rather than the offset it was written with, so the zone is lost anyway. Truncated to the microsecond, because that is the resolution timestamptz keeps: a Go time carries nanoseconds, so a value written and read back differs from the one in memory in its last three digits, and every equality on it — a test comparing what a command returned with what the next command read, an idempotency check that asks whether the resolution time moved — is a coin toss. Truncating at the source makes the round trip exact.

func Run

func Run(ctx context.Context, c *Conn, fn func(ctx context.Context, tx Tx[Tenant]) error) error

Run opens a tenant-scoped transaction for the tenant in ctx and applies it with set_config('platformkit.tenant_id', ..., true), which is what the row-level security policies in migrations/ read. Nested calls join the enclosing tenant transaction, including a request's Pending one.

func RunSystem

func RunSystem(ctx context.Context, c *Conn, tok tenancy.SystemToken, fn func(ctx context.Context, tx Tx[System]) error) error

RunSystem opens a cross-tenant transaction. Only packages under kit/ can obtain a token, and every open is logged with the reason it was minted for. Nested calls join an enclosing system transaction; an open tenant transaction is a scope mismatch, because crossing tenants half way through tenant work is never what the caller meant.

func TenantOf

func TenantOf(tx Tx[Tenant]) tenancy.Tenant

TenantOf is the tenant a tenant-scoped transaction belongs to. It is a function rather than a method because Go does not allow a method on one instantiation of a generic type.

func TryLock

func TryLock(ctx context.Context, c *Conn, name string) (unlock func(), ok bool, err error)

TryLock takes a Postgres advisory lock named name, so that at most one process in a cluster runs the work behind it. It reports false when another process already holds it, which is the ordinary outcome on every replica but one and not an error.

The lock is session-level and lives on a connection of its own, pinned until unlock is called. It has to be: the work it guards is not a transaction — a periodic job opens transactions of its own, and a lock held by an enclosing transaction would make every db.Run inside it a scope mismatch.

The key is Postgres's own hash of the name, so two processes that spell the name the same way take the same lock without a registry of numbers to keep in step. Locks are per-database and independent of any schema.

Types

type Conn

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

Conn is the application connection (role platformkit_app, NOSUPERUSER).

func Open

func Open(ctx context.Context, url string) (*Conn, error)

Open connects as the application role. It refuses a role that row-level security would not bind, because such a connection would make every isolation test and every policy in migrations/ decorative.

func (*Conn) Close

func (c *Conn) Close() error

Close releases the pool.

type Pending

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

Pending is a tenant transaction that has not been opened yet.

It exists for one caller, kit/httpx: a request cannot know at its start whether it will query, and opening a transaction for one that never does means a liveness probe sent to a tenant host fails while the database is down — which restarts every replica during the outage instead of after it. So the request carries a Pending, the first Tx or Run opens it, and Close commits or rolls back once the response is decided.

func Lazy

Lazy puts a Pending for the tenant in ctx on the returned context. Nothing reaches the database until something asks for the transaction.

It takes a kernel token for the same reason RunSystem does. A Pending is a transaction whose commit is somebody else's decision, which is a capability no module may hold: Run opens and closes its own transaction in one call, and that is the only door outside kit/. Only kit/httpx mints a token for it.

func (*Pending) Close

func (p *Pending) Close(keep bool) error

Close ends the transaction: it commits when keep is true and the settings the open placed are still the ones in force, and rolls back otherwise. It is a no-op when nothing was ever opened, and idempotent.

func (*Pending) Err

func (p *Pending) Err() error

Err is the failure that stopped the transaction opening, if one did.

func (*Pending) Tx

func (p *Pending) Tx(ctx context.Context) (Tx[Tenant], error)

Tx opens the transaction if it is not open yet and returns the handle. A failed open is remembered: a request that could not reach the database asks once, not once per query.

type Scope

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

Scope is what a transaction may see. The set is closed: one tenant, or every tenant. isScope is unexported so no package outside kit/db can add a third.

type System

type System struct{}

System scopes a transaction across every tenant. It carries nothing: the reason a system transaction was opened belongs to the token and to the log line RunSystem writes, not to the handle.

type Tenant

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

Tenant scopes a transaction to the tenant that opened it.

type Tx

type Tx[S Scope] struct {
	// contains filtered or unexported fields
}

Tx is a transaction-bound handle whose scope is part of its type. A repository that takes Tx[Tenant] cannot be handed a Tx[System], and neither can be constructed outside this package, so there is no way to query outside a transaction. See scope_compile_test.go.

func (Tx[S]) DB

func (t Tx[S]) DB() *gorm.DB

DB is the transaction-bound GORM handle. Every query on it runs inside the transaction, under the settings Run, RunSystem or Pending placed.

Directories

Path Synopsis
Package dbtest gives one test its own Postgres schema.
Package dbtest gives one test its own Postgres schema.

Jump to

Keyboard shortcuts

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