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
- Variables
- func Adopt(d Dialect, pool *sql.DB, p PoolOptions) (*gorm.DB, error)
- func CloseDB(db *gorm.DB) error
- func EnvName(key string) string
- func ImportMigrationHistory(ctx context.Context, db *gorm.DB, resource string, files []Migration, ...) error
- func Is(err error, k Kind) bool
- func IsUnique(err error, constraint string) bool
- func LockTx(ctx context.Context, tx *gorm.DB, keys ...string) error
- func Migrate(ctx context.Context, db *gorm.DB, resource string, fsys fs.FS, dir string) error
- func Open(ctx context.Context, t Target) (*gorm.DB, error)
- func SchemaLockName(database string) string
- func SplitStatements(text string) []string
- func Verify(ctx context.Context, db *gorm.DB, resource string, fsys fs.FS, dir string) error
- func WithSchemaLock(ctx context.Context, db *sql.DB, apply func(*sql.Conn, string) error) (result error)
- type Dialect
- type Kind
- type Migration
- type MigrationReceipt
- type PoolOptions
- type Registry
- type Source
- type Target
Constants ¶
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.
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).
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", ...).
const LedgerTable = "tp_schema_migrations"
LedgerTable is the per-database ledger of MySQL migrations, keyed by (resource, version).
Variables ¶
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") )
var ( ErrBusy = errors.New("schema migration executor already holds the database lock") ErrOutcomeUnknown = errors.New("migration outcome is unknown; reconcile the existing owner ledger") )
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 ¶
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 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 IsUnique ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
SplitStatements splits a migration file on lines that are exactly "-- @@". Empty pieces and pieces that contain only comments are dropped.
func Verify ¶
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.
func DialectOf ¶
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 ¶
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 ¶
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 ¶
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.
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 )
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 ¶
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 ¶
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 (*Registry) Close ¶
Close closes every owned pool and forgets all entries. It returns the joined errors.
func (*Registry) CloseOne ¶
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).
type Source ¶
Source returns the raw value of a configuration key, or "" when it is not set.
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 ¶
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.