Skip to content

feat: add db migrate command#7985

Open
eduardoboucas wants to merge 5 commits intomainfrom
feat/db-migrate
Open

feat: add db migrate command#7985
eduardoboucas wants to merge 5 commits intomainfrom
feat/db-migrate

Conversation

@eduardoboucas
Copy link
Member

No description provided.

@eduardoboucas eduardoboucas requested a review from a team as a code owner March 4, 2026 21:03
@coderabbitai
Copy link

coderabbitai bot commented Mar 4, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added experimental db migrate command with --to and --json options.
    • Support for db.migrations.path in config to specify a migrations directory.
  • Tests

    • Added comprehensive unit tests for the migrate command covering lifecycle, errors, JSON output, and logging.
  • Refactor

    • Adjusted public typing for the dev-start API.

Walkthrough

Adds a conditional db migrate CLI subcommand enabled when process.env.EXPERIMENTAL_NETLIFY_DB_ENABLED === '1'. The subcommand accepts --to and --json, resolves project root and migrations path (typed via a new optional db.migrations.path on CachedConfig), dynamically imports @netlify/db-dev, starts a local DB under the build .netlify/db, applies migrations (optionally up to a target), prints JSON or human-readable results, and ensures the DB is stopped on success or error. Also updates a dev API return type and adds unit tests for success, errors, and output modes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

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.
Description check ❓ Inconclusive No pull request description was provided by the author, making it impossible to assess relevance to the changeset. Add a description explaining the purpose, functionality, and context of the new db migrate command to help reviewers understand the changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title 'feat: add db migrate command' accurately describes the main change: a new CLI subcommand 'migrate' under the 'db' command.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/db-migrate

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

@github-actions
Copy link

github-actions bot commented Mar 4, 2026

📊 Benchmark results

Comparing with af66409

  • Dependency count: 1,080 (no change)
  • Package size: 330 MB (no change)
  • Number of ts-expect-error directives: 362 (no change)

Copy link

@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

🤖 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/database/migrate.ts`:
- Around line 45-47: The JSON output is being suppressed because the call uses
the CLI helper log(...) which is muted in JSON mode; replace the call that
currently uses log(JSON.stringify({ migrations_applied: applied })) with an
unconditional stdout write (e.g., console.log or process.stdout.write) so that
when the json flag/variable is true the JSON is always emitted; update the
branch in migrate.ts that checks json to use console.log(JSON.stringify(...))
(referencing the json, applied and log symbols) so db migrate --json reliably
prints the JSON.
- Around line 27-43: The dynamic import returns an untyped value causing
unsafe-member-access; cast the imported module and strongly type the db
instance: change the import to const dbDev = (await import('@netlify/db-dev'))
as typeof import('@netlify/db-dev') so NetlifyDB = dbDev.NetlifyDB is typed, and
type the created db as the instance type e.g. const db = new NetlifyDB({
directory: dbDirectory }) as InstanceType<typeof
import('@netlify/db-dev').NetlifyDB> so calls like db.start() and
db.applyMigrations are properly typed.

In `@tests/unit/commands/database/migrate.test.ts`:
- Line 25: Remove the unused ESLint disable directive "//
eslint-disable-next-line import/first" from the test file (it's the comment on
line where that directive appears) because the import/first rule/plugin isn't
configured; simply delete that comment and re-run lint/tests to confirm the
unused-disable no longer errors.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 87d2673c-4cb4-4260-b258-729efe7b1469

📥 Commits

Reviewing files that changed from the base of the PR and between 2f9510a and 2c42866.

📒 Files selected for processing (5)
  • src/commands/database/database.ts
  • src/commands/database/migrate.ts
  • src/commands/dev/programmatic-netlify-dev.ts
  • src/lib/build.ts
  • tests/unit/commands/database/migrate.test.ts

Comment on lines +45 to +47
if (json) {
log(JSON.stringify({ migrations_applied: applied }))
} else if (applied.length === 0) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

--json output can be swallowed in real runs.

Line 46 uses log(...), but log is suppressed in JSON mode (src/utils/command-helpers.ts, Lines 160-167). This can produce empty output for db migrate --json.

🔧 Proposed fix
     if (json) {
-      log(JSON.stringify({ migrations_applied: applied }))
+      process.stdout.write(`${JSON.stringify({ migrations_applied: applied })}\n`)
     } else if (applied.length === 0) {
📝 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
if (json) {
log(JSON.stringify({ migrations_applied: applied }))
} else if (applied.length === 0) {
if (json) {
process.stdout.write(`${JSON.stringify({ migrations_applied: applied })}\n`)
} else if (applied.length === 0) {
🧰 Tools
🪛 GitHub Check: Lint

[failure] 47-47:
Unsafe member access .length on an error typed value


[failure] 46-46:
Unsafe assignment of an error typed value

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/database/migrate.ts` around lines 45 - 47, The JSON output is
being suppressed because the call uses the CLI helper log(...) which is muted in
JSON mode; replace the call that currently uses log(JSON.stringify({
migrations_applied: applied })) with an unconditional stdout write (e.g.,
console.log or process.stdout.write) so that when the json flag/variable is true
the JSON is always emitted; update the branch in migrate.ts that checks json to
use console.log(JSON.stringify(...)) (referencing the json, applied and log
symbols) so db migrate --json reliably prints the JSON.

