Skip to content

feat(cli): add transient declarative schema apply - #6500

Open
avallete wants to merge 9 commits into
developfrom
codex/cli-2281-transient-apply
Open

feat(cli): add transient declarative schema apply#6500
avallete wants to merge 9 commits into
developfrom
codex/cli-2281-transient-apply

Conversation

@avallete

@avallete avallete commented Sep 7, 2026

Copy link
Copy Markdown
Member

Summary

Adds supabase db schema declarative sync --transient for local declarative updates that should not create durable migration artifacts. The command plans against the running local database, displays the exact ordered SQL, requires explicit confirmation, and executes it without writing migration files or migration history.

The change shares planning and compatibility handling between migration-backed and transient flows, preserves destructive-change gates, and includes the complete plan in text and machine-readable success or error output.

It also hardens regular --apply recovery and cleanup, validates migration stems, and distinguishes data-preserving image refreshes from destructive Postgres-major upgrades.

Linear: https://linear.app/supabase/issue/CLI-2281/declarative-sync-apply-without-writing-a-migration-transient-apply-ux

@avallete avallete changed the title Add transient declarative schema apply flow feat(cli): add transient declarative schema apply Sep 7, 2026
@avallete

avallete commented Sep 7, 2026

Copy link
Copy Markdown
Member Author

/ai-review

@avallete
avallete marked this pull request as ready for review September 7, 2026 18:08
@avallete
avallete requested a review from a team as a code owner September 7, 2026 18:08
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Supabase CLI preview

npx --yes https://pkg.pr.new/supabase/cli/supabase@5339e7ed43c8d8d4e08f919bd2886dcb543bf938

Preview package for commit 5339e7e.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Superseded by a newer AI review

🤖 AI Review

Only Claude's independent review was available; the Codex review did not complete. After checking all eight findings against the PR sources and trusted ADRs, seven are confirmed (one narrowed to the verified whitespace issue) and one is refuted because the production adapter derives SQL from the same rendered-file array.

Findings

Severity Location Category Sources Claim
🟠 MAJOR apps/cli/src/commands/db/schema/declarative/sync/sync.handler.ts:935 data-loss claude Failed multi-segment applies delete migration files for earlier segments that may already be committed and recorded in migration history.
🟡 MINOR apps/cli/src/commands/db/schema/declarative/sync/sync.handler.ts:812 error-handling claude An image-preflight refusal is handled as a migration execution failure, producing misleading apply-failure output and deleting the generated files although database execution never began.
🟡 MINOR apps/cli/src/commands/db/schema/declarative/sync/sync.handler.ts:976 error-handling claude The declarative-tree probe can leak a raw PlatformError when the configured path cannot be walked instead of returning a typed declarative error.
🟡 MINOR apps/cli/src/commands/db/schema/declarative/sync/sync.handler.ts:693 ux-safety claude Transient application defaults its confirmation to Yes even when drop warnings identify destructive changes.
⚪ NIT apps/cli/src/commands/db/schema/declarative/declarative.flow.ts:52 input-validation claude Migration-stem validation examines a trimmed value, but noninteractive flag values are written untrimmed, allowing unintended leading or trailing whitespace in filenames.
⚪ NIT apps/cli/src/commands/db/schema/declarative/sync/SIDE_EFFECTS.md:143 documentation claude The documentation says reset recovery is offered after SQL execution starts, but a connection failure can also trigger the offer.
⚪ NIT apps/cli/src/commands/db/schema/declarative/sync/sync.command.ts:50 ux-consistency claude The new transient mode accepts --apply but does not treat it as no-prompt consent, despite --apply being described as applying without prompting.
Refuted findings (kept for transparency, not posted as review comments)
  • apps/cli/src/commands/db/schema/declarative/sync/sync.handler.ts:775 (correctness): A nonempty diffSQL paired with no rendered files would be reported as a successful no-op because the removed fallback no longer writes diffSQL directly.
    Refuted: The real planner adapter derives sql by joining the formatted rendered files and derives files from that same array. An empty rendered array therefore cannot produce the claimed nonempty diffSQL state in production.

Stats

Claude findings: 8 · Codex findings: 0 · Confirmed: 7 · Refuted: 1 · Uncertain: 0


Models: claude-opus-5 + gpt-5.6-sol · Trigger: manual · Workflow run

This review runs once per PR. A maintainer can request another with a /ai-review comment.

