Documentation

Migrations
in package

Database migrations and initialization utilities.

Provides methods for updating database schema, running migrations, and initializing the database.

Tags
since
3.0.0

Table of Contents

Constants

MAX_ATTEMPTS  : mixed = 3
How many times a failed migration is retried before being given up on.
STATUS_APPLIED  : mixed = 'applied'
Status recorded for a migration whose statements all succeeded.
STATUS_FAILED  : mixed = 'failed'
Status recorded for a migration that had at least one failing statement.
HARMLESS_SQL_ERRORS  : array<string|int, int> = [ 1007, // ER_DB_CREATE_EXISTS 1022, // ER_DUP_...
MySQL error codes that mean "nothing to do here", not "this broke".

Methods

alignReferenceColumnTypes()  : void
Make every reference column use the same integer type as the key it points at.
calculateChecksum()  : string
Calculate SHA-256 checksum for a migration file.
captureForeignKeys()  : array<string|int, array{name: string, table: string, columns: string[], refTable: string, refColumns: string[], onUpdate: string, onDelete: string}>
Read back every foreign key in the database, in enough detail to rebuild it.
checkAndUpdate()  : void
Check and/or update the database.
dropAllForeignKeys()  : void
Drop all foreign key constraints from all tables in the database.
getAppliedMigrations()  : array<string|int, string>
Get list of migrations that ran without any failing statement.
getFailedMigrations()  : array<string|int, array{filename: string, attempts: int, error: string}>
Get the migrations that failed, with the error that made them fail.
getMigrationFiles()  : array<string|int, string>
Get list of all migration files from the migrations directory.
getMissingForeignKeys()  : array<string|int, array{name: string, table: string}>
The declared constraints that are still not there.
getRecordedMigrations()  : array<string|int, string>
Get list of migrations that have an entry in `_migrations`, whatever their outcome.
getRetryableMigrations()  : array<string|int, string>
Get migrations that failed and are still worth retrying.
prefixQuery()  : string
Add a prefix to table in a SQL query string.
reconcileForeignKeys()  : int
Add back the declared constraints an install has lost along the way.
recordMigration()  : void
Record the outcome of a migration run.
reparseAllTexts()  : void
Reparse all texts in order.
restoreForeignKeys()  : int
Recreate captured foreign keys that are no longer there.
update()  : void
Update the database if it is using an outdate version.
upgradeMigrationsTable()  : void
Upgrade the _migrations table from old schema to new schema.
validateMigrationIntegrity()  : array{valid: bool, errors: string[]}
Validate that applied migrations haven't been modified.
addMissingForeignKeys()  : int
Create each of the given foreign keys that is not already there.
columnFamilyChanges()  : array<string, array<string|int, string>>
Work out which columns of one family are out of step with their key.
columnsStillExist()  : bool
Check that a table still has all of the given columns.
fetchMigrationNames()  : array<string|int, string>
Run a query returning a `filename` column and collect the values.
isHarmlessFailure()  : bool
Tell an expected statement failure apart from a real one.
runMigrationFile()  : string|null
Run every statement of one migration file.
sanitizeReferentialAction()  : string
Keep a referential action to the values MySQL accepts.

Constants

MAX_ATTEMPTS

How many times a failed migration is retried before being given up on.

public mixed MAX_ATTEMPTS = 3

Retries only happen when new migrations appear (see update()), so this counts upgrades, not requests.

STATUS_APPLIED

Status recorded for a migration whose statements all succeeded.

public mixed STATUS_APPLIED = 'applied'

STATUS_FAILED

Status recorded for a migration that had at least one failing statement.

public mixed STATUS_FAILED = 'failed'

HARMLESS_SQL_ERRORS

MySQL error codes that mean "nothing to do here", not "this broke".

private array<string|int, int> HARMLESS_SQL_ERRORS = [ 1007, // ER_DB_CREATE_EXISTS 1022, // ER_DUP_KEY 1050, // ER_TABLE_EXISTS_ERROR 1060, // ER_DUP_FIELDNAME: baseline already has the column 1061, // ER_DUP_KEYNAME 1091, // ER_CANT_DROP_FIELD_OR_KEY: already dropped 1146, // ER_NO_SUCH_TABLE: legacy table a fresh install never had 1826, ]

Old migrations rename or alter tables that a fresh install never had, because db/schema/baseline.sql already creates the modern schema. Those statements fail by design, so they must not be reported as failures — otherwise every healthy install would show migration errors.

Methods

alignReferenceColumnTypes()

Make every reference column use the same integer type as the key it points at.

public static alignReferenceColumnTypes() : void

LWT names foreign key columns after the primary key they reference: texts.TxLgID points at languages.LgID, word_occurrences.Ti2TxID at texts.TxID, and so on. Over the years several of those primary keys were widened (languages.LgID went from tinyint(3) to int(11) in 20251221_120000_add_inter_table_foreign_keys.sql) without every referencing column following along.

A mismatched pair makes InnoDB reject the foreign key with errno 150, "Foreign key constraint is incorrectly formed" — even under FOREIGN_KEY_CHECKS = 0. When the constraint sits inside a CREATE TABLE, the whole table is never created, which is how installs ended up without books or local_dictionaries and crashed with "Table 'books' doesn't exist" (issue #247).

Each family is aligned on its widest member, so a column is only ever widened, never narrowed: no value can be truncated. Families that already agree are left alone, making this a no-op on healthy installs.

Callers must have dropped foreign keys first — ALTER TABLE MODIFY is refused on a column an FK points at.

calculateChecksum()

Calculate SHA-256 checksum for a migration file.

public static calculateChecksum(string $filepath) : string
Parameters
$filepath : string

Full path to the migration file

Return values
string

SHA-256 hash or empty string if file not readable

captureForeignKeys()

Read back every foreign key in the database, in enough detail to rebuild it.

public static captureForeignKeys() : array<string|int, array{name: string, table: string, columns: string[], refTable: string, refColumns: string[], onUpdate: string, onDelete: string}>

dropAllForeignKeys() clears the way for the migration run, but only the migrations that happen to be pending put constraints back. Everything created by an already-applied migration would be gone for good, so an upgrade used to strip the database of its referential integrity (cascade deletes, orphan protection) without a word. Capturing the set first is what lets restoreForeignKeys() put back what the run did not.

Return values
array<string|int, array{name: string, table: string, columns: string[], refTable: string, refColumns: string[], onUpdate: string, onDelete: string}>

The database's foreign keys

checkAndUpdate()

Check and/or update the database.

public static checkAndUpdate() : void

dropAllForeignKeys()

Drop all foreign key constraints from all tables in the database.

public static dropAllForeignKeys() : void

This is needed before running migrations from scratch because SET FOREIGN_KEY_CHECKS = 0 only affects INSERT/UPDATE/DELETE and DROP TABLE, not ALTER TABLE MODIFY on columns referenced by FKs.

Callers that drop keys to make way for schema changes must capture them first (see captureForeignKeys()) and restore them afterwards.

getAppliedMigrations()

Get list of migrations that ran without any failing statement.

public static getAppliedMigrations() : array<string|int, string>
Return values
array<string|int, string>

List of successfully applied migration filenames

getFailedMigrations()

Get the migrations that failed, with the error that made them fail.

public static getFailedMigrations() : array<string|int, array{filename: string, attempts: int, error: string}>

Meant for administrative reporting: a non-empty result means the schema is incomplete and some features will fail at runtime.

Return values
array<string|int, array{filename: string, attempts: int, error: string}>

Failed migrations

getMigrationFiles()

Get list of all migration files from the migrations directory.

public static getMigrationFiles() : array<string|int, string>
Return values
array<string|int, string>

Sorted list of migration filenames

getMissingForeignKeys()

The declared constraints that are still not there.

public static getMissingForeignKeys() : array<string|int, array{name: string, table: string}>

A non-empty result means writes that should be refused are being accepted, so it is worth showing to an administrator.

Return values
array<string|int, array{name: string, table: string}>

Missing constraints

getRecordedMigrations()

Get list of migrations that have an entry in `_migrations`, whatever their outcome.

public static getRecordedMigrations() : array<string|int, string>

These are the migrations that have already been attempted at least once, so they are not "new" any more.

Return values
array<string|int, string>

List of recorded migration filenames

getRetryableMigrations()

Get migrations that failed and are still worth retrying.

public static getRetryableMigrations() : array<string|int, string>

A migration is retried until MAX_ATTEMPTS is reached; past that it stays on record as failed so an administrator can investigate.

Return values
array<string|int, string>

List of failed migration filenames

prefixQuery()

Add a prefix to table in a SQL query string.

public static prefixQuery(string $sql_line, string $prefix) : string
Parameters
$sql_line : string

SQL string to prefix.

$prefix : string

Prefix to add

Return values
string

Prefixed SQL query

reconcileForeignKeys()

Add back the declared constraints an install has lost along the way.

public static reconcileForeignKeys() : int

Restoring a snapshot only preserves what a database still has. Releases before 3.4.0 dropped the constraints for every migration run and put back only the ones owned by pending migrations, so long-lived installs are missing constraints no snapshot can recover — half of them on a 3.3.0 database (#273). SchemaConstraints::FOREIGN_KEYS says what should be there; whatever is absent is added.

Rows that a missing constraint would have prevented are already in the database. Adding under FOREIGN_KEY_CHECKS = 0 accepts them and gates writes from here on; a constraint InnoDB still refuses is logged and reported by getMissingForeignKeys() rather than failing the upgrade.

Return values
int

How many were added

recordMigration()

Record the outcome of a migration run.

public static recordMigration(string $filename[, string $checksum = '' ][, string $status = self::STATUS_APPLIED ][, string|null $error = null ]) : void

Re-recording the same migration keeps a single row: the status and error are overwritten and the attempt counter is incremented, so a migration that failed once and succeeds on a later upgrade ends up as applied.

Parameters
$filename : string

The migration filename

$checksum : string = ''

SHA-256 hash of the migration file

$status : string = self::STATUS_APPLIED

STATUS_APPLIED or STATUS_FAILED

$error : string|null = null

Error message when the migration failed

reparseAllTexts()

Reparse all texts in order.

public static reparseAllTexts() : void

restoreForeignKeys()

Recreate captured foreign keys that are no longer there.

public static restoreForeignKeys(array<string|int, array{name: string, table: string, columns: string[], refTable: string, refColumns: string[], onUpdate: string, onDelete: string}> $keys) : int

Anything the migration run already put back, under the same name, is left alone. A key whose table or column has since been renamed or dropped is skipped: the migration that renamed it owns the new shape.

Parameters
$keys : array<string|int, array{name: string, table: string, columns: string[], refTable: string, refColumns: string[], onUpdate: string, onDelete: string}>

Foreign keys from captureForeignKeys()

Return values
int

How many were restored

update()

Update the database if it is using an outdate version.

public static update() : void

upgradeMigrationsTable()

Upgrade the _migrations table from old schema to new schema.

public static upgradeMigrationsTable() : void

Old schema stored migrations to be run; new schema tracks applied migrations. This method adds the applied_at and checksum columns.

validateMigrationIntegrity()

Validate that applied migrations haven't been modified.

public static validateMigrationIntegrity() : array{valid: bool, errors: string[]}

Checks the checksum of each applied migration against its stored value. This detects tampering or accidental modification of migration files.

Return values
array{valid: bool, errors: string[]}

Validation result

addMissingForeignKeys()

Create each of the given foreign keys that is not already there.

private static addMissingForeignKeys(array<string|int, array{name: string, table: string, columns: string[], refTable: string, refColumns: string[], onUpdate: string, onDelete: string}> $keys) : int

A key whose table or column is missing is skipped: on an install that never had the table, or where a migration renamed it, the constraint is not this method's business.

Parameters
$keys : array<string|int, array{name: string, table: string, columns: string[], refTable: string, refColumns: string[], onUpdate: string, onDelete: string}>

Constraints to ensure

Return values
int

How many were created

columnFamilyChanges()

Work out which columns of one family are out of step with their key.

private static columnFamilyChanges(string $suffix) : array<string, array<string|int, string>>
Parameters
$suffix : string

Column name suffix identifying the family (e.g. 'LgID')

Return values
array<string, array<string|int, string>>

MODIFY clauses, keyed by table

columnsStillExist()

Check that a table still has all of the given columns.

private static columnsStillExist(string $table, array<string|int, string> $columns) : bool
Parameters
$table : string

Table name

$columns : array<string|int, string>

Column names

Return values
bool

True when every column is present

fetchMigrationNames()

Run a query returning a `filename` column and collect the values.

private static fetchMigrationNames(string $sql) : array<string|int, string>
Parameters
$sql : string

Query selecting the filename column

Return values
array<string|int, string>

List of migration filenames

isHarmlessFailure()

Tell an expected statement failure apart from a real one.

private static isHarmlessFailure(RuntimeException $e) : bool
Parameters
$e : RuntimeException

Exception raised while running a statement

Return values
bool

True when the failure can be safely ignored

runMigrationFile()

Run every statement of one migration file.

private static runMigrationFile(string $filepath, string $filename) : string|null

Statements are independent: one failing statement does not stop the others, because a migration often mixes work that is still needed with work a fresh install already has.

Parameters
$filepath : string

Full path to the migration file

$filename : string

Migration filename, for logging

Return values
string|null

The first real error, or null when nothing broke

sanitizeReferentialAction()

Keep a referential action to the values MySQL accepts.

private static sanitizeReferentialAction(string $action) : string

The value comes from INFORMATION_SCHEMA rather than user input, but it is interpolated into DDL, so it is checked against the allowed set anyway.

Parameters
$action : string

Action read back from the database

Return values
string

A safe ON DELETE/ON UPDATE action

On this page

Search results