Copy link

@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: 1

🧹 Nitpick comments (1)
tests/unit/commands/database/migrate.test.ts (1)

40-41: Use satisfies with type validation instead of double-cast to catch fixture drift early.

The double-cast as unknown as Parameters<typeof migrate>[1] bypasses TypeScript's structural checks. When BaseCommand changes, mismatched fixture properties won't be caught. Use satisfies to validate only the properties your fixture provides, then safely cast to the full type.

♻️ Suggested refactor
+type MigrateCommand = Parameters<typeof migrate>[1]
+
 function createMockCommand(overrides: { buildDir?: string; projectRoot?: string; migrationsPath?: string } = {}) {
   const {
     buildDir = '/project',
     projectRoot = '/project',
     migrationsPath = '/project/netlify/db/migrations',
   } = overrides

-  return {
+  const command = {
     project: { root: projectRoot, baseDirectory: undefined },
     netlify: {
       site: { root: buildDir },
       config: { db: { migrations: { path: migrationsPath } } },
     },
-  } as unknown as Parameters<typeof migrate>[1]
+  } satisfies Pick<MigrateCommand, 'project' | 'netlify'>
+
+  return command as MigrateCommand
 }

Also applies to: lines 81–82, 144

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/commands/database/migrate.test.ts` around lines 40 - 41, Replace
the double-cast "as unknown as Parameters<typeof migrate>[1]" with a
satisfies-based validation to catch fixture drift: change the fixture object to
use "satisfies Partial<Parameters<typeof migrate>[1]>" so TypeScript verifies
the provided properties against the migrate parameter shape, then (if needed)
cast to the full type with "as Parameters<typeof migrate>[1]" for the call;
apply the same pattern wherever the double-cast appears (the instances around
migrate, and the other occurrences noted relating to BaseCommand/fixture lines
81–82 and 144).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/unit/commands/database/migrate.test.ts`:
- Line 53: The test asserts a hardcoded POSIX path causing failures on Windows;
update the expectation in migrate.test.ts to use path.join() so it matches the
production path normalization. Replace the literal '/my/project/.netlify/db'
used with a constructed path via path.join('my','project','.netlify','db') (or
path.join('/my','project','.netlify','db') consistent with how the code builds
it) when asserting MockNetlifyDB was called so MockNetlifyDB is expected with {
directory: path.join(...) } matching the real behavior.

---

Nitpick comments:
In `@tests/unit/commands/database/migrate.test.ts`:
- Around line 40-41: Replace the double-cast "as unknown as Parameters<typeof
migrate>[1]" with a satisfies-based validation to catch fixture drift: change
the fixture object to use "satisfies Partial<Parameters<typeof migrate>[1]>" so
TypeScript verifies the provided properties against the migrate parameter shape,
then (if needed) cast to the full type with "as Parameters<typeof migrate>[1]"
for the call; apply the same pattern wherever the double-cast appears (the
instances around migrate, and the other occurrences noted relating to
BaseCommand/fixture lines 81–82 and 144).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f82c5700-6ac2-4729-b1f1-31a417bb0a08

