Skip to content

Add db migration commands#71

Open
Fermionic-Lyu wants to merge 1 commit intomainfrom
codex/db-migrations-cli
Open

Add db migration commands#71
Fermionic-Lyu wants to merge 1 commit intomainfrom
codex/db-migrations-cli

Conversation

@Fermionic-Lyu
Copy link
Copy Markdown
Contributor

@Fermionic-Lyu Fermionic-Lyu commented Apr 17, 2026

Add db migrations CLI commands for listing, fetching, creating, and applying migrations

  • Adds four subcommands under db migrations: list (shows remote migrations), fetch (writes remote migrations as local .sql files), new <name> (creates a sequenced empty migration file), and up <target> (posts a local migration's SQL to the backend).
  • Adds helper utilities in src/lib/migrations.ts for filename parsing, local sequence validation, directory management, and migration target resolution.
  • Migration filenames follow the strict pattern <sequence>_<name>.sql; new and up enforce contiguous local sequences relative to remote state.
  • Adds a design spec at docs/specs/2026-04-17-db-migrations-command-design.md covering validation rules, API dependencies, and output conventions.
📊 Macroscope summarized 5112bef. 6 files reviewed, 1 issue evaluated, 0 issues filtered, 1 comment posted

🗂️ Filtered Issues

Summary by CodeRabbit

Release Notes

  • New Features

    • Added database migrations CLI command with subcommands to manage database migrations: list to view all remote migrations, fetch to download remote migrations locally, new to create a new migration file, and up to apply a migration to the database.
  • Tests

    • Added comprehensive test coverage for migration utilities and validation logic.
  • Chores

    • Updated CLI package version and dependencies.

Comment thread src/lib/migrations.ts
Comment on lines +99 to +105
export function formatMigrationSql(statements: string[]): string {
return statements
.map((statement) => statement.trim().replace(/;\s*$/u, ''))
.filter(Boolean)
.join(';\n\n')
.concat(statements.length > 0 ? ';\n' : '');
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low lib/migrations.ts:99

formatMigrationSql uses statements.length > 0 to decide whether to append a trailing semicolon, but this checks the original array length, not the length after filtering. When statements contains only whitespace strings, they are filtered out yet the condition still passes, causing the function to return '; ' instead of an empty string.

+export function formatMigrationSql(statements: string[]): string {
+  const formatted = statements
+    .map((statement) => statement.trim().replace(/;\s*$/u, ''))
+    .filter(Boolean);
+  return formatted.length > 0
+    ? formatted.join(';\n\n').concat(';\n')
+    : '';
+}
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file src/lib/migrations.ts around lines 99-105:

`formatMigrationSql` uses `statements.length > 0` to decide whether to append a trailing semicolon, but this checks the original array length, not the length after filtering. When `statements` contains only whitespace strings, they are filtered out yet the condition still passes, causing the function to return `';
'` instead of an empty string.

Evidence trail:
src/lib/migrations.ts lines 99-104 at REVIEWED_COMMIT - formatMigrationSql function shows `.concat(statements.length > 0 ? ';\n' : '')` checking original array length after the chain applies `.filter(Boolean)` which removes empty strings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 17, 2026

Walkthrough

This PR introduces database migration management to the InsForge CLI ecosystem. It adds a new db migrations command with four subcommands (list, fetch, new, up) that interact with a backend API, includes migration filename validation and sequencing logic, defines the feature spec, provides unit tests for utilities, and updates dependencies.

Changes

Cohort / File(s) Summary
Documentation & Specification
docs/specs/2026-04-17-db-migrations-command-design.md
New spec defining CLI surface for migrations including subcommand signatures, API contract (GET/POST /api/database/migrations), validation rules, filename regex, and control flow for list, fetch, new, and up operations.
Migration Utilities
src/lib/migrations.ts, src/lib/migrations.test.ts
New module providing migration filename parsing (parseMigrationFilename), directory management, sequence validation, local migration chain enforcement, SQL formatting, and migration targeting logic; comprehensive test suite validates all helper functions and edge cases.
Database Migrations Command
src/commands/db/migrations.ts
New command implementation registering four subcommands: list (fetch and display remote migrations), fetch (download remote migrations as local .sql files), new <migration-name> (create next local migration), and up <target> (apply a migration with remote sequence validation).
CLI Integration & Type Exports
src/index.ts, src/types.ts, package.json
Wired registerDbMigrationsCommand into the db command; re-exported migration-related types from shared-schemas (Migration, DatabaseMigrationsResponse, CreateMigrationRequest, CreateMigrationResponse); bumped CLI version to 0.1.53-dev.0 and updated shared-schemas dependency to ^1.1.50-dev.0.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Chore/data schema #10: Adds re-exports of shared-schemas types in src/types.ts; this PR extends those re-exports with migration-specific types.

Suggested reviewers

  • jwfing
  • tonychang04

Poem

🐰 Hops with glee through migrations so fine,
Each sequence numbered, each .sql in line,
Fetch, list, and climb to the next remote state,
No gaps, no confusion—just timely updates await!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add db migration commands' directly and clearly summarizes the main change—introducing database migration CLI commands with four subcommands (list, fetch, new, up) as the primary feature.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/db-migrations-cli

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/lib/migrations.test.ts (1)

77-105: Add a regression test for explicit filename targets with duplicate sequences.

Current coverage misses the case where filename-target resolution should succeed even if another file shares the same sequence number.

🧪 Proposed test addition
 describe('resolveMigrationTarget', () => {
   const filenames = ['6_create-users.sql', '7_add-user-index.sql'];
@@
   it('fails when a sequence number is ambiguous', () => {
     expect(() =>
       resolveMigrationTarget('6', ['6_create-users.sql', '6_create-accounts.sql'])
     ).toThrow(/multiple local migration files/i);
   });
+
+  it('uses the exact filename target even when sequence number is duplicated', () => {
+    expect(
+      resolveMigrationTarget(
+        '6_create-users.sql',
+        ['6_create-users.sql', '6_create-accounts.sql'],
+      ),
+    ).toEqual({
+      filename: '6_create-users.sql',
+      sequenceNumber: 6,
+      name: 'create-users',
+    });
+  });
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/migrations.test.ts` around lines 77 - 105, Add a regression test to
migrations.test.ts that verifies resolveMigrationTarget accepts an explicit
filename even when another file shares the same sequence number: create a case
where filenames include two files with the same leading sequence (e.g.,
'6_create-users.sql' and '6_create-accounts.sql') and call
resolveMigrationTarget with the full filename target (e.g.,
'6_create-accounts.sql'), then assert the returned object has the correct
filename, sequenceNumber 6, and name 'create-accounts'; this ensures
resolveMigrationTarget still succeeds for explicit filename targets despite
duplicate sequence numbers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/commands/db/migrations.ts`:
- Around line 102-109: Call assertValidMigrationName(migration.name) for each
remote migration before using it to build a filepath so malicious names can't
escape migrationsDir; inside the for loop that iterates over migrations (the
loop that also calls buildMigrationFilename), insert a validation step calling
assertValidMigrationName(migration.name) immediately before invoking
buildMigrationFilename(migration.sequenceNumber, migration.name) and let the
function throw or handle errors as appropriate to stop processing invalid names.

In `@src/lib/migrations.ts`:
- Around line 99-105: formatMigrationSql currently decides whether to append the
trailing ';\n' using the original statements.length, which emits a lone
semicolon for inputs that are all-blank; modify the function to compute the
cleaned/filtered list first (e.g., map+trim+remove-empty into a local variable)
and then use that filtered array's length when deciding to append the trailing
';\n' (keep the same join delimiter ';\n\n' and trimming/semicolon-stripping
logic in the mapping step) so blank-only inputs produce an empty string instead
of ";\\n".
- Around line 139-150: The code rejects exact filename targets when there are
multiple same-sequence siblings because after verifying
filenames.includes(target) it re-resolves by sequence using
findLocalMigrationBySequence(parsedTarget.sequenceNumber, filenames); change
this to return the migration for the exact filename instead of re-resolving by
sequence — e.g., use a helper to find by filename or construct/lookup the
migration entry for target (referencing parsedTarget, target, filenames, and
replacing the call to findLocalMigrationBySequence) so an explicit filename
never triggers the "multiple local migration files" error.

---

Nitpick comments:
In `@src/lib/migrations.test.ts`:
- Around line 77-105: Add a regression test to migrations.test.ts that verifies
resolveMigrationTarget accepts an explicit filename even when another file
shares the same sequence number: create a case where filenames include two files
with the same leading sequence (e.g., '6_create-users.sql' and
'6_create-accounts.sql') and call resolveMigrationTarget with the full filename
target (e.g., '6_create-accounts.sql'), then assert the returned object has the
correct filename, sequenceNumber 6, and name 'create-accounts'; this ensures
resolveMigrationTarget still succeeds for explicit filename targets despite
duplicate sequence numbers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9087ec1f-6fa5-4a2a-9761-9ba6ad7cb1da

📥 Commits

Reviewing files that changed from the base of the PR and between cea7763 and 5112bef.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (7)
  • docs/specs/2026-04-17-db-migrations-command-design.md
  • package.json
  • src/commands/db/migrations.ts
  • src/index.ts
  • src/lib/migrations.test.ts
  • src/lib/migrations.ts
  • src/types.ts

Comment on lines +102 to +109
for (const migration of [...migrations].sort(
(left, right) => left.sequenceNumber - right.sequenceNumber,
)) {
const filename = buildMigrationFilename(
migration.sequenceNumber,
migration.name,
);
const filePath = join(migrationsDir, filename);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Verify where remote migration names are converted to file paths:"
rg -n -C4 "buildMigrationFilename\\(|writeFileSync\\(|migration\\.name" src/commands/db/migrations.ts

echo
echo "Verify validation usage points:"
rg -n -C2 "assertValidMigrationName\\(" src/commands/db/migrations.ts

Repository: InsForge/CLI

Length of output: 2368


🏁 Script executed:

# Get more context around the fetch flow to understand error handling pattern
sed -n '95,125p' src/commands/db/migrations.ts

Repository: InsForge/CLI

Length of output: 1038


🏁 Script executed:

# Check how errors are typically handled in this file for consistency
rg -n "throw new CLIError|warn\\(|logger" src/commands/db/migrations.ts | head -20

Repository: InsForge/CLI

Length of output: 524


🏁 Script executed:

# Verify the full context of remote migration fetch and processing
sed -n '60,120p' src/commands/db/migrations.ts

Repository: InsForge/CLI

Length of output: 1913


🏁 Script executed:

# Check the full signature and behavior of assertValidMigrationName
sed -n '48,52p' src/commands/db/migrations.ts

Repository: InsForge/CLI

Length of output: 251


🏁 Script executed:

# Look for all usages of migration.name in context of file operations
rg -n "migration\.name|migrationName" src/commands/db/migrations.ts

Repository: InsForge/CLI

Length of output: 324


🏁 Script executed:

# Verify if there are other remote name sources that might need validation
rg -n "fetchRemoteMigrations|from.*api|from.*remote" src/commands/db/migrations.ts

Repository: InsForge/CLI

Length of output: 418


Validate remote migration names before using in file paths.

Line 107 passes migration.name from the remote API directly to buildMigrationFilename without validation. A name containing path separators (e.g., ../../../x) could write files outside the intended .insforge/migrations/ directory. Call assertValidMigrationName(migration.name) before line 105 to harden against malicious or corrupted API responses.

🔒 Proposed hardening
         for (const migration of [...migrations].sort(
           (left, right) => left.sequenceNumber - right.sequenceNumber,
         )) {
+          assertValidMigrationName(migration.name);
+
           const filename = buildMigrationFilename(
             migration.sequenceNumber,
             migration.name,
           );
           const filePath = join(migrationsDir, filename);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/db/migrations.ts` around lines 102 - 109, Call
assertValidMigrationName(migration.name) for each remote migration before using
it to build a filepath so malicious names can't escape migrationsDir; inside the
for loop that iterates over migrations (the loop that also calls
buildMigrationFilename), insert a validation step calling
assertValidMigrationName(migration.name) immediately before invoking
buildMigrationFilename(migration.sequenceNumber, migration.name) and let the
function throw or handle errors as appropriate to stop processing invalid names.

Comment thread src/lib/migrations.ts
Comment on lines +99 to +105
export function formatMigrationSql(statements: string[]): string {
return statements
.map((statement) => statement.trim().replace(/;\s*$/u, ''))
.filter(Boolean)
.join(';\n\n')
.concat(statements.length > 0 ? ';\n' : '');
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid emitting a lone semicolon for all-blank statements.

The trailing semicolon condition uses the original array length, not the filtered statements. For input like [' '], output becomes ";\n" unexpectedly.

🛠️ Proposed fix
 export function formatMigrationSql(statements: string[]): string {
-  return statements
+  const normalizedStatements = statements
     .map((statement) => statement.trim().replace(/;\s*$/u, ''))
-    .filter(Boolean)
-    .join(';\n\n')
-    .concat(statements.length > 0 ? ';\n' : '');
+    .filter(Boolean);
+
+  return normalizedStatements
+    .join(';\n\n')
+    .concat(normalizedStatements.length > 0 ? ';\n' : '');
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function formatMigrationSql(statements: string[]): string {
return statements
.map((statement) => statement.trim().replace(/;\s*$/u, ''))
.filter(Boolean)
.join(';\n\n')
.concat(statements.length > 0 ? ';\n' : '');
}
export function formatMigrationSql(statements: string[]): string {
const normalizedStatements = statements
.map((statement) => statement.trim().replace(/;\s*$/u, ''))
.filter(Boolean);
return normalizedStatements
.join(';\n\n')
.concat(normalizedStatements.length > 0 ? ';\n' : '');
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/migrations.ts` around lines 99 - 105, formatMigrationSql currently
decides whether to append the trailing ';\n' using the original
statements.length, which emits a lone semicolon for inputs that are all-blank;
modify the function to compute the cleaned/filtered list first (e.g.,
map+trim+remove-empty into a local variable) and then use that filtered array's
length when deciding to append the trailing ';\n' (keep the same join delimiter
';\n\n' and trimming/semicolon-stripping logic in the mapping step) so
blank-only inputs produce an empty string instead of ";\\n".

Comment thread src/lib/migrations.ts
Comment on lines +139 to +150
const parsedTarget = parseMigrationFilename(target);
if (!parsedTarget) {
throw new CLIError(
'Migration file names must match <sequence_number>_<migration-name>.sql.',
);
}

if (!filenames.includes(target)) {
throw new CLIError(`Local migration file not found: ${target}`);
}

return findLocalMigrationBySequence(parsedTarget.sequenceNumber, filenames);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Exact filename targets should not fail due to same-sequence siblings.

After Line 146 confirms the exact filename exists, Line 150 re-resolves by sequence and can throw “multiple local migration files” unnecessarily. This breaks explicit filename targeting.

✅ Proposed fix
 export function resolveMigrationTarget(
   target: string,
   filenames: string[],
 ): ParsedMigrationFile {
   if (/^[1-9][0-9]*$/u.test(target)) {
     return findLocalMigrationBySequence(Number(target), filenames);
   }
@@
   if (!filenames.includes(target)) {
     throw new CLIError(`Local migration file not found: ${target}`);
   }
 
-  return findLocalMigrationBySequence(parsedTarget.sequenceNumber, filenames);
+  return parsedTarget;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/migrations.ts` around lines 139 - 150, The code rejects exact
filename targets when there are multiple same-sequence siblings because after
verifying filenames.includes(target) it re-resolves by sequence using
findLocalMigrationBySequence(parsedTarget.sequenceNumber, filenames); change
this to return the migration for the exact filename instead of re-resolving by
sequence — e.g., use a helper to find by filename or construct/lookup the
migration entry for target (referencing parsedTarget, target, filenames, and
replacing the call to findLocalMigrationBySequence) so an explicit filename
never triggers the "multiple local migration files" error.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant