relational

package module
v0.0.0-...-9257d16 Latest Latest
Warning

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

Go to latest
Published: Nov 12, 2026 License: Apache-2.0 Imports: 25 Imported by: 0

Documentation

Overview

Package relational is the one place the platform decides which relational database it talks to.

It does four things and nothing else: read the configuration (Driver, Target), open a pooled *gorm.DB (Open, Adopt), name the dialect so a repository can choose between two explicit implementations (Dialect, DialectOf), and classify driver errors (Classify). It never rewrites SQL text and is not a second ORM; see docs/RELATIONAL-ADAPTER-DESIGN.md.

The package has no dependency on encore.dev, so both the platform module and the encore-api module import it.

Index

Constants

View Source
const AdvisoryLocksDDL = "CREATE TABLE IF NOT EXISTS " + AdvisoryLocksTable +
	" (lock_key CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL PRIMARY KEY) ENGINE=InnoDB"

AdvisoryLocksDDL is the baseline-migration statement for AdvisoryLocksTable.

View Source
const AdvisoryLocksTable = "tp_advisory_locks"

AdvisoryLocksTable is the table LockTx uses on MySQL. Every MySQL schema whose code calls LockTx must create it in its baseline migration with AdvisoryLocksDDL (it is not created here: a lock helper must not run DDL).

View Source
const KeyDriver = "relational.driver"

Configuration keys. A process reads them through a Source, so the platform (viper) and the Encore app (environment variables) share one definition of what the keys mean.

relational.driver                     postgres (default) | mysql          one value for the whole deployment
relational.<engine>.<resource>.dsn    a complete DSN for one logical resource
relational.<engine>.<field>           structured settings shared by every resource of that engine:
                                      host, port, username, password, time_zone, open_conns, idle_conns,
                                      conn_max_lifetime, log_level, slow_threshold_ms
relational.<engine>.<resource>.<field> the same fields for one resource (database is normally set here)

<engine> is "postgres" or "mysql"; <resource> is the logical resource name ("platform", "device_metadata", ...).

View Source
const LedgerTable = "tp_schema_migrations"

LedgerTable is the per-database ledger of MySQL migrations, keyed by (resource, version).

Variables

View Source
var (
	// ErrChecksumMismatch: a file the ledger already records was edited. Executed migrations are never edited.
	ErrChecksumMismatch = errors.New("a registered migration file was changed")
	// ErrBehind: the database has not applied every migration file this binary ships (Verify), or a file that sorts
	// before an applied one was never applied (Migrate).
	ErrBehind = errors.New("database schema is behind the migration files")
	// ErrUnsupportedDialect: Migrate and Verify only exist for MySQL.
	ErrUnsupportedDialect = errors.New("relational: migrations are executed by this package for MySQL only")
)
View Source
var (
	ErrBusy           = errors.New("schema migration executor already holds the database lock")
	ErrOutcomeUnknown = errors.New("migration outcome is unknown; reconcile the existing owner ledger")
)
View Source
var ErrNotInTransaction = errors.New("relational: LockTx must be called inside a transaction")

ErrNotInTransaction: LockTx was given a handle that is not an open transaction, where a lock would be meaningless.

Functions

func Adopt

func Adopt(d Dialect, pool *sql.DB, p PoolOptions) (*gorm.DB, error)

Adopt wraps a pool somebody else created (for example the *sql.DB behind an Encore sqldb.Database) in a GORM handle and applies the pool options. The pool's owner keeps closing it, so register a handle from Adopt with owned=false.

func CloseDB

func CloseDB(db *gorm.DB) error

CloseDB closes the pool behind db. Call it only for a pool you created with Open.

func EnvName

func EnvName(key string) string

EnvName maps a configuration key to its environment variable.

func ImportMigrationHistory

func ImportMigrationHistory(ctx context.Context, db *gorm.DB, resource string, files []Migration, applied []MigrationReceipt) error

ImportMigrationHistory copies verified receipts from an older migration ledger into tp_schema_migrations. Every receipt must map to exactly one currently shipped migration. Existing new-ledger receipts must agree too. The caller remains responsible for reading the legacy ledger and preserving its receipt rows for consumers that still require them.

func Is

func Is(err error, k Kind) bool

Is reports whether err is of kind k.

func IsUnique

func IsUnique(err error, constraint string) bool

IsUnique reports a unique violation. When constraint is not empty it must also name the violated constraint (PostgreSQL constraint name) or index (MySQL, from the "table.index" form of its message); that is how a caller tells "duplicate device number" from "duplicate idempotency key" when one INSERT can hit either.

func LockTx

func LockTx(ctx context.Context, tx *gorm.DB, keys ...string) error

LockTx takes a transaction-scoped mutual-exclusion lock on each key and holds it until tx commits or rolls back.