Comment thread apps/cli/src/commands/db/schema/declarative/sync/sync.handler.ts Outdated
Comment thread apps/cli/src/commands/db/schema/declarative/sync/sync.handler.ts Outdated
Comment thread apps/cli/src/commands/db/schema/declarative/sync/sync.handler.ts Outdated
Comment thread apps/cli/src/commands/db/schema/declarative/sync/sync.handler.ts Outdated
Comment thread apps/cli/src/commands/db/schema/declarative/declarative.flow.ts Outdated
Comment thread apps/cli/src/commands/db/schema/declarative/sync/SIDE_EFFECTS.md Outdated
Comment thread apps/cli/src/commands/db/schema/declarative/sync/sync.command.ts
@avallete

avallete commented Sep 8, 2026

Copy link
Copy Markdown
Member Author

/ai-review

# Conflicts:
#	apps/cli/src/command-internal/migration-apply.ts
#	apps/cli/src/command-internal/migration-apply.unit.test.ts
#	apps/cli/src/commands/db/schema/declarative/declarative.flow.unit.test.ts
#	apps/cli/src/commands/db/schema/declarative/declarative.orchestrate.integration.test.ts
#	apps/cli/src/commands/db/schema/declarative/declarative.orchestrate.ts
#	apps/cli/src/commands/db/schema/declarative/sync/SIDE_EFFECTS.md
#	apps/cli/src/commands/db/schema/declarative/sync/sync.handler.ts
#	apps/cli/src/commands/db/schema/declarative/sync/sync.integration.test.ts
#	apps/cli/src/commands/db/schema/declarative/sync/sync.layers.ts
#	apps/cli/src/commands/db/shared/legacy-pgdelta-engine.service.ts
#	apps/cli/src/commands/db/shared/pgdelta-engine.next.layer.integration.test.ts
#	apps/cli/src/commands/db/shared/pgdelta-engine.next.layer.ts
#	apps/cli/src/commands/db/shared/pgdelta-files.ts
#	apps/cli/src/commands/db/shared/pgdelta-next-shadow.layer.ts
#	apps/cli/src/commands/db/shared/pgdelta-next-shadow.service.ts
#	apps/cli/src/commands/db/shared/pgdelta.seam.layer.ts
#	apps/cli/src/commands/db/shared/pgdelta.seam.layer.unit.test.ts
@Coly010

Coly010 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Merged develop into this branch to resolve the merge conflicts caused by #6525 (the mechanical removal of the legacy/Legacy naming prefix across ~1400 files). This branch predated that rename, so develop's renamed identifiers/paths (e.g. LegacyCliSettingsCommandSettings, legacy-pgdelta-engine.service.tspgdelta-engine.service.ts) collided with this branch's own new code, which still used the pre-rename names.

No functional changes to this PR's own logic — every conflict was resolved by keeping this branch's feature work (transient apply, partial-migration preservation, etc.) intact and simply renaming its identifiers/imports to match develop's post-#6525 naming. pnpm check:all (types/lint/fmt/knip) and the full apps/cli unit + integration suites pass on the merged tree.

Please double check the merge, especially:

  • apps/cli/src/commands/db/schema/declarative/sync/sync.handler.ts — rewritten wholesale to weave the transient-apply feature back into develop's renamed baseline (largest/most conflicted file).
  • apps/cli/src/command-internal/migration-apply.ts — the extracted execMigrationStatements/applyRenderedSqlUnits helpers, merged in around develop's renamed applyMigrationFile.
  • apps/cli/src/commands/db/shared/pgdelta-engine.service.ts and pgdelta-next-shadow.service.ts — new fields/interfaces (source on PgDeltaDeclarativePlanInput, PgDeltaNextDeclarativeShadow) ported in with renamed types since develop didn't have this branch's additions yet.

Let me know if anything looks off.

Comment thread apps/cli/src/commands/db/shared/pgdelta.seam.layer.ts
Comment thread apps/cli/src/commands/db/schema/declarative/sync/sync.handler.ts Outdated
@jgoux
jgoux self-requested a review September 9, 2026 09:18
A fresh-volume start would migrate, seed, and record history before the
user consents. Same-major standard↔OrioleDB image drift now uses
stop --all --no-backup.
@avallete

avallete commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

/ai-review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 AI Review

Both independent reviews completed. After deduplication, six findings are confirmed and three are refuted. The most serious confirmed issue is deletion of generated migrations after SQL may already have committed; the remaining confirmed issues concern validation, diagnostics, stale-container detection, remediation guidance, and documentation.

Findings

