Closes stacksjs/bun-query-builder#1137.
The argument was declared and discarded
executeMigration(dir?: string) declared the parameter, carried a comment explaining why it was retained, and never read it — it resolved the corpus from config.migrationDir on every call. So executeMigration('/somewhere/else') ran the configured directory and reported success, and a caller pointing it at the wrong place never found out.
One corpus, several directories
The reason it came up: with package auto-discovery, the corpus for a run is the application's database/migrations plus each installed package's. Calling once per directory does not work. The file name is the ordinal, and a package's tables carry foreign keys into the application's and never the reverse — so sorting each directory separately and running them back to back puts REFERENCES "users" before users exists. Postgres and MySQL reject that; SQLite does not, so the failure mode is green locally and red on deploy.
executeMigration() // config.migrationDir — unchanged
executeMigration(dir)
executeMigration([appDir, ...packageDirs])Files from every directory are sorted together by name, so a package migration can be placed before or after an application one deliberately. Relative paths resolve against the workspace root, the same root config.migrationDir resolves against.
Refused rather than absorbed
Three things a multi-directory run could do quietly, each of which ends in a migration that never ran on a run that exited zero:
- A file name repeated across directories. The ledger keys on the bare name, so the second file would look already-executed and be skipped. Refused, naming both paths.
- A directory that is not there. Would otherwise read as an empty corpus and report success.
- An empty list. Reading it as "the configured corpus" ignores what the caller asked for; reading it as "no corpus" skips everything. A caller that means the default omits the argument.
An omitted argument still creates database/migrations when missing, as before; named directories are never created.
Breaking
A caller that was passing something other than a migrations directory now gets that directory enumerated. The models directory was the common one — generateMigration(modelsDir) sits directly above it — and this repo's own tests were doing exactly that; they pass no argument now. Callers that pass nothing, including bin/cli.ts and seed, are unaffected.
Worth checking downstream before this lands: stacksjs/stacks passes modelsDir at storage/framework/core/database/src/migrations.ts:1597, which has always been wrong and has never mattered.
Also
The docs described executeMigration(migration: GenerateMigrationResult) — a signature it has not had. That would now be a type error rather than a silently ignored argument, so docs/features/migrations.md and docs/api/reference.md are corrected and document the multi-directory shape.
Tests
New test/migrate-multiple-dirs.test.ts, 10 cases: global ordering across directories (with a third file that fails outright unless the cross-directory order held), order independent of the order the directories are given in, bare names in the ledger, second-pass no-op, and each of the three refusals. Verified the ordering cases fail when the global sort is removed.
Full suite: 2141 pass, 0 fail. tsc --noEmit clean; pickier clean apart from one pre-existing no-console warning.
🤖 Generated with Claude Code