PostgreSQL: pg_advisory_xact_lock(hashtextextended(key, 0))
MySQL:      INSERT ... ON DUPLICATE KEY UPDATE (an exclusive row lock) on one row of tp_advisory_locks, keyed by SHA-256 of the key.
            (GET_LOCK is session-scoped and does not end with the transaction, so it is not a substitute.)

This is the one cross-module exception to "no shared SQL in pkg/relational" (decision O3): the semantics must be identical everywhere. With several keys they are de-duplicated and taken in sorted order, so two transactions locking {a,b} and {b,a} cannot deadlock. A lock wait that exceeds the server timeout surfaces as KindTimeout.

func Migrate

func Migrate(ctx context.Context, db *gorm.DB, resource string, fsys fs.FS, dir string) error

Migrate applies every unregistered file of dir to the MySQL database behind db, in version order, holding the database-wide schema lock (ErrBusy when another executor holds it). Registered files must be byte-identical to what was applied (ErrChecksumMismatch). Services that only read a schema they do not own call Verify instead.

func Open

func Open(ctx context.Context, t Target) (*gorm.DB, error)

Open creates the connection pool of one logical resource, verifies it with a ping, and returns the GORM handle. The caller owns the pool and must close it (CloseDB, or Registry.Close when the handle was registered).

func SchemaLockName

func SchemaLockName(database string) string

SchemaLockName is the shared cross-owner lock contract. The caller must pass the exact UTF-8 database name returned by SELECT DATABASE(), never a DSN alias, supplied schema label, owner name, or lowercased approximation.

func SplitStatements

func SplitStatements(text string) []string

SplitStatements splits a migration file on lines that are exactly "-- @@". Empty pieces and pieces that contain only comments are dropped.

func Verify

func Verify(ctx context.Context, db *gorm.DB, resource string, fsys fs.FS, dir string) error

Verify is the read-only counterpart for a service that does not own the schema: it fails with ErrBehind when the ledger lacks a file the binary ships (or the ledger table does not exist yet) and with ErrChecksumMismatch when a registered file differs. It never writes and takes no lock.

func WithSchemaLock

func WithSchemaLock(ctx context.Context, db *sql.DB, apply func(*sql.Conn, string) error) (result error)

WithSchemaLock uses one dedicated physical connection from acquisition through schema verification and owner-ledger acknowledgement. A lost connection cannot transparently switch the migration to an unlocked session. All MySQL owners in this database must use this exact lock contract. This function does not merge ledgers or make MySQL DDL transactional.

Types

type Dialect

type Dialect string

Dialect names a relational engine. The zero value is not valid; use Postgres or MySQL.

const (
	// Postgres is the default engine (the PostgreSQL + TimescaleDB distribution).
	Postgres Dialect = "postgres"
	// MySQL is the configurable replacement (MySQL 8.4).
	MySQL Dialect = "mysql"
)

func DialectOf

func DialectOf(db *gorm.DB) (Dialect, error)

DialectOf reports the engine behind db. It reads the GORM dialector, so a repository switches on the handle it was given rather than on a global. It returns an error for a handle opened with any other dialector.

func MustDialectOf

func MustDialectOf(db *gorm.DB) Dialect

MustDialectOf is DialectOf for constructors that cannot proceed on an unknown engine; it panics, which is a programming error (the handle did not come from Open or Adopt).

func ParseDialect

func ParseDialect(raw string) (Dialect, error)

ParseDialect reads the value of relational.driver. Empty means Postgres. Anything else that is not a known name is an error, so a typo never silently selects a database. "postgresql", "pg" and "mariadb" are not accepted.

func ResolveDialect

func ResolveDialect(src Source) (Dialect, error)

ResolveDialect reads relational.driver. It is the only place a driver name is turned into a Dialect; every process calls it once at start-up (through ResolveTarget or directly) and passes the result down.

func (Dialect) String

func (d Dialect) String() string

func (Dialect) Valid

func (d Dialect) Valid() bool

Valid reports whether d is one of the two engines.

type Kind

type Kind int

Kind is the engine-independent class of a database error. Repositories branch on Kind, never on a vendor code.

const (
	// KindOther is every error not listed below; it is passed through unchanged.
	KindOther Kind = iota
	// KindUnique is a unique or primary key violation (PostgreSQL 23505, MySQL 1062).
	KindUnique
	// KindNotFound means no row matched (sql.ErrNoRows, gorm.ErrRecordNotFound).
	KindNotFound
	// KindTimeout is a cancelled or expired statement, or a lock wait that gave up: context deadline, PostgreSQL
	// 57014 and 55P03, MySQL 1205 and 3024, a network timeout.
	KindTimeout
	// KindDeadlock is a deadlock victim or a serialization failure; the whole transaction is safe to retry
	// (PostgreSQL 40P01 and 40001, MySQL 1213).
	KindDeadlock
	// KindForeignKey is a foreign key violation (PostgreSQL 23503, MySQL 1451 and 1452).
	KindForeignKey
)