Severity Location Category Sources Claim
🟠 MAJOR apps/cli/src/commands/db/schema/declarative/sync/sync.handler.ts:903 data-consistency claude+codex A failed durable apply can delete generated migration files after some SQL has already committed, leaving the local database partially changed without corresponding migration history or migration files.
🟡 MINOR apps/cli/src/commands/db/shared/pgdelta.seam.layer.ts:287 error-handling claude When an image tag has no parseable Postgres major, the remediation can recommend a data-preserving restart even if the underlying major changed, potentially leaving PostgreSQL unable to start on the existing data directory.
🟡 MINOR apps/cli/src/commands/db/schema/declarative/sync/sync.handler.ts:155 cli-ux claude --transient --apply=true is accepted but --apply does not provide consent, despite its help text promising application without prompting.
🟡 MINOR apps/cli/src/commands/db/schema/declarative/sync/sync.handler.ts:351 correctness claude The transient running-database gate treats a stopped but existing container as running, so the intended actionable error is replaced by a later connection failure.
🟡 MINOR apps/cli/src/command-internal/migration-apply.ts:696 error-handling claude+codex Transient multi-unit failures omit the failing unit name and report a unit-local statement index without qualification.
🟡 MINOR apps/cli/src/commands/db/schema/declarative/generate/SIDE_EFFECTS.md:98 documentation claude Generate's SIDE_EFFECTS.md still documents destructive remediation for every real version/tag mismatch even though the shared guard now preserves data for same-major tag drift.

Findings outside the diff

  • 🟡 MINOR apps/cli/src/commands/db/schema/declarative/generate/SIDE_EFFECTS.md:98 — Generate's SIDE_EFFECTS.md still documents destructive remediation for every real version/tag mismatch even though the shared guard now preserves data for same-major tag drift.
Refuted findings (kept for transparency, not posted as review comments)
  • apps/cli/src/commands/db/schema/declarative/sync/sync.handler.ts:971 (observability): A failed transient apply misleadingly reports applied:false to machine consumers without communicating that earlier SQL may have committed.
    Refuted: applied:false accurately indicates that the complete plan did not succeed, while the same machine envelope's error.suggestion explicitly warns that nontransactional or earlier units may already have applied.
  • apps/cli/src/commands/db/schema/declarative/sync/sync.handler.ts:956 (behavior-change): The recursive SQL-file presence check incorrectly makes durable sync reject manifest-only or non-SQL declarative trees that previously reached planning.
    Refuted: Although the implementation changes behavior for non-SQL entries, it brings the probe into alignment with the trusted pre-existing contract that SQL files must exist; a manifest alone was not documented as a valid schema tree.
  • apps/cli/src/commands/db/schema/declarative/sync/SIDE_EFFECTS.md:46 (documentation): The statement that transient mode never starts the database is contradicted by staged-export recovery calling ensureLocalDatabaseStarted.
    Refuted: The staged-export helper is reachable only after the transient running check succeeded, and ensureLocalDatabaseStarted then returns without starting anything. Its separate reset prompt does not contradict the narrower claim that transient mode never starts a stopped database.

Stats

Claude findings: 9 · Codex findings: 2 · Confirmed: 6 · Refuted: 3 · Uncertain: 0


Models: claude-opus-5 + gpt-5.6-sol · Trigger: manual · Workflow run

This review runs once per PR. A maintainer can request another with a /ai-review comment.

if (isSlimImageRef(expected) !== isSlimImageRef(actual)) {
return "The image family changed (slim vs docker.io). Run supabase stop, then supabase start with the same SUPABASE_USE_SLIM_IMAGES setting before syncing declarative schemas.";
}
return "Run supabase stop, then supabase start before syncing declarative schemas.";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 MINOR · error-handling · source: claude

When an image tag has no parseable Postgres major, the remediation can recommend a data-preserving restart even if the underlying major changed, potentially leaving PostgreSQL unable to start on the existing data directory.

Evidence: apps/cli/src/commands/db/shared/pgdelta.seam.layer.ts:263-287 returns undefined for unparseable tags and only selects destructive major-upgrade remediation when both majors are known; the fallback at line 287 recommends plain stop/start. The unit test at apps/cli/src/commands/db/shared/pgdelta.seam.layer.unit.test.ts:64-71 confirms this behavior for :latest.

Suggested fix: Warn that a destructive reset may be required when either major is unknown and PostgreSQL rejects the existing data directory.

