scanning directories for unit tests...
scanning present working directory for unit tests...
shuffled the files with seed 283193022 (replay with VERITAS_SEED=283193022)
found unit tests: (core.scm dialect.scm sql-runner.scm sql-parser.scm)
running unit tests from file: core.scm, entrypoint: spec
+----------------------------------------------+
[📑 suite] the engines and the names people call them
[🧪 test] the engines are listed once each
[⚙️ assert] in the order help lists them
✅ got: (sqlite3 postgresql cockroachdb mysql)
[🧪 test] every engine takes SQL migration files
[⚙️ assert] SQL for PostgreSQL
✅ got: (.sql)
[⚙️ assert] and for SQLite, which also knows its own suffix
✅ got: (.sql .sqlite.sql)
[🧪 test] a database reached through another driver keeps its own identity
[⚙️ assert] CockroachDB is opened by the PostgreSQL driver
✅ got: postgresql
[⚙️ assert] which is what guile-dbi is told
✅ got: postgresql
[⚙️ assert] and MariaDB by the MySQL one
✅ got: mysql
[🧪 test] an unknown name is refused, naming the ones that exist
[⚙️ assert] there is no such engine
✅ got: #f
[⚙️ assert] and the error says what there is instead
✅ got: #t
[🧪 test] an engine carries the port and the ledger it defaults to
[⚙️ assert] PostgreSQL's port
✅ got: 5432
[⚙️ assert] MySQL's port
✅ got: 3306
[⚙️ assert] CockroachDB's port
✅ got: 26257
[⚙️ assert] and SQLite has none
✅ got: 0
[⚙️ assert] the SQL ledger's default name
✅ got: migrations_history
[🧪 test] every alias resolves to the engine it names
[⚙️ assert] pg is PostgreSQL
✅ got: postgresql
[⚙️ assert] and so is psql
✅ got: postgresql
[⚙️ assert] a PostgreSQL-compatible database is reached as one
✅ got: postgresql
[⚙️ assert] mariadb is MySQL's engine
✅ got: mysql
[⚙️ assert] and so is percona
✅ got: mysql
[⚙️ assert] crdb is CockroachDB
✅ got: cockroachdb
[🧪 test] a name is matched without case
[⚙️ assert] MySQL and mysql are the same request
✅ got: mysql
[⚙️ assert] and so are POSTGRES and postgres
✅ got: postgresql
[🧪 test] every engine is a relational one
[⚙️ assert] SQLite is relational
✅ got: relational
[⚙️ assert] and so is PostgreSQL
✅ got: relational
[⚙️ assert] which is what the core asks
✅ got: #t
+----------------------------------------------+
[📑 suite] connection URLs
[🧪 test] something that is not a URL is a connection string
[⚙️ assert] the string is passed through
✅ got: alice:secret:appdb:tcp:host:5432
[⚙️ assert] with the engine it was told
✅ got: postgresql
[🧪 test] a PostgreSQL URL is split into its parts
[⚙️ assert] the engine
✅ got: postgresql
[⚙️ assert] the host
✅ got: db.example.net
[⚙️ assert] the port
✅ got: 5432
[⚙️ assert] the user
✅ got: alice
[⚙️ assert] and the database
✅ got: appdb
[🧪 test] a SQLite URL is a path, taken verbatim
[⚙️ assert] the engine
✅ got: sqlite3
[⚙️ assert] the absolute path is kept whole
✅ got: /var/lib/app.db
[⚙️ assert] and is the database
✅ got: /var/lib/app.db
[⚙️ assert] a relative path stays relative
✅ got: ./local.db
[🧪 test] a URL becomes the driver's own connection string
[⚙️ assert] PostgreSQL's colon-delimited form
✅ got: alice:secret:appdb:tcp:db.example.net:5432
[⚙️ assert] and MySQL's
✅ got: alice:secret:appdb:tcp:127.0.0.1:3306
[🧪 test] a port left out is the engine's own
[⚙️ assert] PostgreSQL defaults to 5432
✅ got: alice:secret:appdb:tcp:db.example.net:5432
[⚙️ assert] and MySQL to its own port on localhost
✅ got: alice:secret:appdb:tcp:localhost:3306
[🧪 test] a parameter is percent-decoded
[⚙️ assert] a space written as %20
✅ got: a b
[🧪 test] a password with a colon is refused rather than truncated
[⚙️ assert] the reason and the way out are both named
✅ got: #t
[🧪 test] the description of a connection never holds the password
[⚙️ assert] it says where it is going and as whom
✅ got: #t
[⚙️ assert] and not the password
✅ got: #f
[⚙️ assert] and it names the driver when that is the interesting part
✅ got: #t
+----------------------------------------------+
[📑 suite] a migration file and its order
[🧪 test] a name is read into a version and a description
[⚙️ assert] the file it is
✅ got: 2026-03-31_20:24:00_init.sql
[⚙️ assert] the version --target compares against
✅ got: 2026-03-31_20:24:00_init
[⚙️ assert] and that it is versioned
✅ got: versioned
[🧪 test] a dotted version sorts by number, not by text
[⚙️ assert] 1.2.10 comes after 1.2.9
✅ got: #t
[⚙️ assert] and the order key of the files agrees
✅ got: #t
[⚙️ assert] a version that is not numeric has no such key
✅ got: #f
[🧪 test] Flyway's names are read the way Flyway reads them
[⚙️ assert] the version is the number
✅ got: 1.2
[⚙️ assert] and the description what follows the underscores
✅ got: add-user
[⚙️ assert] a V file is versioned
✅ got: versioned
[⚙️ assert] an R file is repeatable
✅ got: repeatable
[⚙️ assert] and a U file is a rollback
✅ got: undo
[🧪 test] a `.repeatable.sql' name is a repeatable migration
[⚙️ assert] even without Flyway's `R__' prefix
✅ got: repeatable
[⚙️ assert] with the marker stripped from the version
✅ got: 2026-04-01_21:54:00_test
[🧪 test] the discovery of a directory is by extension, and sorted
[⚙️ assert] the SQL directory, in order
✅ got: (2026-03-31_20:24:00_init.sql 2026-04-01_21:54:00_test.repeatable.sql)
[🧪 test] a file is read with its byte order mark removed
[⚙️ assert] the BOM an editor wrote
✅ got: SELECT 1
[⚙️ assert] and a file that never had one
✅ got: SELECT 1
[🧪 test] a file's checksum is its content's
[⚙️ assert] a SHA-256 is 64 hexadecimal characters
✅ got: 64
[⚙️ assert] and it is the same on the next read
✅ got: 0de08501706302a7910d747dd602b9a974ce4b2fe2580170a70917231a5064e6
[⚙️ assert] while another file has another
✅ got: #f
[🧪 test] a directory that is not there is refused
[⚙️ assert] the directory is refused
✅ got: #t
[🧪 test] which files are migrations is the engine's answer
[⚙️ assert] SQL for PostgreSQL
✅ got: #t
[⚙️ assert] and not JSON
✅ got: #f
[⚙️ assert] nor does an engine take a stray note
✅ got: #f
+----------------------------------------------+
[📑 suite] placeholders
[🧪 test] a `key=value' list becomes an alist
[⚙️ assert] spaces around the parts are not part of them
✅ got: ((user . ada) (schema . app))
[⚙️ assert] a comma-separated string is the same thing
✅ got: ((user . ada))
[⚙️ assert] an empty one defines nothing
✅ got: ()
[🧪 test] a migration's `${name}' becomes what it was given
[⚙️ assert] both placeholders substituted
✅ got: SELECT * FROM app.users WHERE id = 7
[🧪 test] a placeholder nobody defined is left exactly as it was
[⚙️ assert] an empty set of placeholders changes nothing
✅ got: SELECT '${not-ours}'
[⚙️ assert] and neither does an incomplete one
✅ got: SELECT ${other}
[🧪 test] the question of whether there is a placeholder at all
[⚙️ assert] a `${' and a `}' together are one
✅ got: #t
[⚙️ assert] a positional parameter is not
✅ got: #f
[🧪 test] a spec with no `=' is refused
[⚙️ assert] and the form it wants is named
✅ got: #t
+----------------------------------------------+
[📑 suite] the ledger entry the core reads
[🧪 test] a time is read from either column name
[⚙️ assert] the server's own stamp
✅ got: 1791981743651
[⚙️ assert] and the older spelling
✅ got: 5
[🧪 test] a success is read whether it is a boolean or 1 and 0
[⚙️ assert] 1 is a success
✅ got: #t
[⚙️ assert] 0 is a failure
✅ got: #f
[⚙️ assert] and an entry that does not mention it succeeded
✅ got: #t
[⚙️ assert] an explicit #f is a failure
✅ got: #f
[🧪 test] a row is recognised as an entry
[⚙️ assert] an alist with a name is one
✅ got: #t
[⚙️ assert] a string is not
✅ got: #f
[⚙️ assert] and neither is an alist without a name
✅ got: #f
[🧪 test] a kind is read whether it is a symbol, a string or missing
[⚙️ assert] the symbol the runner writes
✅ got: versioned
[⚙️ assert] the string a driver answers with
✅ got: repeatable
[⚙️ assert] and a name that says so when the field is gone
✅ got: repeatable
[🧪 test] an entry becomes a migration file for reporting
[⚙️ assert] with its version
✅ got: 2026-04-01_21:54:00_test
[⚙️ assert] and its kind
✅ got: versioned
[⚙️ assert] and known to be applied
✅ got: #t
[⚙️ assert] an entry with no time took none
✅ got: 0
[🧪 test] the newest applied version is the newest versioned one
[⚙️ assert] the newest versioned migration
✅ got: 2026-03-31_20:24:00_init
[⚙️ assert] and an empty ledger has none
✅ got: #f
+----------------------------------------------+
[📑 suite] comparing a directory with a ledger
[🧪 test] a file edited after it was applied is a change
[⚙️ assert] the state is changed
✅ got: changed
[⚙️ assert] which is a warning by default
✅ got: warn
[⚙️ assert] and a problem
✅ got: (((name . 2026-03-31_20:24:00_init.sql) (path . migrations/lucidplan/2026-03-31_20:24:00_init.sql) (version . 2026-03-31_20:24:00_init) (description . 2026-03-31_20:24:00_init) (kind . versioned) (order-key . 2026-03-31_20:24:00_init) (checksum . 0de08501706302a7910d747dd602b9a974ce4b2fe2580170a70917231a5064e6) (state . changed) (action . warn) (applied-on . 1791981743651) (execution-time . 12) (success . #t)))
[⚙️ assert] which validate turns into a refusal
✅ got: fail
[⚙️ assert] the same question asked on its own
✅ got: (((name . 2026-03-31_20:24:00_init.sql) (path . migrations/lucidplan/2026-03-31_20:24:00_init.sql) (version . 2026-03-31_20:24:00_init) (description . 2026-03-31_20:24:00_init) (kind . versioned) (order-key . 2026-03-31_20:24:00_init)))
[🧪 test] an empty ledger makes every file pending
[⚙️ assert] both files are in the plan
✅ got: (((name . 2026-03-31_20:24:00_init.sql) (path . migrations/lucidplan/2026-03-31_20:24:00_init.sql) (version . 2026-03-31_20:24:00_init) (description . 2026-03-31_20:24:00_init) (kind . versioned) (order-key . 2026-03-31_20:24:00_init) (checksum . 0de08501706302a7910d747dd602b9a974ce4b2fe2580170a70917231a5064e6) (state . pending) (action . apply) (applied-on . #f) (execution-time . #f) (success . #t)) ((name . 2026-04-01_21:54:00_test.repeatable.sql) (path . migrations/lucidplan/2026-04-01_21:54:00_test.repeatable.sql) (version . 2026-04-01_21:54:00_test) (description . 2026-04-01_21:54:00_test) (kind . repeatable) (order-key . 2026-04-01_21:54:00_test) (checksum . b937e3b288e0595080667978a5f5e0bf09943508f25a5e90af7483e5a5e4a6b3) (state . repeatable-pending) (action . apply) (applied-on . #f) (execution-time . #f) (success . #t)))
[⚙️ assert] and both are pending
✅ got: (pending repeatable-pending)
[⚙️ assert] so both would run
✅ got: (((name . 2026-03-31_20:24:00_init.sql) (path . migrations/lucidplan/2026-03-31_20:24:00_init.sql) (version . 2026-03-31_20:24:00_init) (description . 2026-03-31_20:24:00_init) (kind . versioned) (order-key . 2026-03-31_20:24:00_init) (checksum . 0de08501706302a7910d747dd602b9a974ce4b2fe2580170a70917231a5064e6) (state . pending) (action . apply) (applied-on . #f) (execution-time . #f) (success . #t)) ((name . 2026-04-01_21:54:00_test.repeatable.sql) (path . migrations/lucidplan/2026-04-01_21:54:00_test.repeatable.sql) (version . 2026-04-01_21:54:00_test) (description . 2026-04-01_21:54:00_test) (kind . repeatable) (order-key . 2026-04-01_21:54:00_test) (checksum . b937e3b288e0595080667978a5f5e0bf09943508f25a5e90af7483e5a5e4a6b3) (state . repeatable-pending) (action . apply) (applied-on . #f) (execution-time . #f) (success . #t)))
[⚙️ assert] and the summary says so
✅ got: ((pending . 1) (repeatable-pending . 1))
[🧪 test] a ledger entry whose file has gone is missing
[⚙️ assert] the file and the entry that has none
✅ got: (((name . 2026-03-31_20:24:00_init.sql) (path . migrations/lucidplan/2026-03-31_20:24:00_init.sql) (version . 2026-03-31_20:24:00_init) (description . 2026-03-31_20:24:00_init) (kind . versioned) (order-key . 2026-03-31_20:24:00_init) (checksum . 0de08501706302a7910d747dd602b9a974ce4b2fe2580170a70917231a5064e6) (state . applied) (action . skip) (applied-on . 1791981743651) (execution-time . 12) (success . #t)) ((name . 2026-01-01_00:00:00_gone.sql) (path . #f) (version . 2026-01-01_00:00:00_gone) (description . 2026-01-01_00:00:00_gone.sql) (kind . versioned) (order-key . 2026-01-01_00:00:00_gone) (checksum . dead) (applied? . #t) (state . missing) (action . warn) (applied-on . 1791981743651) (execution-time . 12) (success . #t)))
[⚙️ assert] the entry is missing its file
✅ got: missing
[⚙️ assert] and is reported rather than run
✅ got: warn
[⚙️ assert] the orphans are found on their own too
✅ got: (((name . 2026-01-01_00:00:00_gone.sql) (checksum . dead) (description . 2026-01-01_00:00:00_gone.sql) (kind . versioned) (installed-on . 1791981743651) (installed-by . mutastructura) (execution-time . 12) (success . 1)))
[🧪 test] a file the ledger knows about is skipped
[⚙️ assert] it is applied
✅ got: applied
[⚙️ assert] and nothing is done
✅ got: skip
[⚙️ assert] so there is nothing to run
✅ got: ()
[⚙️ assert] and nothing to complain about
✅ got: ()
[🧪 test] a target holds back what is above it
[⚙️ assert] the migration at the target runs
✅ got: pending
[⚙️ assert] and the one after it does not
✅ got: beyond-target
[⚙️ assert] so one of the two would run
✅ got: (((name . 2026-03-31_20:24:00_init.sql) (path . migrations/lucidplan/2026-03-31_20:24:00_init.sql) (version . 2026-03-31_20:24:00_init) (description . 2026-03-31_20:24:00_init) (kind . versioned) (order-key . 2026-03-31_20:24:00_init) (checksum . 0de08501706302a7910d747dd602b9a974ce4b2fe2580170a70917231a5064e6) (state . pending) (action . apply) (applied-on . #f) (execution-time . #f) (success . #t)))
[⚙️ assert] and holding one back is not a problem
✅ got: ()
[🧪 test] repair names the recorded failures and the changed files
[⚙️ assert] nothing to repair when the ledger agrees
✅ got: ()
[⚙️ assert] a changed file is a candidate
✅ got: (((name . 2026-03-31_20:24:00_init.sql) (path . migrations/lucidplan/2026-03-31_20:24:00_init.sql) (version . 2026-03-31_20:24:00_init) (description . 2026-03-31_20:24:00_init) (kind . versioned) (order-key . 2026-03-31_20:24:00_init) (checksum . 0de08501706302a7910d747dd602b9a974ce4b2fe2580170a70917231a5064e6) (state . changed) (action . warn) (applied-on . 1791981743651) (execution-time . 12) (success . #t)))
[⚙️ assert] which repair realigns
✅ got: changed
[⚙️ assert] and so is a recorded failure
✅ got: (((name . 2026-03-31_20:24:00_init.sql) (path . migrations/lucidplan/2026-03-31_20:24:00_init.sql) (version . 2026-03-31_20:24:00_init) (description . 2026-03-31_20:24:00_init) (kind . versioned) (order-key . 2026-03-31_20:24:00_init) (checksum . 0de08501706302a7910d747dd602b9a974ce4b2fe2580170a70917231a5064e6) (state . pending) (action . apply) (applied-on . #f) (execution-time . 0) (success . #f)))
[⚙️ assert] which repair forgets
✅ got: #f
[🧪 test] a pending migration older than the newest one is held back
[⚙️ assert] it is superseded by what is applied
✅ got: superseded
[⚙️ assert] unless out-of-order runs are allowed
✅ got: apply
+----------------------------------------------+
[📑 suite] rolling back what can be rolled back
[🧪 test] repeatable migrations are not rollback candidates
[⚙️ assert] a repeatable is left for migrate to re-run
✅ got: ()
[⚙️ assert] and an empty database has nothing to undo
✅ got: ()
[🧪 test] --to leaves the target applied and undoes what is above it
[⚙️ assert] newest first, target excluded
✅ got: (2026-01-03_c.sql 2026-01-02_b.sql)
[⚙️ assert] a target that is already the newest undoes nothing
✅ got: ()
[⚙️ assert] and a target that was never applied is refused
✅ got: #t
[🧪 test] --all with nothing reversible is a boundary alone
✅ got: (((action . boundary) (name . 2026-01-01_a.sql) (version . 2026-01-01_a) (file (name . 2026-01-01_a.sql) (path . /m/2026-01-01_a.sql) (version . 2026-01-01_a) (description . 2026-01-01_a) (kind . versioned) (order-key . 2026-01-01_a)) (undo . #f) (irreversible . #f) (success . #t) (applied-on . 1791981743651) (reason . no undo script)))
[⚙️ assert] the newest migration is the wall
✅ got: boundary
[🧪 test] --all stops at the first migration it cannot undo
[⚙️ assert] newest first, then the boundary
✅ got: (undo undo boundary)
[⚙️ assert] which is the migration without a script
✅ got: 2026-01-01_a.sql
[🧪 test] a file marked irreversible is blocked, until it is not
[⚙️ assert] the directive stops the default
✅ got: blocked
[⚙️ assert] and says why
✅ got: marked irreversible
[⚙️ assert] while the explicit override runs it
✅ got: undo
[🧪 test] --all is not combined with --steps or --to
[⚙️ assert] the alternatives are named
✅ got: #t
[🧪 test] a recorded failure blocks its own rollback
✅ got: blocked
✅ got: records a failure; run repair first
[🧪 test] one migration is undone by default, and it is the newest
[⚙️ assert] the safe default is one step
✅ got: (((name . 2026-01-03_c.sql) (version . 2026-01-03_c) (file (name . 2026-01-03_c.sql) (path . /m/2026-01-03_c.sql) (version . 2026-01-03_c) (description . 2026-01-03_c) (kind . versioned) (order-key . 2026-01-03_c)) (undo (name . 2026-01-03_c.undo.sql) (path . /m/2026-01-03_c.undo.sql) (version . 2026-01-03_c) (description . 2026-01-03_c) (kind . undo) (order-key . 2026-01-03_c)) (irreversible . #f) (success . #t) (applied-on . 1791981743651) (action . undo) (reason . undo script)))
[⚙️ assert] the newest applied migration
✅ got: 2026-01-03_c.sql
[⚙️ assert] with the undo script beside it
✅ got: undo
[⚙️ assert] which is the file the runner would apply
✅ got: 2026-01-03_c.undo.sql
[🧪 test] more steps than there are migrations is refused
[⚙️ assert] rather than quietly undoing fewer
✅ got: #t
[🧪 test] a requested number of steps reports what blocks it
[⚙️ assert] the two that can, and the one that cannot
✅ got: (undo undo blocked)
[⚙️ assert] and why the last one is blocked
✅ got: no undo script
ℹ️ info - ran 7 tasks in: 1 seconds, -844914 microseconds
running unit tests from file: dialect.scm, entrypoint: spec
+----------------------------------------------+
[📑 suite] quoting, per engine
[🧪 test] a literal is written as its SQL type
[⚙️ assert] a number as itself
✅ got: 36
[⚙️ assert] true as 1
✅ got: 1
[⚙️ assert] false as 0
✅ got: 0
[⚙️ assert] the null value as NULL
✅ got: NULL
[⚙️ assert] a symbol as a quoted string
✅ got: 'versioned'
[🧪 test] an identifier that holds a quote is still one identifier
[⚙️ assert] SQLite doubles the inner double quote
✅ got: "we""ird"
[⚙️ assert] MySQL doubles the inner backtick
✅ got: `we``ird`
[🧪 test] MySQL doubles a backslash and SQLite does not
[⚙️ assert] SQLite leaves a backslash alone
✅ got: 'C:\temp'
[⚙️ assert] MySQL doubles it
✅ got: 'C:\\temp'
[⚙️ assert] and MariaDB doubles it too
✅ got: 'C:\\temp'
[🧪 test] a string is quoted with doubled single quotes
[⚙️ assert] an ordinary value
✅ got: 'users'
[⚙️ assert] an apostrophe does not end the literal
✅ got: 'user''s'
[🧪 test] an identifier is quoted the way the engine quotes them
[⚙️ assert] SQLite doubles the double quote
✅ got: "users"
[⚙️ assert] PostgreSQL does the same
✅ got: "users"
[⚙️ assert] MySQL uses backticks
✅ got: `users`
[⚙️ assert] and so does MariaDB, through the same alias
✅ got: `users`
+----------------------------------------------+
[📑 suite] transactions and locking
[🧪 test] every other engine opens a plain transaction
[⚙️ assert] PostgreSQL
✅ got: BEGIN
[⚙️ assert] MySQL
✅ got: BEGIN
[🧪 test] only MySQL cannot undo a failed migration's DDL
[⚙️ assert] MySQL commits DDL as it runs
✅ got: #f
[⚙️ assert] SQLite rolls it back
✅ got: #t
[⚙️ assert] and so does PostgreSQL
✅ got: #t
[🧪 test] MySQL asks for a lock and gives up after ten seconds
[⚙️ assert] the lock is named and timed
✅ got: #t
[🧪 test] a lock is released by the engine that took it
[⚙️ assert] PostgreSQL unlocks its own advisory lock
✅ got: #t
[⚙️ assert] MySQL releases the named lock
✅ got: #t
[🧪 test] PostgreSQL takes one advisory lock, named for the ledger
[⚙️ assert] one statement
✅ got: (SELECT pg_advisory_lock(7789568688506408848))
[⚙️ assert] and it is the advisory lock
✅ got: #t
[⚙️ assert] the same ledger gets the same key
✅ got: (SELECT pg_advisory_lock(5756235829675053970))
[⚙️ assert] and another ledger a different one
✅ got: #f
[🧪 test] SQLite takes its write lock with the transaction
[⚙️ assert] the write lock comes with BEGIN
✅ got: BEGIN IMMEDIATE
[⚙️ assert] and no lock statement is needed
✅ got: ()
[🧪 test] CockroachDB takes no advisory lock at all
[⚙️ assert] no statement to take a lock that is not there
✅ got: ()
[⚙️ assert] and none to release it
✅ got: ()
[⚙️ assert] and it speaks PostgreSQL's SQL otherwise
✅ got: BEGIN
[⚙️ assert] with transactional DDL
✅ got: #t
+----------------------------------------------+
[📑 suite] the ledger's own SQL
[🧪 test] the catalogue is asked, not the table
[⚙️ assert] SQLite asks sqlite_master
✅ got: #t
[⚙️ assert] PostgreSQL asks information_schema
✅ got: #t
[⚙️ assert] MySQL asks information_schema too
✅ got: #t
[🧪 test] each engine gets a ledger it can create
[⚙️ assert] SQLite's own autoincrement
✅ got: #t
[⚙️ assert] PostgreSQL's serial and timestamps
✅ got: #t
[⚙️ assert] MySQL's unique key and engine
✅ got: #t
[🧪 test] creating the ledger twice is not an error
[⚙️ assert] MySQL
✅ got: #t
[⚙️ assert] PostgreSQL
✅ got: #t
[🧪 test] the ledger has the same columns on every engine
[⚙️ assert] what the runner reads and writes
✅ got: (name checksum description kind installed-on installed-by execution-time success)
[⚙️ assert] MySQL agrees
✅ got: (name checksum description kind installed-on installed-by execution-time success)
[⚙️ assert] and PostgreSQL
✅ got: (name checksum description kind installed-on installed-by execution-time success)
[🧪 test] the columns are read from the catalogue as well
[⚙️ assert] SQLite gets the pragma
✅ got: #t
[⚙️ assert] PostgreSQL gets the column names
✅ got: #t
+----------------------------------------------+
[📑 suite] reading a ledger column
[🧪 test] the timestamp column is read as text, where the driver needs it
[⚙️ assert] PostgreSQL, whose driver cannot unpack a TIMESTAMPTZ
✅ got: "installed_on"::text AS "installed_on"
[⚙️ assert] MySQL, whose driver answers with a number instead
✅ got: DATE_FORMAT(`installed_on`, '%Y-%m-%d %H:%i:%s') AS `installed_on`
[⚙️ assert] and MariaDB through the same alias
✅ got: DATE_FORMAT(`installed_on`, '%Y-%m-%d %H:%i:%s') AS `installed_on`
[⚙️ assert] SQLite, which keeps its timestamps as text already
✅ got: "installed_on"
[⚙️ assert] and CockroachDB, which is reached as PostgreSQL
✅ got: "installed_on"::text AS "installed_on"
[⚙️ assert] including the name the first ledger called it
✅ got: "applied_at"::text AS "applied_at"
[🧪 test] every other column is read as it is named
[⚙️ assert] a ledger column comes back quoted
✅ got: "name"
[⚙️ assert] with the engine's own quoting
✅ got: `success`
+----------------------------------------------+
[📑 suite] dropping what clean found
[🧪 test] an object type is normalised to one of four
[⚙️ assert] a base table is a table
✅ got: table
[⚙️ assert] and so is a system table
✅ got: table
[⚙️ assert] a view is a view
✅ got: view
[⚙️ assert] a trigger is a trigger
✅ got: trigger
[⚙️ assert] an index is an index
✅ got: index
[🧪 test] a view, a trigger and an index are dropped as what they are
[⚙️ assert] a view is dropped as a view
✅ got: DROP VIEW IF EXISTS "active_users" CASCADE
[⚙️ assert] a trigger as a trigger, whatever the case
✅ got: DROP TRIGGER IF EXISTS "touch_users"
[⚙️ assert] an index as an index
✅ got: DROP INDEX IF EXISTS `email_1`
[🧪 test] each engine lists what it can drop
[⚙️ assert] SQLite, without its own internal tables
✅ got: #t
[⚙️ assert] PostgreSQL, tables and views
✅ got: #t
[⚙️ assert] MySQL, through information_schema
✅ got: #t
[🧪 test] only PostgreSQL drops with CASCADE
[⚙️ assert] PostgreSQL cascades
✅ got: DROP TABLE IF EXISTS "users" CASCADE
[⚙️ assert] SQLite has nothing to cascade
✅ got: DROP TABLE IF EXISTS "users"
[⚙️ assert] and neither has MySQL
✅ got: DROP TABLE IF EXISTS `users`
ℹ️ info - ran 5 tasks in: 0 seconds, 94603 microseconds
running unit tests from file: sql-runner.scm, entrypoint: spec
+----------------------------------------------+
[📑 suite] the ledger, on a real database
[🧪 test] opening a session creates the ledger
[⚙️ assert] opening a session creates the ledger
✅ got: #t
[⚙️ assert] and the catalogue agrees it is there
✅ got: #t
[⚙️ assert] and it holds nothing yet
✅ got: ()
[⚙️ assert] with the columns the ledger is made of
✅ got: (id name checksum description kind installed-on installed-by execution-time success)
[🧪 test] forgetting a migration removes its row
[⚙️ assert] forgetting a migration removes its row
✅ got: ()
[⚙️ assert] and looking it up answers nothing
✅ got: #f
[🧪 test] a session that may not write writes nothing
[⚙️ assert] a session that may not create the ledger does not
✅ got: #f
[⚙️ assert] and a database with no ledger has no history to report
✅ got: ()
[⚙️ assert] and nothing was written to the database at all
✅ got: ()
[🧪 test] recording the same migration again replaces its row
[⚙️ assert] recording the same migration again leaves one row
✅ got: (((id . 1) (name . 2026-01-01_10:00:00_init.sql) (checksum . d0f1) (description . init) (kind . versioned) (installed-on . 2026-11-10 19:48:09) (installed-by . mutastructura) (execution-time . 99) (success . 1)))
[⚙️ assert] with what the second recording said
✅ got: 99
[🧪 test] a closed session can be collected without an error
[⚙️ assert] closing a session twice, and collecting it, is not an error
✅ got: #t
[🧪 test] a recorded migration reads back in full
[⚙️ assert] one migration is one row
✅ got: (((id . 1) (name . 2026-01-01_10:00:00_init.sql) (checksum . d0f1) (description . init) (kind . versioned) (installed-on . 2026-11-10 19:48:09) (installed-by . mutastructura) (execution-time . 12) (success . 1)))
[⚙️ assert] with the name it was recorded under
✅ got: 2026-01-01_10:00:00_init.sql
[⚙️ assert] and its checksum
✅ got: d0f1
[⚙️ assert] and its kind, which only this ledger version has
✅ got: versioned
[⚙️ assert] and how long it took
✅ got: 12
[⚙️ assert] and a success the core can read
✅ got: #t
+----------------------------------------------+
[📑 suite] reading rows
[🧪 test] every row of a query comes back
[⚙️ assert] every row of a query comes back
✅ got: (((a . 1) (b . x)) ((a . 2) (b . y)))
[⚙️ assert] with the values the database has
✅ got: 1
[🧪 test] a refused query raises
[⚙️ assert] a refused query raises the database's own message
✅ got: #t
[🧪 test] an empty answer is not a failure
[⚙️ assert] a query with no rows answers with no rows, and no error
✅ got: ()
+----------------------------------------------+
[📑 suite] running a migration
[🧪 test] a file becomes its statements, placeholders and all
[⚙️ assert] a file is read as its statements
✅ got: (((index . 0) (line . 2) (sql . -- ${other} is somebody else's
CREATE TABLE app_users (id INTEGER PRIMARY KEY))) ((index . 1) (line . 3) (sql . CREATE INDEX users_idx ON app_users (id))))
[⚙️ assert] with the placeholder substituted in the first
✅ got: #t
[⚙️ assert] and in the second
✅ got: #t
[⚙️ assert] and a placeholder nobody defined is left alone
✅ got: #t
[🧪 test] a dry run reports and changes nothing
[⚙️ assert] a dry run reports the statements it would run
✅ got: 2
[⚙️ assert] and runs none of them
✅ got: #f
[🧪 test] applying then cleaning leaves an empty database
[⚙️ assert] applying a migration creates its table
✅ got: #t
[⚙️ assert] a dry run names what clean would drop
✅ got: 3
[⚙️ assert] and drops nothing
✅ got: #t
[⚙️ assert] clean drops what the catalogue reports
✅ got: 3
[⚙️ assert] and the database, ledger included, is empty
✅ got: ()
+----------------------------------------------+
[📑 suite] the vtable
[🧪 test] the SQL runner and the engines it answers for
[⚙️ assert] the SQL runner is the relational family
✅ got: relational
[⚙️ assert] and it says what it speaks
✅ got: #t
[⚙️ assert] sqlite3 is spoken to by it
✅ got: relational
[⚙️ assert] and so is an engine reached through MySQL's driver
✅ got: relational
ℹ️ info - ran 4 tasks in: 0 seconds, 529458 microseconds
running unit tests from file: sql-parser.scm, entrypoint: spec
+----------------------------------------------+
[📑 suite] a semicolon is not always the end
[🧪 test] nothing between two semicolons is not a statement
[⚙️ assert] the empty one is dropped
✅ got: (SELECT 1 SELECT 2)
[🧪 test] a file of whitespace and comments holds no statements
[⚙️ assert] nothing to run
✅ got: ()
[⚙️ assert] and an empty file has nothing either
✅ got: ()
[🧪 test] the line a statement starts on is reported
[⚙️ assert] the line of the first real character
✅ got: (1 3)
[🧪 test] a semicolon inside a string is part of the string
[⚙️ assert] the insert is not cut in half
✅ got: (INSERT INTO t VALUES ('a;b') SELECT 1)
[⚙️ assert] and it still holds its semicolon
✅ got: #t
[🧪 test] a statement that is never terminated is still a statement
[⚙️ assert] the last statement is read
✅ got: (SELECT 1 SELECT 2)
[🧪 test] a doubled quote does not end the string
[⚙️ assert] the escaped quote is part of the value
✅ got: (SELECT 'it''s; fine')
[🧪 test] two statements separated by semicolons are two
[⚙️ assert] both are read
✅ got: (((index . 0) (line . 1) (sql . SELECT 1)) ((index . 1) (line . 1) (sql . SELECT 2)))
[⚙️ assert] and the delimiter is not part of either
✅ got: (SELECT 1 SELECT 2)
[⚙️ assert] each carries its position
✅ got: (0 1)
[🧪 test] a semicolon inside a quoted identifier is part of the identifier
[⚙️ assert] the identifier is not cut in half
✅ got: (CREATE TABLE "we;ird" (x int))
+----------------------------------------------+
[📑 suite] comments
[🧪 test] a comment can be stripped for the question of what is in a statement
[⚙️ assert] the SQL is kept
✅ got: #t
[⚙️ assert] and every comment is gone
✅ got: #f
[⚙️ assert] a block comment goes too
✅ got: #t
[🧪 test] what a statement means is decided without its comments
[⚙️ assert] SQL is meaningful
✅ got: #t
[⚙️ assert] a comment is not
✅ got: #f
[⚙️ assert] and neither is whitespace
✅ got: #f
[⚙️ assert] the two questions are each other's opposite
✅ got: #t
[🧪 test] a `--' comment runs to the end of the line
[⚙️ assert] the semicolon in it is not one
✅ got: (-- a; comment
SELECT 1)
[🧪 test] a trailing comment neither splits nor extends a statement
[⚙️ assert] the comment is not a second statement
✅ got: (SELECT 1)
[🧪 test] `#' is a comment for MySQL and an operator for PostgreSQL
[⚙️ assert] MySQL reads it as a comment
✅ got: (# a; comment
SELECT 1)
[⚙️ assert] PostgreSQL does not, so the `;' splits
✅ got: (# a comment
SELECT 1)
[🧪 test] a block comment nests where the engine nests them
[⚙️ assert] PostgreSQL reads one comment and one statement
✅ got: (/* outer /* inner; */ still; */ SELECT 1)
[⚙️ assert] with the select after it
✅ got: #t
[⚙️ assert] and the others split at the inner semicolon
✅ got: (/* outer /* inner; */ still */ SELECT 1)
+----------------------------------------------+
[📑 suite] what only one dialect does
[🧪 test] an `E' that is the tail of a word is not one
[⚙️ assert] a word ending in W is not an escape string
✅ got: (SELECT W'it\'s fine';)
[🧪 test] PostgreSQL's `E' strings are escape strings
[⚙️ assert] the `E' makes the backslash an escape
✅ got: (SELECT E'it\'s; fine')
[🧪 test] a backtick identifier is MySQL's alone
[⚙️ assert] MySQL reads the backtick as an identifier
✅ got: (CREATE TABLE `we;ird` (x int))
[⚙️ assert] and PostgreSQL does not
✅ got: (CREATE TABLE `we ird` (x int))
[🧪 test] a backslash escapes inside a MySQL string
[⚙️ assert] MySQL's escaped quote keeps the string open
✅ got: (INSERT INTO t VALUES ('a\';b'))
[⚙️ assert] PostgreSQL's string ends at the quote
✅ got: (INSERT INTO t VALUES ('a\' b');)
+----------------------------------------------+
[📑 suite] a body a semicolon must not end
[🧪 test] a statement that is not a CREATE is left exactly as it is
[⚙️ assert] three statements, not one
✅ got: (BEGIN SELECT 1 COMMIT)
[🧪 test] an `END IF' inside the body does not close it
[⚙️ assert] the routine and the select
✅ got: (CREATE PROCEDURE p()
BEGIN
IF x THEN
SELECT 1;
END IF;
SELECT 2;
END SELECT 3)
[⚙️ assert] the body after the END IF is still in it
✅ got: #t
[🧪 test] SQLite's trigger body is one statement
[⚙️ assert] the trigger and the select
✅ got: (CREATE TRIGGER touch AFTER INSERT ON users
BEGIN
UPDATE users SET n = 1;
DELETE FROM tmp;
END SELECT 1)
[⚙️ assert] the whole body is in the first
✅ got: #t
[⚙️ assert] including its END
✅ got: #t
+----------------------------------------------+
[📑 suite] dollar quoting
[🧪 test] a positional parameter is not a tag
[⚙️ assert] the statement still ends at its semicolon
✅ got: (SELECT $1, $2 SELECT 3)
[🧪 test] a function body written with `$$' is one statement
[⚙️ assert] the function and the select
✅ got: (CREATE FUNCTION f() RETURNS int AS $$
BEGIN
RETURN 1;
END;
$$ LANGUAGE plpgsql SELECT 1)
[⚙️ assert] the body is in the first statement
✅ got: #t
[🧪 test] a tag makes the quoting explicit, and can appear in the body
[⚙️ assert] the function and the select
✅ got: (CREATE FUNCTION f() RETURNS int AS $body$
SELECT 1; -- $$ is just text in here
$body$ LANGUAGE sql SELECT 2)
[⚙️ assert] and the `$$' inside is data
✅ got: #t
+----------------------------------------------+
[📑 suite] the mysql client's own directive
[🧪 test] a DELIMITER line is recognised, and only when it is one
[⚙️ assert] the delimiter is reported
✅ got: (delimiter . $$)
[⚙️ assert] in any case
✅ got: (delimiter . //)
[⚙️ assert] a word that only starts with it is not it
✅ got: #f
[⚙️ assert] nor is one with nothing to set
✅ got: #f;;; note: source file ./src/mutastructura/mutastructura.scm
;;; newer than compiled /gnu/store/21afi1i81zcsj3g9fjxxaxrkfxm3gfjp-profile/lib/guile/3.0/site-ccache/mutastructura/mutastructura.go
;;; found fresh local cache at /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/mutastructura/trunk/src/mutastructura/mutastructura.scm.go
;;; note: source file ./src/mutastructura/persistence/migration-runner.scm
;;; newer than compiled /gnu/store/21afi1i81zcsj3g9fjxxaxrkfxm3gfjp-profile/lib/guile/3.0/site-ccache/mutastructura/persistence/migration-runner.go
;;; found fresh local cache at /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/mutastructura/trunk/src/mutastructura/persistence/migration-runner.scm.go
;;; note: source file ./src/mutastructura/persistence/util.scm
;;; newer than compiled /gnu/store/21afi1i81zcsj3g9fjxxaxrkfxm3gfjp-profile/lib/guile/3.0/site-ccache/mutastructura/persistence/util.go
;;; found fresh local cache at /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/mutastructura/trunk/src/mutastructura/persistence/util.scm.go
;;; note: source file ./src/mutastructura/persistence/sql-parser.scm
;;; newer than compiled /gnu/store/21afi1i81zcsj3g9fjxxaxrkfxm3gfjp-profile/lib/guile/3.0/site-ccache/mutastructura/persistence/sql-parser.go
;;; found fresh local cache at /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/mutastructura/trunk/src/mutastructura/persistence/sql-parser.scm.go
[⚙️ assert] nor one asking for two things
✅ got: #f
[⚙️ assert] nor a commented-out one
✅ got: #f
[🧪 test] the directive retargets the splitter
[⚙️ assert] the trigger and the select
✅ got: (CREATE TRIGGER t AFTER INSERT ON u
BEGIN
SELECT 1;
END SELECT 2)
[⚙️ assert] the body survived the new delimiter
✅ got: #t
[⚙️ assert] and the directive is not sent anywhere
✅ got: #f
[⚙️ assert] nor is the one that restored it
✅ got: #f
+----------------------------------------------+
[📑 suite] the entry points agree with each other
[🧪 test] an alias resolves to the dialect it names
[⚙️ assert] MariaDB lexes as MySQL
✅ got: (# a; comment
SELECT 1)
[🧪 test] the string-in/list-of-strings-out entry point
[⚙️ assert] the simple entry point splits the same way
✅ got: (SELECT 1 SELECT 2)
[⚙️ assert] and knows the engine it was given
✅ got: (# a; comment
SELECT 1)
[⚙️ assert] with the `#' line read as a comment
✅ got: #t
[🧪 test] the byte order mark some editors write is not part of the SQL
[⚙️ assert] the BOM is dropped
✅ got: SELECT 1
[🧪 test] the first statement of a file, and the line it is on
[⚙️ assert] the first statement is the first
✅ got: SELECT 1
[⚙️ assert] and it knows which line it starts on
✅ got: 3
ℹ️ info - ran 7 tasks in: 0 seconds, 87891 microseconds
finished running tests
--------------------------------------------------------------
Maak 0.14.0: the infinitely extensible command runner
Loading tasks from Maak file: /etc/byggsteg/job-clone/mutastructura/trunk/maak.scm
Executing Maak tasks: test
--------------------------------------------------------------
=> Running Maak task: test
Run the unit test suite with Veritas.
Execute command:
env VERITAS_TEST_TYPE=unit VERITAS_RUN_BENCHMARKS=false guix shell -m manifest.scm -- guile -L ./src -L ./test -L . -c '((@ (veritas runner) run-tests))'
Executing: env VERITAS_TEST_TYPE=unit VERITAS_RUN_BENCHMARKS=false guix shell -m manifest.scm -- guile -L ./src -L ./test -L . -c '((@ (veritas runner) run-tests))'