Filename grammar ok plain name parses ok date ok seq defaults to 1 ok slug ok suffixed name parses ok seq reads the suffix ok rejected (no date): foo.sql ok rejected (short date): 2026821_x.sql ok rejected (not .sql): 20260821_x.SQL.bak ok rejected (one-digit sequence): 20260821-2_x.sql ok rejected (uppercase slug): 20260821_Mixed-Case.sql ok rejected (empty slug): 20260821_.sql ok rejected (no slug): 20260821.sql ok rejected (February 31st): 20260231_x.sql ok rejected (month 13): 20261301_x.sql ok rejected (second underscore): 20260821_x_y.sql Ordering ok a naive sort really does get this wrong (why migration_sort exists) ok migration_sort puts the unsuffixed file first ok full ordering is date then sequence ok sequence 10 sorts after sequence 2 ok an unparseable name is reported, not sorted ok and is left out of the order ok two files claiming one slot are reported Splitting ok two statements split ok trailing statement with no semicolon still counts ok a comment-only file yields no statements ok apostrophes in comments do not open a string literal ok a semicolon inside a string does not split ok '' escape inside a string is handled ok backslash escape inside a string is handled ok block comments are stripped, semicolons inside them ignored ok removing a block comment keeps the tokens apart ok CR ends a line comment ok a semicolon inside a backtick identifier does not split ok # comments are stripped ok -- without whitespace is not a comment ok an unterminated string is an error, not a truncated statement ok an unterminated block comment is an error ok a trigger is refused rather than split on its inner semicolons ok a MySQL version hint is refused rather than discarded ok a MariaDB executable comment is refused rather than discarded ok DELIMITER is refused Checksums ok rewriting the comments does not change the checksum ok reindenting does not change the checksum ok changing a column type DOES change the checksum ok adding a statement changes the checksum ok an unsplittable file has no checksum ok whitespace inside seed data changes the checksum ok whitespace inside a quoted identifier changes the checksum ok escaped quotes do not hide significant literal whitespace Naming the next one ok first of the day is unsuffixed ok second of the day gets -02 ok third gets -03, counting from the highest present ok a gap does not repeat a used sequence ok a 100th migration in one day is refused (-100 would not parse) Config validation ok a minimal config passes ok two targets, constants and a null schema pass ok refused: not an array ok refused: unknown key ok refused: bad timezone ok refused: empty bootstrap ok refused: absolute bootstrap ok refused: escaping bootstrap ok refused: no targets ok refused: uppercase target ok refused: target without conn ok refused: target without schema key ok refused: two targets, one dir ok refused: lowercase constant migrations/config.php ok config is valid ok bootstrap file exists: scripts/migrate-bootstrap.inc.php migrations/ as it stands (target app) ok migrations/ exists ok schema file exists: docs/schema.sql ok every filename parses ok no two claim one slot ok all 5 migrations split cleanly (10 statements) run order: 1. 20261009_schema-migration.sql 2. 20261014_ticket-join-code-expiry.sql 3. 20261014-02_public-link-tokens.sql 4. 20261014-03_call-declined.sql 5. 20261015_device-token-sandbox.sql 76 passed, 0 failed