Comment on lines +903 to +935
const appliedSegmentCount = yield* Ref.get(appliedSegments);
let keepGeneratedFiles = appliedSegmentCount > 0;
if (keepGeneratedFiles) {
yield* output.raw(
"Generated migration files were kept because one or more segments were already recorded in migration history.\n",
"stderr",
);
} else if (tty.stdinIsTty && !yes) {
keepGeneratedFiles = yield* output.promptConfirm("Keep the generated migration file(s)?", {
defaultValue: false,
});
}
if (!keepGeneratedFiles) {
if (!debugBundle.migrationSqlSaved) {
yield* output.raw(
"Generated migration files were kept because debug artifacts could not be saved.\n",
"stderr",
);
} else {
for (const migrationPath of migrationPaths) {
yield* fs
.remove(migrationPath)
.pipe(
Effect.catch((error) =>
output.raw(
`Warning: failed to remove generated migration ${migrationPath}: ${error.message}\n`,
"stderr",
),
),
);
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 MAJOR · data-consistency · source: claude+codex

A failed durable apply can delete generated migration files after some SQL has already committed, leaving the local database partially changed without corresponding migration history or migration files.

Evidence: apps/cli/src/commands/db/schema/declarative/sync/sync.handler.ts:903-935 retains files automatically only when a complete segment incremented appliedSegments. apps/cli/src/command-internal/migration-apply.ts:547-583 executes transaction_mode=none statements sequentially before inserting history, while lines 663-676 can also commit pipeline-incompatible statements before a later failure.

Suggested fix: Preserve generated files whenever execution may have committed any SQL; only delete after a positive indication that no statement executed or committed.

Comment on lines +155 to +175
if (Option.isSome(flags.apply) && !flags.apply.value) {
return yield* Effect.fail(
new DeclarativeMutuallyExclusiveFlagsError({
message: "--transient cannot be combined with --apply=false",
}),
);
}
const conflicts: Array<string> = [];
if (Option.isSome(flags.noApply)) conflicts.push("no-apply");
if (Option.isSome(flags.file)) conflicts.push("file");
if (Option.isSome(flags.name)) conflicts.push("name");
if (conflicts.length > 0) {
return yield* Effect.fail(
new DeclarativeMutuallyExclusiveFlagsError({
message: `--transient cannot be combined with ${conflicts
.map((flag) => `--${flag}`)
.join(", ")}`,
}),
);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 MINOR · cli-ux · source: claude

--transient --apply=true is accepted but --apply does not provide consent, despite its help text promising application without prompting.

Evidence: apps/cli/src/commands/db/schema/declarative/sync/sync.handler.ts:155-175 rejects only --apply=false for transient mode; lines 664-674 require --yes independently of flags.apply. apps/cli/src/commands/db/schema/declarative/sync/sync.command.ts:40-42 describes --apply as applying without prompting.

Suggested fix: Either reject every --transient/--apply combination or treat --apply=true as transient-apply consent.

Comment on lines +351 to +358
if (!(yield* seam.isLocalDatabaseRunning())) {
return yield* Effect.fail(
new DeclarativeLocalDbNotRunningError({
message: `${aqua("supabase start")} is not running.`,
suggestion: "Start the local database, then rerun sync --transient.",
}),
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 MINOR · correctness · source: claude

The transient running-database gate treats a stopped but existing container as running, so the intended actionable error is replaced by a later connection failure.

Evidence: apps/cli/src/commands/db/schema/declarative/sync/sync.handler.ts:351 relies on seam.isLocalDatabaseRunning. Its implementation delegates to isLocalDbRunning, where apps/cli/src/command-internal/db-bootstrap/local-db-running.ts:97-123 returns true solely from a successful container inspect; Docker inspection also succeeds for stopped containers.

Suggested fix: Inspect the container's State.Running value rather than treating existence as evidence that it is running.

Comment on lines +696 to +701
(unit) =>
execMigrationStatements(session, splitAndTrim(unit.sql), unit.transactionMode).pipe(
Effect.mapError((error) =>
error instanceof DbConnectError
? error
: mapError(errorMessage(error), formattedExecBatchDbError(error)),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 MINOR · error-handling · source: claude+codex

Transient multi-unit failures omit the failing unit name and report a unit-local statement index without qualification.

Evidence: apps/cli/src/command-internal/migration-apply.ts:679-705 defines each unit's name but never reads it during execution or error mapping. execMigrationStatements begins statement indexing from zero for each invocation at lines 547-552.

Suggested fix: Include the unit name and sequence in mapped errors or translate the index to a cumulative plan-wide offset.

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.

3 participants