feat(cli): add schema-first schema and migrations commands - #6274
feat(cli): add schema-first schema and migrations commands#6274avallete wants to merge 15 commits into
Conversation
Stable and alpha share one Effect runtime: declarations in supabase/schemas, durable remote mutations only through migrations push, and --from local resolving this project's native stack or Docker DB.
| source: { | ||
| kind: target.kind, | ||
| identity: target.identity, | ||
| connection: redactConnectionString(target.connectionString), |
There was a problem hiding this comment.
🟡 Severity: MEDIUM
Connection-string redaction leaves query credentials intact. schema pull --from <connection-string> places target.connectionString into output after redactConnectionString, which only replaces the URI password field; secrets supplied as query parameters such as ?password=... remain and are emitted to stdout or CI logs.
Helpful? Add 👍 / 👎
💡 Fix Suggestion
Suggestion: Update the redactConnectionString function in apps/cli/src/shared/database/database-target.ts to also redact credential-related URL query parameters. Currently the function only replaces the URI authority password (parsed.password), leaving secrets passed as query parameters (e.g., ?password=..., ?sslpassword=...) intact. Fix the function by iterating over parsed.searchParams and replacing values for known sensitive parameter names with ****. Example fix:
export function redactConnectionString(url: string): string {
try {
const parsed = new URL(url);
if (parsed.password) parsed.password = "****";
const sensitiveParams = ["password", "sslpassword", "secret", "token", "key", "apikey", "api_key"];
for (const param of sensitiveParams) {
if (parsed.searchParams.has(param)) {
parsed.searchParams.set(param, "****");
}
}
return parsed.toString();
} catch {
return "<unparseable-connection-string>";
}
}This ensures all credential paths in a connection string are sanitised before the value is emitted to stdout or CI logs.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 77aa3fc405
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| await pool.query(ENSURE_HISTORY); | ||
| const result = await pool.query<MigrationHistoryRow>(LIST_HISTORY); |
There was a problem hiding this comment.
Do not create migration history during read operations
When the target database does not yet have supabase_migrations.schema_migrations, listRemote executes ENSURE_HISTORY, which creates the schema and table. This makes migrations list mutate a database it reports as read-only and, more critically, lets migrations push alter a remote before authorizeMutation rejects an unconfirmed or destructive invocation. Use a side-effect-free existence/read query here and create the history table only after mutation authorization.
Useful? React with 👍 / 👎.
| const localTarget = yield* targets.resolve({ kind: "local" }).pipe(Effect.option); | ||
| if (localTarget._tag === "Some") { |
There was a problem hiding this comment.
Run remote drift checks without a local stack
When supabase start is not running, resolving the local target returns None and the entire drift check is skipped, even though the block only uses a newly provisioned isolated shadow and never uses localTarget. In common CI or deployment environments without a local stack, pending migrations are therefore applied to a drifted remote despite the command promising to fail closed on drift; provision the shadow and perform this comparison unconditionally.
Useful? React with 👍 / 👎.
| disposable: false, | ||
| durable: true, | ||
| connectionVerified: false, | ||
| projectRef: linked.value.project.ref, |
There was a problem hiding this comment.
Preserve linked-target identity authorization
Every linked target is marked connectionVerified: false, so authorizeMutation treats the normal linked-project path exactly like a raw --db-url: it always requires --allow-remote and then returns before checking the supplied --project-ref. Consequently the documented linked push flow cannot use its project-ref confirmation, while enabling the raw-URL bypass also ignores a mismatched ref. Linked targets need their own identity-assertion path rather than this raw-URL classification.
Useful? React with 👍 / 👎.
| if (!liveVsDeclared.changes) { | ||
| yield* runner.recordApplied(localPool, written); | ||
| } |
There was a problem hiding this comment.
Keep schema generation from recording local history
Whenever the running local database already matches the declarations, schema generate inserts the newly written versions into migration history without executing them. This silently mutates the database even though the result reports mutated_database: false, and a later apply will skip those files; if checkpoint persistence subsequently fails, cleanup deletes the files but leaves their history rows behind. Generation should not register migrations as applied implicitly.
Useful? React with 👍 / 👎.
| const migrations = yield* MigrationRepository; | ||
| const runner = yield* MigrationRunner; | ||
|
|
||
| const target = yield* targets.resolve({ kind: "local" }); |
There was a problem hiding this comment.
Allow schema generation without a running local stack
The local target is resolved before checking dryRun or building the isolated migration-to-declaration plan, although it is only used later by the implicit history-reconciliation block. Thus schema generate, including --dry-run and --baseline, fails with “Run supabase start” in an otherwise valid project, contradicting the isolated-shadow M → D workflow and making generation unusable in offline/CI environments without a running stack.
Useful? React with 👍 / 👎.
| for (let attempt = 0; attempt < attempts; attempt++) { | ||
| const result = yield* Effect.result(effect); | ||
| if (result._tag === "Success") return result.success; | ||
| if (attempt === attempts - 1) { | ||
| return yield* Effect.fail(result.failure); |
There was a problem hiding this comment.
Use a Schedule for shadow readiness retries
The native-shadow runtime implements readiness and cache-lock polling with a custom counted for loop around Effect.result and Effect.sleep. Replace this with an Effect Schedule so retry timing, exhaustion, failure propagation, and interruption follow the workspace's required runtime model.
AGENTS.md reference: AGENTS.md:L62-L68
Useful? React with 👍 / 👎.
| const directory = input.mode === "output" ? (input.outputDir ?? schemasDir) : schemasDir; | ||
| const directoryDisplay = | ||
| input.mode === "output" | ||
| ? path.relative(paths.projectRoot, directory) |
There was a problem hiding this comment.
Resolve schema pull output relative to the project workdir
When the stable CLI is invoked with --workdir /path/to/project and a relative schema pull --output snapshot, this code passes snapshot directly to the filesystem, so it is created relative to the process's original working directory rather than /path/to/project. The existing legacy declarative commands resolve output against their configured workdir; resolve this path against paths.projectRoot before reading or writing it.
Useful? React with 👍 / 👎.
| const declarations = yield* workspace.readDeclarationFiles; | ||
| const localMigrations = yield* migrations.listLocal; | ||
| const existingCheckpoint = yield* state.readCheckpoint; |
There was a problem hiding this comment.
Reject migration-head changes before generating a draft
When a draft journal exists from schema apply, generation reads the current migration files but never compares them with the journal's startingMigrationHeadDigest. Adding or removing a migration during the draft therefore changes the clean replay used as the source, yet generation still writes a change set and marks the journal generated, incorrectly treating a migration based on a different starting history as the durable recipe. Fail closed on a changed head before planning.
Useful? React with 👍 / 👎.
| const result = yield* generateSchema({ | ||
| name: Option.getOrUndefined(flags.name), | ||
| dryRun: flags.dryRun, | ||
| baseline: flags.baseline, | ||
| }); |
There was a problem hiding this comment.
Show task progress while schema generation runs
In text mode this handler waits for the complete isolated-shadow planning and verification pipeline before emitting any output, so starting or restoring several Postgres clusters can make the CLI appear hung for an extended period. Acquire an output.task before invoking the use case and settle it on success or failure; machine formats will suppress that task automatically.
AGENTS.md reference: apps/cli/AGENTS.md:L453-L465
Useful? React with 👍 / 👎.
| } else { | ||
| yield* runner.applyPending(pool, localMigrations); | ||
| } |
There was a problem hiding this comment.
Report migrations applied as part of schema apply
When no draft is active, schema apply first applies pending migration files but discards the returned applied list. If those migrations bring the database into agreement with the declarations, the subsequent no-change branch reports mutated_database: false and “already matches” even though this invocation changed schema and history. Preserve the apply result and include it in the mutation flag and user-visible result.
Useful? React with 👍 / 👎.
Drop the checkpoint sidecar and --allow-data-loss. Generate and local reset clear the journal; apply records history when the live catalog already matches; push live-verifies ahead and drift unless --skip-verify.
| }); | ||
| } | ||
| const applied: Array<string> = []; | ||
| for (const file of pending) { |
There was a problem hiding this comment.
🟡 Severity: MEDIUM
Concurrent migrations push invocations can both observe the same pending versions and enter this loop, then execute each migration before inserting its history row. Transactional scripts can race, while nontransactional scripts can be applied twice or partially, corrupting the target schema despite the history primary key.
Helpful? Add 👍 / 👎
💡 Fix Suggestion
Suggestion: To prevent concurrent migrations push processes from racing, acquire a PostgreSQL session-level advisory lock before computing the pending set and release it after all migrations are applied. The implementation should: (1) Obtain a dedicated client via pool.connect() instead of using pool.query() throughout applyPending, since session-level advisory locks are tied to a single connection—using the pool would release the lock prematurely when connections are returned. (2) Before line 83 (before calling ENSURE_HISTORY), execute SELECT pg_advisory_lock(LOCK_ID) on the dedicated client, where LOCK_ID is a stable application-specific bigint (e.g., 7723485672041572291n, derived from hashing the string 'supabase_migrations.schema_migrations'). (3) Wrap the entire ENSURE_HISTORY, LIST_HISTORY, pending-compute, and migration-apply sequence in a try/finally block that calls SELECT pg_advisory_unlock(LOCK_ID) and then releases the client back to the pool. This ensures that even if migration execution fails mid-loop, the lock is always freed. Because this change requires switching all pool.query() calls inside applyPending to use a single dedicated client, restructuring into a pool.connect() / try-finally / client.release() pattern, and spanning the lock across all individual per-migration transactions, it cannot be expressed as a single-location code replacement.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2866313a8e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } satisfies SchemaCommandResult; | ||
| } | ||
|
|
||
| const outcome = yield* engine.applyPlan({ pool, plan }); |
There was a problem hiding this comment.
Journal the plan before applying it
When applyPlan fails its Effect—for example, a connection loss after the database accepted one or more actions—execution exits here before writeJournal runs, leaving a partially mutated local database with no draft record. A subsequent invocation can therefore replan and retry without the action statuses or in-doubt state that the journal is meant to preserve; persist the planned journal before applying and update it with the outcome afterward.
Useful? React with 👍 / 👎.
| await pool.query("BEGIN"); | ||
| try { | ||
| await pool.query(file.content); | ||
| await pool.query(INSERT_HISTORY, [file.version, file.name, [file.content]]); | ||
| await pool.query("COMMIT"); |
There was a problem hiding this comment.
Run each migration transaction on one checked-out client
When this pool has multiple idle clients, these independent pool.query calls may execute BEGIN, the migration SQL, the history insert, and COMMIT on different sessions. The preceding catalog operations can populate several pool connections, so a failed migration can leave SQL or history committed even though the code issues ROLLBACK; acquire one client and use that client for the entire transaction.
Useful? React with 👍 / 👎.
| const declarations = yield* workspace.readDeclarationFiles; | ||
| const localMigrations = yield* migrations.listLocal; |
There was a problem hiding this comment.
Read generation inputs after acquiring the schema lock
When schema generate overlaps schema pull, both declarations and migrations are read before withLock is acquired. Generate can therefore cache a partially written or stale declaration tree, wait for pull to release the lock, and then write and verify migrations against that stale snapshot rather than the files now on disk; move these reads into the locked effect.
Useful? React with 👍 / 👎.
| const remoteVersions = new Set(remote.map((row) => row.version)); | ||
| const pending = local.filter((file) => !remoteVersions.has(file.version)); | ||
| const remoteOnly = remote.filter( | ||
| (row) => !local.some((file) => file.version === row.version), | ||
| ); | ||
| if (remoteOnly.length > 0 && pending.length > 0) { |
There was a problem hiding this comment.
Reject holes in applied migration history
When the remote has a newer local version recorded but is missing an older one—for example, history contains v2 while local files contain v1 and v2—this filter treats v1 as pending and applies it after v2. The later divergence check does not catch this because there are no remote-only versions, so migrations can execute out of their defined order; require pending migrations to form a strict suffix of local history.
Useful? React with 👍 / 👎.
| import { legacyMigrationsPullCommand } from "./pull/pull.command.ts"; | ||
| import { legacyMigrationsPushCommand } from "./push/push.command.ts"; | ||
|
|
||
| export const legacyMigrationsCommand = Command.make("migrations").pipe( |
There was a problem hiding this comment.
Add the required legacy side-effect manifests
Neither new stable-shell command group includes a SIDE_EFFECTS.md, so the database writes, filesystem changes, environment inputs, and failure exits introduced by these commands have no compatibility checklist. Add the required manifest for both schema and migrations rather than shipping undocumented legacy command surfaces.
AGENTS.md reference: apps/cli/AGENTS.md:L185-L189
Useful? React with 👍 / 👎.
| } else { | ||
| await pool.query(file.content); | ||
| await pool.query(INSERT_HISTORY, [file.version, file.name, [file.content]]); |
There was a problem hiding this comment.
Serialize concurrent migration deployments in Postgres
When two push processes target the same database, both can observe the same pending version and execute this non-transactional migration before either inserts its history row. One history insert then fails on the primary key, but both migration bodies have already committed, so DML or other non-idempotent effects can be applied twice; hold a database advisory lock across history inspection, migration execution, and history recording.
Useful? React with 👍 / 👎.
| yield* fs | ||
| .writeFileString(target, file.sql) | ||
| .pipe( | ||
| Effect.mapError((error) => ioError(`Failed to write ${target}: ${error.message}`)), | ||
| ); |
There was a problem hiding this comment.
Stage schema exports before replacing declarations
When a later write, removal, or manifest update fails during schema pull --force—for example because the disk fills—earlier files have already been overwritten in the live declaration tree. The command returns an error while leaving a partial mixture of old and new declarations, and may already have deleted stale files; build the managed export in a staging directory and publish it atomically or restore the original tree on failure.
Useful? React with 👍 / 👎.
| if (drift.changes) { | ||
| return yield* new SchemaRemoteDriftError({ | ||
| detail: "Remote database shape has drifted from migration replay.", | ||
| suggestion: "Run `supabase migrations pull` and reconcile before pushing.", |
There was a problem hiding this comment.
Fail closed when remote drift coverage is incomplete
When pg-delta reports a coverage gap for an unmodeled remote or replayed object, drift.changes can be false because no modeled action represents that object, even though drift.coverageBlocked is true. This branch then treats the remote as verified and proceeds with pending migrations despite being unable to prove that replay matches it; reject coverage-blocked drift plans in addition to plans with rendered changes.
Useful? React with 👍 / 👎.
| const load = Effect.gen(function* () { | ||
| const next = yield* loadFromPath(projectHome.projectLinkPath); | ||
| if (Option.isSome(next)) return next; | ||
| return yield* loadFromLegacyTemp; |
There was a problem hiding this comment.
Prefer the stable link state after a legacy relink
When a checkout still has .supabase/project.json from the next shell and the user subsequently runs stable supabase link or unlink, the stable command updates only supabase/.temp, but this adapter always returns the stale next-shell record first. Schema and migration commands can therefore authorize against the old project ref and select its Postgres version after the user linked a different project; dual-write the next state from legacy link/unlink or make this adapter prefer the current stable state.
AGENTS.md reference: apps/cli/AGENTS.md:L372-L377
Useful? React with 👍 / 👎.
The sidecar is unused. Drop the type, workspace path, and journal-store read/write so existing .schema-checkpoint.json files stay inert.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 836b871756
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const plan = yield* engine.diffPools({ | ||
| sourcePool, | ||
| desiredPool: remotePool, | ||
| allowDrops: true, | ||
| }); |
There was a problem hiding this comment.
Preserve remote history versions during migration pull
When the remote history contains a version absent from the checkout, this path compares only database catalogs and never reads that history. It therefore either reports clean or writes equivalent SQL under a new timestamp; the next push then sees the original remote-only version together with the newly pending local version and raises SchemaHistoryConflictError, whose suggested migrations pull repeats the same dead end. Pull must materialize remote migrations using their original versions or otherwise reconcile the history explicitly.
Useful? React with 👍 / 👎.
| const files: Array<SchemaSqlFile> = []; | ||
| for (const name of names) { | ||
| const relative = prefix === "" ? name : `${prefix}/${name}`; | ||
| if (prefix === "" && name === SCHEMA_CUSTOM_DIRECTORY_NAME) continue; |
There was a problem hiding this comment.
Include
_custom SQL in declarative planning
When users place hand-authored or unsupported declarations under the reserved _custom/ directory, this unconditional skip also affects readDeclarationFiles, not just export ownership classification. As a result, schema apply, schema generate, and the declarations-ahead check in migrations push silently omit those files and can report that declarations already match while never generating their changes. Exclude _custom only when managing exported files, but include it when reading the complete declaration tree for planning.
Useful? React with 👍 / 👎.
| yes: Flag.boolean("yes").pipe( | ||
| Flag.withDescription("Answer ordinary prompts. Does not skip target identity or live verify."), | ||
| Flag.withAlias("y"), | ||
| ), |
There was a problem hiding this comment.
Consume the existing global
--yes flag
For the supported persistent-flag form supabase --yes migrations push, the root consumes the global LegacyYesFlag, while this same-named local flag remains false and is the value passed to authorizeMutation; a non-interactive linked push is therefore rejected despite explicit confirmation. Reuse or merge the root global flag rather than shadowing it locally.
AGENTS.md reference: apps/cli/AGENTS.md:L340-L344
Useful? React with 👍 / 👎.
| message: `Compared ${rows.length} migration(s) against ${target.identity}.`, | ||
| data: { | ||
| status: "clean", | ||
| target: target.identity, | ||
| migrations: rows, |
There was a problem hiding this comment.
Render migration rows in text-mode list output
On a default text invocation, the shared renderer prints only this summary message and never displays data.migrations. Consequently supabase migrations list reports only how many rows were compared, without showing any version, name, or local/remote presence—the information the command exists to provide. Include the rows in the text result, preferably using the repository's table renderer.
Useful? React with 👍 / 👎.
| message: plan.changes | ||
| ? `${summary}\nResult: preview only; nothing was changed` | ||
| : "Live database matches migration replay.", | ||
| data: { | ||
| status: plan.changes ? "drift" : "clean", | ||
| plan_id: plan.planId, | ||
| hazards: plan.hazards, | ||
| sql: plan.files.map((file) => file.sql).join("\n\n"), |
There was a problem hiding this comment.
Print preview SQL from text-mode migration diffs
When drift exists and the user does not pass --file, the generated SQL is stored only in data.sql; the default text renderer prints message, whose plan summary contains counts and fingerprints but no SQL. Thus the documented migrations diff --against … examples do not actually preview the required SQL unless users switch to machine output or already know to add --file. Include the rendered SQL in the text result when no output file is requested.
Useful? React with 👍 / 👎.
| const postgresDir = yield* resolveNativeBinary(cliConfig.supabaseHome, { | ||
| service: "postgres", | ||
| version: postgresVersion, | ||
| }).pipe( |
There was a problem hiding this comment.
Provide a shadow fallback on unsupported native platforms
On shipped win32 and darwin-x64 CLI builds, postgresAssetName() returns null in packages/stack/src/Platform.ts, so this unconditional resolveNativeBinary call always fails with BinaryNotFoundError before any schema plan can run. This makes shadow-dependent commands such as schema generate, schema apply, migrations diff, migrations pull, and default-verified migrations push unusable on those platforms even when a Docker stack is available; provide a Docker shadow fallback or another supported Postgres runtime.
Useful? React with 👍 / 👎.
Push, pull, and baseline generate now print copy-pasteable `migration repair` commands instead of looping on pull-and-reconcile. Env URLs are unverifiable `--allow-remote` targets; without them, stable connect uses the TypeScript linked resolver.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fb075606b2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (parsed.password) parsed.password = "****"; | ||
| return parsed.toString(); |
There was a problem hiding this comment.
Redact query-string passwords from connection output
When schema pull uses a PostgreSQL URI such as postgresql://user@host/db?password=secret, the password is a query parameter, so parsed.password is empty and this function returns the secret unchanged. The pull result includes this redacted value in data.source.connection, causing JSON and stream-JSON output—often captured by CI logs—to disclose the database credential; remove sensitive connection parameters as well as authority passwords.
Useful? React with 👍 / 👎.
| if (!plan.changes) { | ||
| return { | ||
| status: "clean", | ||
| message: "No remote drift to record.", |
There was a problem hiding this comment.
Reject coverage-blocked migration pulls
When pg-delta reports an unmodeled remote object, plan.coverageBlocked can be true even when plan.changes is false or the rendered files cover only part of the drift. This branch nevertheless reports the remote as clean, or the later branch writes an incomplete migration and recommends marking it applied, leaving remote state without a durable local recipe. Check the coverage/rename gates before accepting or writing this pull plan.
Useful? React with 👍 / 👎.
| yield* renderSchemaResult("Generate schema migrations", generated); | ||
| if (flags.apply) { | ||
| const applied = yield* applySchema({ | ||
| yes: true, | ||
| allowRemote: false, | ||
| }); | ||
| yield* renderSchemaResult("Apply declarative schema", applied); |
There was a problem hiding this comment.
Emit one result for declarative sync --apply
With db schema declarative sync --apply --output-format json, the first render writes a complete JSON object to stdout and the second render writes another, while JSON mode is a single-result transport. Consumers performing one JSON.parse(stdout) therefore fail; if apply errors, stdout similarly contains the generation success followed by an error envelope. Compose both operations into one final result instead of rendering the intermediate generation result.
Useful? React with 👍 / 👎.
| if (input.file !== undefined) { | ||
| const fs = yield* FileSystem.FileSystem; | ||
| const sql = plan.files.map((file) => file.sql).join("\n\n"); | ||
| yield* fs.writeFileString(input.file, sql).pipe( |
There was a problem hiding this comment.
Resolve migration diff files against the project root
When either shell selects a project through --workdir or project-root discovery and --file is relative, this writes relative to the process's original working directory rather than the selected project. For example, running from /tmp with --workdir /repo migrations diff --file drift.sql creates /tmp/drift.sql; resolve the destination against the schema workspace's project root before writing and reporting it.
Useful? React with 👍 / 👎.
| const target = path.join(directory, rel); | ||
| yield* fs | ||
| .makeDirectory(path.dirname(target), { recursive: true }) | ||
| .pipe( | ||
| Effect.mapError((error) => | ||
| ioError(`Failed to create ${path.dirname(target)}: ${error.message}`), | ||
| ), | ||
| ); | ||
| yield* fs | ||
| .writeFileString(target, file.sql) |
There was a problem hiding this comment.
Refuse to write schema exports through symlinked directories
When an existing declaration tree contains a symlinked directory matching an exported path, walkSqlFiles deliberately skips that symlink during classification, but this write still follows it. A checkout containing supabase/schemas/public -> /somewhere can therefore make schema pull --force overwrite files outside the project despite the relative-path validation; verify every destination component is not a symlink or stage the export in a fresh tree before publishing it.
Useful? React with 👍 / 👎.
| const postgresVersion = versions.runtimeVersions.postgres ?? DEFAULT_VERSIONS.postgres; | ||
| const loadedConfig = yield* loadProjectConfig(projectHome.projectRoot).pipe( | ||
| Effect.provideService(FileSystem.FileSystem, fs), | ||
| Effect.provideService(Path.Path, path), | ||
| Effect.mapError(toShadowError), | ||
| ); | ||
| const autoExposeNewTables = loadedConfig?.config.api.auto_expose_new_tables ?? false; |
There was a problem hiding this comment.
Honor the configured Postgres major for native shadows
For an unlinked project with db.major_version set to 14, 15, or 16, neither linked state nor .supabase/local-versions.json normally supplies a Postgres override, so this resolves the default 17.6 binary even though loadProjectConfig immediately below has the configured major. Schema generation, diffing, and verification then replay migrations against a different PostgreSQL major than supabase start and the intended remote, which can accept unsupported SQL or produce version-specific plans; select a native artifact matching the configured database major when no explicit service-version pin overrides it.
Useful? React with 👍 / 👎.
| return ` | ||
| export PATH="${pgBinDir}:$PATH" | ||
| export PGPASSWORD=postgres | ||
| db="${migrationsDir}" |
There was a problem hiding this comment.
Stop native baseline initialization on the first SQL failure
When any platform migration psql invocation exits nonzero, this generated shell script has no set -e or explicit status check, so it continues into later setup commands; if the final password-update query succeeds, requireExitZero sees exit code 0 and publishes the partially initialized cluster as a reusable baseline. Subsequent schema plans then run against a corrupt platform state without surfacing the original migration error. Make the script fail immediately, while retaining the one explicitly nonfatal stats-reset command.
Useful? React with 👍 / 👎.
| readonly versions: ReadonlyArray<string>; | ||
| readonly flags?: MigrationRepairFlags; | ||
| }): string { | ||
| const parts = ["supabase", "migration", "repair"]; |
There was a problem hiding this comment.
Provide migration repair in the next command tree
The shared baseline, pull, apply, and drift paths emit this as their copy-pasteable recovery command in both shells, but the newly added next-shell migration group registers only new, list, and up. A next user following the required post-baseline or post-pull action therefore gets an unknown-subcommand error and cannot reconcile history through that shell; add a next repair command or emit a recovery action that exists in the invoking command tree.
AGENTS.md reference: apps/cli/AGENTS.md:L18-L22
Useful? React with 👍 / 👎.
| const exists = yield* fs.exists(schemasDir).pipe(Effect.orElseSucceed(() => false)); | ||
| if (!exists) return []; |
There was a problem hiding this comment.
Propagate declaration-directory access failures
If the existence check fails with a permission or transient filesystem error, this converts the failure to false and treats the declaration set as intentionally empty. schema generate can then produce verified drop migrations from the existing migration replay to an empty schema, while schema apply can plan those drops against the local database; only a genuine not-found result should yield an empty declaration list, and other failures should remain typed workspace errors.
AGENTS.md reference: AGENTS.md:L64-L68
Useful? React with 👍 / 👎.
| const exists = yield* fs.exists(filePath).pipe(Effect.orElseSucceed(() => false)); | ||
| if (!exists) return Option.none(); |
There was a problem hiding this comment.
Fail closed when the draft journal cannot be inspected
When fs.exists fails because .supabase is inaccessible or an I/O error occurs, this returns Option.none() exactly as if no journal existed. In particular, migrations push uses that result to pass assertNoUngeneratedDraft and does not later acquire the schema lock, so it can deploy migration files while an active declarative draft is present but unreadable. Treat only NotFound as absence and map every other failure to SchemaStateError.
AGENTS.md reference: AGENTS.md:L64-L68
Useful? React with 👍 / 👎.
…rfc-schema-first-development # Conflicts: # packages/stack/src/effect.ts
Supabase CLI previewnpx --yes https://pkg.pr.new/supabase/cli/supabase@5c45575a47981964e43a09a5e05aa6182f557048Preview package for commit |
schema generate --baseline now fails closed when migrations already exist, and drift/pull next-actions include pasteable --from / --project-ref flags without echoing secrets.
Drop native Postgres and next/ command copies so generate, apply, and push reuse the existing shadow-baseline tar pool and legacy migration helpers.
The platform image still installs pgjwt, so RESTRICT drop of pgcrypto fails during CLI-owned shadow prep. Name the failing statement and point persistent misses at supabase issue bug.
Stop duplicate clack success/error lines and print short diagnostic names instead of a generic coverage gap. Full engine text stays on --debug.
Declaration-prep drops pgcrypto, so push drift and applyLocalPending prefix-match failed or reported false remote drift.
Picks up supabase-profile filtering of platform parameter ACLs, storage/realtime RLS in exports, vault_presence hazards, OWNED BY filed with the owning table, and per-statement load fallback.
alpha.43's supabase profile already drops the bootstrap log_min_messages grants, so the CLI copy and extra catalog query are redundant.
Pull, diff, and schema export were emitting unindented statements because the next-engine default was only lowercase + width 180.
Schema and migrations --help now describe the local-first loop, drop the Prisma/Drizzle mapping, and print next steps as guidance instead of a bare command list.
Summary
Adds the schema-first
schemaandmigrationscommands on the stable CLI (and the same verbs on alpha). Declarative SQL insupabase/schemasis shape intent; migration files stay the durable recipe;migrations pushis the only durable remote mutation.The draft journal is only an ungenerated local
schema applyartifact. Generate and local reset clear it. Apply recordssupabase_migrationswhen the live catalog already matches full file replay. Push live-verifies declarations-ahead and remote drift unless--skip-verify. There is no--allow-data-lossand no tracked schema checkpoint.--from localresolves this project's native stack first, then this project's Docker DB container and the port it published. Stable--profileis forwarded into the shared runtime so schema/migrations honor the already-resolved profile.Stacked on #6223 because planning uses isolated native Postgres APIs from that lineage (
connectLayer, managed stack state). The working spec isdocs/cli/schema-first-v1-plan.md.Linked issue
Supabase maintainer change; no public issue to close.