Documentation
¶
Overview ¶
Package check holds the checks spirit's runners make before and while they change data: configuration, privileges, and refusals of tables and statements spirit does not support.
Every check declares two things: the phases of a run it belongs to (Phase, when it runs) and the runtimes it applies to (Runtime, which runner runs it). RunChecks runs the checks that match both, so a check that only applies to one runner is never run by another, and a check that does apply fails rather than passes when the runner does not supply what it needs.
Index ¶
- Constants
- Variables
- func Checks(runtime Runtime, phase Phase) []string
- func CutoverOldName(tableName string) string
- func ReverseWindowSchemaObjectsError(ctx context.Context, sources []Endpoint) error
- func RevertRetiredName(tableName string) string
- func RunChecks(ctx context.Context, r Resources, logger *slog.Logger, phase Phase, ...) error
- func SourceSchemaObjectsError(ctx context.Context, sources []Endpoint) error
- func StatementRefusal(ctx context.Context, stmt, currentCreateTable string, logger *slog.Logger) (reason string, refused bool, err error)
- func TargetSchemaDiff(table, sourceCreate, targetCreate string) (string, error)
- func TargetSchemaObjectsError(ctx context.Context, r Resources) error
- func UnsupportedNameError(r Resources) error
- type Change
- type Credentials
- type Endpoint
- type Options
- type Phase
- type Resources
- type Runtime
Constants ¶
const ConfigurationCheckName = "configuration"
ConfigurationCheckName is the configuration check's name, for a runner that excludes it (see RunChecks).
const MinForeignKeyVersion = "9.7"
MinForeignKeyVersion is the oldest MySQL version a migration supports a table with foreign keys on, under --enable-experimental-foreign-keys.
MySQL 9.6 moved foreign key checks and cascades out of InnoDB and into the SQL layer (WL#11249). Before that, a change a cascade made to a child table (ON DELETE CASCADE, ON UPDATE CASCADE, SET NULL) was made inside InnoDB and never written to the binary log, so the copy of a child table missed it. From 9.6 the cascaded changes are logged as row events of the child table, like any other change, unless the server was started with innodb_native_foreign_keys. 9.7 is the first LTS release with the change.
const TargetStateCheckName = "target_state"
TargetStateCheckName is the registered name of the target-state check — the one post-setup check that validates target-side state (tables absent, or empty with a matching schema) and therefore the only one that wiping the target can cure. The runner's --force path excludes it (via RunChecksExcluding) when deciding whether a wipe would actually help.
Variables ¶
var ErrRefused = errors.New("refused by a check")
ErrRefused matches (with errors.Is) a check failure that is a decision rather than an error: the check ran and found a reason to refuse, so a retry would be refused again. A check error that does not match it, such as a failed query, may be transient. A cutover retries on a transient error from its checks under lock, and fails without a retry on a refusal.
var ErrReplicaNotHealthy = fmt.Errorf("replica is not healthy")
ErrReplicaNotHealthy is returned when a replica's IO or SQL thread is not running.
Functions ¶
func Checks ¶ added in v0.19.0
Checks reports the names of the checks registered for runtime and phase, in the order RunChecks runs them.
Phase membership is a curated set, not an accident of registration: each phase states what a check in it must tolerate and what its failure entitles the caller to conclude. A caller relying on that contract can pin the set it was written against, so a check added to the phase later has to be judged against the contract rather than inherit a verdict made about its predecessors.
func CutoverOldName ¶ added in v0.19.0
CutoverOldName returns the name the move cutover renames a source table to (`<table>_old`). Unlike the migration package's utils.OldTableName, no truncation is applied — renameSafetyCheck instead rejects tables whose _old name would exceed MySQL's identifier limit before any rows are copied.
func ReverseWindowSchemaObjectsError ¶ added in v0.19.0
ReverseWindowSchemaObjectsError returns a refusal (see ErrRefused) listing every trigger, on any table, and every event in any source schema, grouped by source, or nil.
During a reverse window the source's retired `<table>_old` tables are kept current by the reverse feed, and a reverse cutover puts them back into service. A trigger or an event in the source schema can write to them without passing through the reverse feed, so the rollback could make live data that differs from the target; a trigger on an _old table would also go live with it. The runner calls this when it enters the window (fresh and on resume) and in the reverse cutover, before any rename. It does not refuse views, procedures or functions: they do not change what the rollback makes live, and must not block an emergency rollback. The forward pre-cutover check (SourceSchemaObjectsError) has checked the whole schema just before a fresh window starts.
Like SourceSchemaObjectsError, it first checks that the user can see the schema's triggers and events, and refuses if not.
func RevertRetiredName ¶ added in v0.19.0
RevertRetiredName returns the name a reverse-window rollback renames a former target table to (`<table>_revert`). It is deliberately distinct from CutoverOldName's `_old` (which retires the *source* on a forward cutover): only a revert ever produces `_revert`, so a later move can unambiguously drop a leftover `_revert` table without risking a forward move's `_old` backup.
func RunChecks ¶
func RunChecks(ctx context.Context, r Resources, logger *slog.Logger, phase Phase, exclude ...string) error
RunChecks runs every check registered for both r.Runtime and phase, except those named in exclude. r.Runtime must be exactly one runtime: a check that branches on the runtime reads it as the runner it is running for. Every name in exclude must be a registered check. Checks run in name order so that a run failing more than one check always reports the same error.
logger may be nil: checks log as they run, and a caller classifying a statement without a logger to hand must get a verdict rather than a panic.
func SourceSchemaObjectsError ¶ added in v0.19.0
SourceSchemaObjectsError returns an error listing every trigger, view, stored procedure, stored function and event in any source schema, grouped by source, or nil if there are none.
Move creates the target from each moved table's CREATE TABLE and copies rows; it copies none of these objects. The cutover retires the source tables, so the objects would be left behind on the source. The whole schema is checked, not only the moved tables, even when only a subset of tables is moved: a trigger on a table that is not moved can write to a moved table, and routines and events cannot be mapped to tables.
information_schema only shows objects the connecting user has a privilege on (see schemaGrants). The privileges check requires those grants at preflight, and every call checks them again for each source before it trusts an empty result, and refuses if they are missing: a reverse-window resume runs no preflight, and a grant can be revoked during a long move.
Finding objects, or missing grants, is a refusal (see ErrRefused). Any other error, such as a failed query, may be transient.
The reverse window (entry and reverse cutover) uses the narrower ReverseWindowSchemaObjectsError.
func StatementRefusal ¶ added in v0.19.0
func StatementRefusal(ctx context.Context, stmt, currentCreateTable string, logger *slog.Logger) (reason string, refused bool, err error)
StatementRefusal reports whether Spirit deterministically refuses stmt, and the reason it would give.
It runs the PhaseStatement checks: the subset of Spirit's preflight checks that decide from the statement — plus, when currentCreateTable is supplied, the table's existing column definitions — and that MySQL's native DDL cannot complete behind their back. That makes it the entry point for a caller which needs to know an apply's outcome before starting it, such as a planning tool classifying DDL. Checks Spirit adds to the phase later are picked up without any change here.
currentCreateTable is the table's current definition, normally its SHOW CREATE TABLE. It must describe the table stmt alters. Pass an empty string when it is not available: coverage then narrows to the checks that need only the statement — the ENUM/SET checks, which compare a redeclared column against its current type, the missing-primary-key refusal, which reads the current key definition, and the primary key collation prediction, which reads the key columns' current charsets and collations, are skipped. The definition must also reflect the table's true key set: SHOW CREATE TABLE output collected with show_gipk_in_create_table_and_information_schema disabled omits a generated invisible primary key and misreports the table as unkeyed.
logger may be nil, in which case the checks' own logging is discarded.
refused is true only when Spirit will refuse the statement, so a caller may act on it. A statement that is not an ALTER TABLE is never refused: Spirit runs CREATE TABLE and DROP TABLE as native DDL rather than through the copy process. err reports input that cannot be classified — an unparseable statement, more than one statement, a currentCreateTable for a different table, or one that does not describe a column the statement redeclares — and is never itself a refusal.
func TargetSchemaDiff ¶ added in v0.19.0
TargetSchemaDiff compares a move's SOURCE table against a pre-created TARGET table and returns a runnable ALTER TABLE statement describing how they differ, or an empty string if the move will accept the target as it stands. It is the comparison the source→target checks use (target_state, resume_state).
Two divergences are tolerated, and only these two. Both are a target that is deliberately stricter or leaner than the unsharded source it moves from, so that a declaratively-managed target does not have to mirror artifacts of its source:
- the source's column-level AUTO_INCREMENT may be absent on the target, whose ids come from elsewhere (e.g. a Vitess sequence);
- a column the source declares nullable may be NOT NULL on the target — a shard key, typically, which cannot be NULL in a sharded keyspace.
Anything else is a difference, the reverse of either included: a target looser than its source fails, as does any change to types, charset, collation, indexes or constraints. See statement.DiffCreateTables for the canonicalization rules underneath.
It is exported because "which divergences does a move tolerate" must have exactly one definition. A caller that wants to know in advance whether a move will accept a given target — strata's `keyspace move-tables` previews it before the operator confirms anything — would otherwise reassemble the option set by hand, and drift in either direction is a bug: the caller blocks a move that would have succeeded, or promises one that fails here in pre-flight. Adding to or removing from the tolerated set is therefore a change in public behaviour, not an internal one.
The nullability tolerance exists because refusing it forced a choice. A Vitess primary vindex cannot map NULL to a keyspace id, so a sharded target must declare its shard key NOT NULL — while the source may still permit NULL because the ALTER to tighten it was never affordable on a multi-terabyte unsharded table. Without this, an operator had to pick between a correct target schema and being able to move into it at all.
The relaxation cannot mask a NULL that actually exists. On a sharded target the row never reaches an INSERT: the applier hashes the shard key first and a NULL fails there. For any other tightened column MySQL does coerce it — Spirit runs a non-strict sql_mode (conn.go sets NO_AUTO_VALUE_ON_ZERO alone) and its copy writes are INSERT IGNORE, so the would-be ER_BAD_NULL_ERROR becomes a warning and the implicit default is stored — but the write does not survive that. RetryableTransaction reads SHOW WARNINGS after every statement and fails on any warning it does not explicitly tolerate, which is what dbconn.UnsafeWarningError is for: on an IGNORE statement the warning is the only evidence the copy lost data. The row's batch takes the whole run down with it, and because the error is fatal it is not retried.
So the outcome is a failed move, never a silently altered value, and the failure is immediate rather than deferred to the checksum. It is still worth probing the tightened columns for NULLs before starting the copy: unchecked, the run dies whenever the copier reaches the chunk holding that row — hours, on a table large enough to want this relaxation — and reports a raw warning code against a batch rather than naming the column.
One note for maintainers: what is exported is this function and not the options it builds, deliberately. The nullability tolerance is directional — statement.IgnoreNotNullRelaxation lets the schema being *validated* be stricter than its *reference*, and here the target is the validated schema, so it must reach DiffCreateTables as "got" with the source as "want". Handing a caller the options would hand them that trap: passed the other way round they forgive the opposite, dangerous direction, silently and with no diff to show for it. The parameter names below are what prevents that, which is why the option and the argument order never leave this function.
func TargetSchemaObjectsError ¶ added in v0.19.0
TargetSchemaObjectsError returns a refusal (see ErrRefused) listing, for every target in r.Targets, each trigger on a table the run writes to and each event in the target schema, grouped by target, or nil.
The run writes to r.SourceTables on every target, and to r.CheckpointTable on targets[0]. A pre-created target table may carry a trigger (move's target_state accepts an empty table whose definition matches the source, and a sync resumes onto tables that exist), and a trigger fires once for every copied row and again for every replayed change, so its writes are applied twice or more. An event runs on its own schedule and can write to the tables the run writes to. A trigger on another table is not refused: it fires only when something else writes to that table.
Table names are compared the way the target compares them: case-insensitively when its lower_case_table_names is nonzero, and exactly when it is 0.
information_schema only shows triggers and events the user has the TRIGGER and EVENT privilege on, so every call first checks that the user has both on the target schema, and refuses if not (see targetObjectVisibility).
--force does not bypass the refusal: the runner runs this check before it wipes the target, and the wipe never drops events.
func UnsupportedNameError ¶ added in v0.19.0
UnsupportedNameError returns an error for the first schema or table name in a move or a sync that contains a '.' or a backtick: each source and target schema, then each source table and its schema.
The move runner also calls it directly when resuming a reverse window, which runs no check phase.
Types ¶
type Change ¶ added in v0.19.0
type Change struct {
Table *table.TableInfo
NewTable *table.TableInfo // the altered shadow table; set from PhasePostSetup onwards
Statement *statement.AbstractStatement
}
Change is the table and statement of one change in a migration. The migration runner runs its checks once per change.
type Credentials ¶ added in v0.19.0
type Credentials struct {
Host string
Username string
Password string
TLSMode string
TLSCertificatePath string
}
Credentials are the connection settings PhasePreRun checks connect with: the run has no connection of its own yet.
type Endpoint ¶ added in v0.19.0
type Endpoint struct {
DB *sql.DB
// Config is the connection's configuration. Config.DBName is the schema.
Config *mysql.Config
// Tables are the tables of this endpoint the run acts on.
Tables []*table.TableInfo
}
Endpoint is one MySQL server a run reads from or writes to, at one schema.
type Options ¶ added in v0.19.0
type Options struct {
Threads int
ReplicaMaxLag time.Duration
// ExperimentalForeignKeys is --enable-experimental-foreign-keys: a table
// with foreign keys of its own is supported on MySQL 9.7 and later.
ExperimentalForeignKeys bool
// MoveEverything is true when a move was given no explicit table list,
// so every table in each source database is moved. The
// source_schema_consistency check then also requires an identical table
// *set* across all sources: when moving everything, an extra or missing
// table on one shard is a drift error; when only a named subset is
// moved, tables outside that subset are ignored.
MoveEverything bool
}
Options are the settings of the run that checks validate or depend on.
type Phase ¶ added in v0.19.0
type Phase uint8
Phase is when in a run a check runs. A check registers for every phase it runs in.
const ( PhasePreRun Phase = 1 << 0 PhasePreflight Phase = 1 << 1 PhasePostSetup Phase = 1 << 2 PhaseCutover Phase = 1 << 3 // PhaseResume runs when a move resumes from a checkpoint, in place of // PhasePostSetup: an object can be created on a source or a target // between runs, so the checks of the setup are repeated. PhaseResume Phase = 1 << 4 // PhaseStatement marks checks a caller can run ahead of an apply to learn // that Spirit will refuse a statement. Most are preflight checks that also // run as part of a migration; a check registered only here predicts, from // the statement, a refusal a later phase makes for every statement it // refuses. Callers run them via // RunChecks with Resources.Change.Statement set and, optionally, // Resources.Change.Table — the table's current metadata, which widens coverage to // the checks that compare the statement against the existing column // definitions. Neither needs a database connection: Table can be built // from the table's DDL with statement.CreateTable.ToTableInfo. A check // tagged with this phase must tolerate every Resources field except // Change.Statement being unset. // // A failure here is a refusal the caller can report as certain, so the // phase only carries checks that no earlier stage can bypass on any // server. The migration runner runs this phase itself, once it has loaded // the tables and before it attempts MySQL's native DDL, so it refuses // every statement these checks refuse. Spirit attempts that native DDL — // ALGORITHM=INSTANT, then a safe-INPLACE subset — before it runs // preflight checks, and MySQL decides what that completes, which varies // with the server version and the table. A preflight check the native DDL // may complete (dropadd, rename) is deliberately excluded: claiming those // as refusals would report failure for an apply that succeeds, and the // runner, running them here, would then make it fail. // // That exclusion only ever under-reports, which is the safe direction: // passing these checks is not a promise Spirit will accept the statement, // only a failure is a claim. An excluded check still refuses at preflight // whenever the native attempt does not take the statement — Spirit skips // the attempt altogether for a multi-table change, and an older server // rejects shapes a newer one completes instantly. Checks that need a live // connection (existing foreign keys, triggers, privileges, ...) are not in // this phase either: they run at preflight, and the foreign key and // trigger checks run again before and during the cutover, to catch one // created while the migration runs. // // A caller that reports these refusals somewhere other than its own logs // judges the text by reading the checks that produce it, and a check added // to the phase is picked up without any change on the caller's side. Pin // the membership with Checks to keep that judgement attached to the // checks it was made about. PhaseStatement Phase = 1 << 6 // PhaseCutoverLocked runs while the cutover holds its table lock, after // the final flush and immediately before the RENAME. LOCK TABLES ... WRITE // keeps out any DDL that needs a metadata lock on the table, so a check // here sees the state the RENAME acts on. The runner stops acting on // schema-change notifications once the cutover starts, so this is the // only check of anything created while the cutover waits for its lock. // A refusal (an error that matches ErrRefused, see refuse) fails the // cutover without a retry. Any other error is retried like any other // cutover failure, so a check must return ErrRefused for a permanent // condition, or it takes the table lock again on every retry. Keep these // checks to fast reads: they run while the application's writes to the // table are blocked. // // A move runs this phase while its forward cutover holds the table locks // on every source, after the final flush and before the traffic switch // and the source rename. A refusal leaves the source live. PhaseCutoverLocked Phase = 1 << 7 )
type Resources ¶
type Resources struct {
// Runtime is the runner running the checks. RunChecks runs only the
// checks registered for it.
Runtime Runtime
// Sources are the servers the run reads from. A migration has exactly
// one: the server it alters the table on.
Sources []Endpoint
// Targets are the servers a move or a sync writes to.
Targets []applier.Target
// SourceTables are the first source's tables. They stand for every
// source where only their reported definition matters; Sources[i].Tables
// holds each source's own (Move and Sync).
SourceTables []*table.TableInfo
// CheckpointTable is the checkpoint table the run writes on Targets[0]
// (Move and Sync).
CheckpointTable string
// Replicas are the migration's replicas, for the replica checks.
Replicas []*sql.DB
// Change is the migration's table and statement (Migration only).
Change Change
Options Options
Credentials Credentials
// contains filtered or unexported fields
}
Resources is what a run gives its checks. Runtime is required; which of the other fields are set depends on the runtime and the phase.
Source Files
¶
- check.go
- configuration.go
- dropadd.go
- enumreorder.go
- enumsetremoval.go
- enumsetutil.go
- foreignkey.go
- illegalclause.go
- primarykey.go
- primarykeybit.go
- primarykeycollation.go
- primarykeycollation_statement.go
- primarykeyexists.go
- primarykeyfloat.go
- privileges.go
- refusal.go
- rename.go
- rename_safety.go
- replicahealth.go
- replicaprivileges.go
- resume_state.go
- schema_compare.go
- schema_object_visibility.go
- setreorder.go
- settings.go
- source_schema_consistency.go
- source_schema_objects.go
- statement_refusal.go
- table_compatibility.go
- tableidentifier.go
- tablename.go
- target_schema_objects.go
- target_state.go
- triggers.go
- unsynced_source_objects.go
- version.go