func Classify

func Classify(err error) Kind

Classify maps err to a Kind. A nil error is KindOther.

func (Kind) Retryable

func (k Kind) Retryable() bool

Retryable reports whether re-running the whole transaction may succeed: a deadlock victim, or a lock wait timeout.

func (Kind) String

func (k Kind) String() string

type Migration

type Migration struct {
	Version    int64
	Name       string // file name
	SHA256     string // hex, of the file bytes
	Statements []string
}

Migration is one parsed file.

func LoadMigrations

func LoadMigrations(fsys fs.FS, dir string) ([]Migration, error)

LoadMigrations reads dir of fsys (typically an embed.FS), sorted by version. Any .sql file that does not match NNNN_name.up.sql is an error, so a misnamed file is never silently skipped; other extensions (PG-MAP.txt) are ignored.

type MigrationReceipt

type MigrationReceipt struct {
	Version int64
	Name    string
	SHA256  string
}

MigrationReceipt describes an already applied migration in an older ledger. It may be imported only when the currently shipped migration has the same version, name, and SHA-256. This is for explicit ledger-format transitions; it does not infer applied state from the schema.

type PoolOptions

type PoolOptions struct {
	MaxOpenConns    int
	MaxIdleConns    int
	ConnMaxLifetime time.Duration
	ConnMaxIdleTime time.Duration
	// LogLevel is GORM's logger level: 1 silent, 2 error, 3 warn, 4 info. 0 means error.
	LogLevel int
	// SlowThreshold logs statements slower than this at warn level. 0 disables.
	SlowThreshold time.Duration
}

PoolOptions are the pool settings of one logical resource. Zero fields take the defaults of DefaultPool.

func DefaultPool

func DefaultPool(o PoolOptions) PoolOptions

DefaultPool returns o with the defaults filled in.

type Registry

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

Registry holds the handles of one process: exactly one per logical resource. It is how "one pool per resource, and whoever created it closes it" is enforced: a second Register of the same name is an error, and Close closes only the pools the process opened itself (owned), never one it merely adopted.

func NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty registry.

func (*Registry) Close

func (r *Registry) Close() error

Close closes every owned pool and forgets all entries. It returns the joined errors.

func (*Registry) CloseOne

func (r *Registry) CloseOne(name string) error

CloseOne forgets name and closes its pool when the registry owns it. Closing a name that is not registered is not an error (a service may shut down twice).

func (*Registry) Get

func (r *Registry) Get(name string) (*gorm.DB, bool)

Get returns the handle registered under name.

func (*Registry) Names

func (r *Registry) Names() []string

Names lists the registered resources, sorted.

func (*Registry) Register

func (r *Registry) Register(name string, db *gorm.DB, owned bool) error

Register stores db under name. owned=true for a handle from Open; owned=false for one from Adopt.

type Source

type Source func(key string) string

Source returns the raw value of a configuration key, or "" when it is not set.

func EnvSource

func EnvSource() Source

EnvSource reads keys from environment variables: "relational.mysql.platform.dsn" is TP_RELATIONAL_MYSQL_PLATFORM_DSN.

func MapSource

func MapSource(m map[string]string) Source

MapSource reads keys from a map (tests, and callers that already hold the values).

type Target

type Target struct {
	// Name is the logical resource identity: "platform", "device_metadata", "automation", ... It keys the Registry,
	// the configuration keys and the migration history; it is never derived from the database name.
	Name    string
	Dialect Dialect

	// DSN, when set, is used as is (pgx URL or key/value string for Postgres; go-sql-driver DSN for MySQL). The
	// structured fields below are ignored.
	DSN string

	Host     string
	Port     int
	Database string
	Username string
	Password string
	// TimeZone is the session time zone ("Asia/Shanghai"). MySQL also uses it to read DATETIME values.
	TimeZone string

	Pool PoolOptions
}

Target is everything needed to open one logical resource (one connection pool).

func ResolveTarget

func ResolveTarget(src Source, resource string) (Target, error)

ResolveTarget builds the Target of one resource from src. The dialect comes from relational.driver; the DSN or the structured fields come from the keys of that dialect, the resource-specific key winning over the shared one.

func (Target) Validate

func (t Target) Validate() error

Validate checks the target without connecting.

Directories

Path Synopsis
Package reltest runs one scenario against PostgreSQL and MySQL in lockstep and compares every step.
Package reltest runs one scenario against PostgreSQL and MySQL in lockstep and compares every step.

Jump to

Keyboard shortcuts

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