fix(db): ignore subdirectories and non-SQL files in migrations/ - #230
fix(db): ignore subdirectories and non-SQL files in migrations/#230agent-zhang-beihai[bot] wants to merge 1 commit into
Conversation
listLocalMigrationFilenames returned every entry in migrations/, so a subdirectory such as migrations/_archive/ (a common convention for superseded reference SQL) was validated as a migration filename. That made `db migrations new <name>` and `db migrations up --all` fail with "Invalid migration filename: _archive. Expected <migration_version>_<migration-name>.sql." Only return `.sql` entries that are not directories, so subdirectories and stray non-SQL files never reach parseStrictLocalMigrations. Filtering on !isDirectory() rather than isFile() keeps symlinked migration files visible instead of silently skipping them.
jwfing
left a comment
There was a problem hiding this comment.
Review: fix(db): ignore subdirectories and non-SQL files in migrations/
Summary: Tightly-scoped, well-tested bug fix that makes listLocalMigrationFilenames skip subdirectories and non-.sql entries so a migrations/_archive/ folder (or a stray README.md) no longer aborts db migrations new / up --all.
Requirements context
Consulted docs/specs/2026-04-17-db-migrations-command-design.md (the db-migrations command design; InsForge/CLI keeps specs under docs/specs/, not /docs/superpowers/). The spec defines the migration filename contract as ^(\d{14})_([a-z0-9-]+)\.sql$ — lowercase .sql only. The fix is consistent with that contract, and the PR body ties the change to feedback item 016dd35e. No behavioral requirement in the spec is violated.
Findings
Critical
(none)
Suggestion
(none)
Information
- Functionality — silent skip of mis-cased
.SQLfiles (src/lib/migrations.ts:126-129). The filterentry.name.endsWith('.sql')is case-sensitive, matching the lowercase-onlyMIGRATION_FILENAME_REGEX, so no valid migration can be dropped — good. One subtle behavior change worth noting: previously a file like20260418091500_x.SQLreachedparseStrictLocalMigrationsand threw a loudInvalid migration filenameerror; now it is silently ignored. This is the same "silent skip vs. loud error" tradeoff the PR body calls out for symlinks — and here it lands on the silent side. It's a genuinely marginal case (uppercase extensions were never valid migrations), so this is informational only, not a request to change. If you wanted symmetry with the symlink reasoning you could match.sqlcase-insensitively and let the strict parser reject the casing, but the current behavior is defensible.
Notes on the four review dimensions
- Software engineering — Strong. Four new tests in
src/lib/migrations.test.tscover the fix (_archive/+README.mdskipped), thedb migrations newintegration path (parseStrictLocalMigrations+getNextLocalMigrationVersion), the regression that misnamed*.sqlstill throws, and the missing-dir[]case. Follows existing test conventions (temp dirs viamkdtempSync,node:fs/node:pathimports). The!isDirectory()-over-isFile()choice for symlink safety is correct and well-commented. Verified locally:vitest run src/lib/migrations.test.ts→ 38 passed;eslinton both changed files → clean. - Functionality — Fix is complete.
listLocalMigrationFilenamesis the single source for all three callers (fetchdedup,newstrict-parse,up), so filtering at the source covers every affected path. No command surface, flag, or output shape changed. - Security — No security-relevant changes. Reads local directory entries only; no new user input reaches SQL/shell/HTTP, nothing new logged.
- Performance — No concern. Same single
readdirSyncwith an added filter/map on a normally small directory; no extra I/O, no N+1.
Verdict
approved (informational; a human still gives the explicit GitHub approval). Zero Critical findings — clean, verified, in-scope fix with solid regression coverage.
Greptile SummaryThis PR narrows local migration discovery to SQL entries so archive directories and unrelated files no longer break migration commands.
Confidence Score: 4/5The PR appears safe to merge, with a non-blocking edge case remaining for validly named SQL symlinks that target directories. Direct archive directories and non-SQL files are now excluded as intended, but the broad symbolic-link predicate can still pass a directory target into migration file reads. Files Needing Attention: src/lib/migrations.ts
|
| Filename | Overview |
|---|---|
| src/lib/migrations.ts | Filters migration directory entries before parsing, with a non-blocking edge case for symbolic links whose targets are directories. |
| src/lib/migrations.test.ts | Adds focused filesystem tests for directory and extension filtering while preserving strict SQL filename validation. |
Reviews (1): Last reviewed commit: "fix(db): ignore subdirectories and non-S..." | Re-trigger Greptile
| return readdirSync(migrationsDir, { withFileTypes: true }) | ||
| .filter((entry) => !entry.isDirectory() && entry.name.endsWith('.sql')) |
There was a problem hiding this comment.
Directory-targeting symlinks pass filtering
A validly named .sql symlink to a directory passes !entry.isDirectory() because the Dirent describes the link itself; db migrations up then follows the link in readFileSync, fails with EISDIR, and can interrupt a batch after earlier migrations were applied.
Knowledge Base Used: Config Management
What was broken
listLocalMigrationFilenamesinsrc/lib/migrations.tsreturned every entry ofmigrations/viareaddirSync, including subdirectories. Becausedb migrations newanddb migrations up --allpass that list straight toparseStrictLocalMigrations, a subdirectory was validated as if it were a migration file.With a
migrations/_archive/folder present (a common convention for holding superseded reference SQL), both commands failed:The only workaround was moving the archive folder out of
migrations/.What changed
listLocalMigrationFilenamesnow reads withwithFileTypes: trueand keeps only non-directory entries ending in.sqlbefore sorting, so subdirectories and stray non-SQL files (e.g.README.md) never reachparseStrictLocalMigrations.The filter uses
!entry.isDirectory()rather thanentry.isFile()deliberately:isFile()is false for symlinks, which would silently skip a symlinked migration file — a worse failure than the error being fixed. Misnamed*.sqlfiles are still rejected exactly as before.Scope is one function; no command surface, flags, or output shapes changed, so no
InsForge/agent-skillsupdate is needed.How it was verified
New
listLocalMigrationFilenamestests insrc/lib/migrations.test.tsbuild a tempmigrations/containing_archive/(with a.sqlfile inside), a valid20260418091500_create-users.sql, and aREADME.md, and assert:.sqlfile is listed;parseStrictLocalMigrationssucceeds andgetNextLocalMigrationVersionpicks the right next version (thedb migrations newpath);*.sqlfile still throwsInvalid migration filename;migrations/still returns[].Confirmed the two new assertions fail on
mainand pass with the fix. Full suite green:npm run lint→ 785 passed / 13 skipped, eslint clean.Addresses user feedback 016dd35e-d192-40f2-bc7b-da45899e24ce (cli): db migrations new/up validate subdirectories in migrations/ as migration filenames
Summary by cubic
Fixes migration discovery by ignoring subdirectories and non-SQL files in migrations/. Previously we returned every entry, so a folder like migrations/_archive/ was validated as a migration filename and made
db migrations newanddb migrations up --allfail. Now we return only non-directory.sqlentries; symlinked.sqlfiles are still included. Addresses Linear feedback 016dd35e.listLocalMigrationFilenames(usesreaddirSync(..., { withFileTypes: true })and!entry.isDirectory()+.sqlsuffix)..sqlfiles still error; subdirectories and files likeREADME.mdare ignored.src/lib/migrations.test.tscover subdirectories, non-SQL files, misnamed.sql, and missingmigrations/. No rollout or migration actions required.Written for commit 60ddbda. Summary will update on new commits.
Note
Fix
listLocalMigrationFilenamesto skip subdirectories and non-SQL files in migrations/Updates
listLocalMigrationFilenamesin migrations.ts to filter directory entries usingwithFileTypes: true, keeping only non-directory entries with.sqlextensions. Symlinked.sqlfiles are included because the filter uses!isDirectory()rather thanisFile(). New tests in migrations.test.ts cover missing directories, mixed file types, and interaction withparseStrictLocalMigrations.Macroscope summarized 60ddbda.