📥 Commits

Reviewing files that changed from the base of the PR and between 2c42866 and dfb5a65.

📒 Files selected for processing (1)
  • tests/unit/commands/database/migrate.test.ts

test('creates NetlifyDB with the correct directory', async () => {
await migrate({}, createMockCommand({ buildDir: '/my/project' }))

expect(MockNetlifyDB).toHaveBeenCalledWith({ directory: '/my/project/.netlify/db' })
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check Windows CI configuration
rg -n "windows|Windows" .github/workflows --type yaml

# Read the test file around line 53
wc -l tests/unit/commands/database/migrate.test.ts

Repository: netlify/cli

Length of output: 1211


🏁 Script executed:

# Show the test file content around line 53
sed -n '40,70p' tests/unit/commands/database/migrate.test.ts

Repository: netlify/cli

Length of output: 976


🏁 Script executed:

# Check for path imports in the test file
head -20 tests/unit/commands/database/migrate.test.ts

Repository: netlify/cli

Length of output: 730


🏁 Script executed:

# Find the migrate function implementation
rg -n "export.*migrate|function migrate" src/commands/database/migrate.ts -A 5

Repository: netlify/cli

Length of output: 387


🏁 Script executed:

# Read the migrate file to understand how directory is built
head -100 src/commands/database/migrate.ts

Repository: netlify/cli

Length of output: 1765


Fix OS-portable path assertion in test.

The production code uses path.join() to construct the database directory path, which returns OS-specific normalized paths. However, the test asserts against a hardcoded POSIX path /my/project/.netlify/db. On Windows runners (which are configured in CI), this assertion will fail because path.join() produces a different path format.

Update the assertion to use path.join() to match the production code behavior:

Suggested fix
+import path from 'node:path'
 import { describe, expect, test, vi, beforeEach } from 'vitest'
 ...
-    expect(MockNetlifyDB).toHaveBeenCalledWith({ directory: '/my/project/.netlify/db' })
+    expect(MockNetlifyDB).toHaveBeenCalledWith({ directory: path.join('/my/project', '.netlify', 'db') })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/commands/database/migrate.test.ts` at line 53, The test asserts a
hardcoded POSIX path causing failures on Windows; update the expectation in
migrate.test.ts to use path.join() so it matches the production path
normalization. Replace the literal '/my/project/.netlify/db' used with a
constructed path via path.join('my','project','.netlify','db') (or
path.join('/my','project','.netlify','db') consistent with how the code builds
it) when asserting MockNetlifyDB was called so MockNetlifyDB is expected with {
directory: path.join(...) } matching the real behavior.

Copy link

@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.

♻️ Duplicate comments (1)
src/commands/database/migrate.ts (1)

52-53: ⚠️ Potential issue | 🟠 Major

--json output is silently swallowed.

The log() helper is suppressed when --json flag is present (per src/utils/command-helpers.ts lines 160-167). This results in empty output for db migrate --json.

🔧 Proposed fix
     if (json) {
-      log(JSON.stringify({ migrations_applied: applied }))
+      console.log(JSON.stringify({ migrations_applied: applied }))
     } else if (applied.length === 0) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/database/migrate.ts` around lines 52 - 53, The JSON output is
being suppressed because the command uses the log() helper which is muted for
--json; in migrate.ts replace the call to log(JSON.stringify({
migrations_applied: applied })) with a non-suppressed emitter (e.g.,
process.stdout.write or console.log) so the JSON is printed even when the json
flag is set; keep the same payload (using the applied variable) and ensure a
trailing newline is emitted.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/commands/database/migrate.ts`:
- Around line 52-53: The JSON output is being suppressed because the command
uses the log() helper which is muted for --json; in migrate.ts replace the call
to log(JSON.stringify({ migrations_applied: applied })) with a non-suppressed
emitter (e.g., process.stdout.write or console.log) so the JSON is printed even
when the json flag is set; keep the same payload (using the applied variable)
and ensure a trailing newline is emitted.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9e8bc11a-6cfa-4d89-857b-2be7ea34630c

📥 Commits

Reviewing files that changed from the base of the PR and between dfb5a65 and bdb6fca.

📒 Files selected for processing (1)
  • src/commands/database/migrate.ts

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