Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
77aa3fc
feat(cli): add schema-first schema and migrations commands
avallete Aug 19, 2026
2866313
feat(cli): keep the schema journal as a local draft, not the apply gate
avallete Aug 19, 2026
836b871
refactor(cli): delete the leftover schema checkpoint store
avallete Aug 19, 2026
fb07560
feat(cli): guide schema-first history with migration repair
avallete Aug 19, 2026
a51494f
Merge branch 'avallete/main-db-baseline-restore' into feat/implement-…
avallete Aug 19, 2026
c732def
fix(cli): refuse --baseline on existing history and name pull targets
avallete Aug 19, 2026
5365d6b
refactor(cli): run schema-first shadows on Docker baselines
avallete Aug 20, 2026
dba44b9
fix(cli): drop pgjwt before pgcrypto on PG15+ declarative shadows
avallete Aug 20, 2026
169f646
chore(cli): wrap declarative shadow prep lines for oxfmt
avallete Aug 20, 2026
91c7b9a
fix(cli): name schema coverage gaps without flooding output
avallete Aug 20, 2026
d93d7fa
fix(cli): replay migration verify onto a platform shadow
avallete Aug 20, 2026
e4cdd2c
feat(cli): upgrade pg-delta next to alpha.43
avallete Aug 20, 2026
9ebb5ba
refactor(cli): let pg-delta own platform parameter ACL coverage
avallete Aug 20, 2026
c7dcb61
fix(cli): pretty-print pg-delta SQL by default
avallete Aug 20, 2026
5c45575
feat(cli): default schema pull to local and rewrite workflow help
avallete Aug 20, 2026
d745d73
feat(cli): humanize schema-first text output
avallete Aug 21, 2026
9ad90eb
fix(cli): keep image extensions unless decls recreate them
avallete Aug 21, 2026
f43f7f4
fix(cli): consume pg-delta alpha.46 load assist
avallete Aug 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/cli/docs/go-cli-divergences.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@ These commands exist in the TS CLI today but have no direct top-level equivalent
`supabase start`, `db start`, `--from-backup`, and shadow containers (the last is why the
shadow baseline cache's cold export can stop/start in ~1s). Timing is not part of the
Go-parity surface (ADR 0016).
- Bundled pg-delta SQL for `db diff`/`db pull`/`db schema declarative generate`/`sync`
defaults to uppercase keywords, indent 2, width 180, trailing commas, and column/key
alignment when `[experimental.pgdelta] format_options` is unset. `format_options = "null"`
emits raw renderer output; partial JSON overlays those defaults. `schema pull` and
`schema generate` always use the same defaults (they do not read `format_options`). The
Go edge-runtime engine already defaulted to upper + width 180 (library fills the rest);
TS now makes indent/align explicit on the next-engine path.
- `db schema declarative generate`/`sync` default declarative directory is `supabase/schemas`;
the old Go CLI reference (pre-`7b469f5b3`) used `supabase/database`. The move aligns the
default with the product-wide declarative-schemas convention. To keep the upgrade visible,
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/docs/supabase/db/diff.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ By default, all schemas in the target database are diffed. Use the `--schema pub

Projects created by a recent `supabase init` default to the pg-delta diff engine (`[experimental.pgdelta] enabled = true` in `config.toml`). Existing projects are unaffected and keep using migra unless they opt in. To fall back to the legacy migra engine, set `enabled = false` under `[experimental.pgdelta]`, or pass `--use-migra` for a single run.

With the bundled pg-delta engine, diff SQL defaults to lowercase keywords and a maximum width of 180, matching its declarative export. When `-f` writes migrations, execution-aware transaction semantics are preserved as ordered per-unit files; non-transactional units carry a directive that the CLI apply path honors. Flattened review output retains the rendered SQL and preambles, but not the unit boundaries supplied to a migration runner. Configure overrides with `[experimental.pgdelta] format_options`, or set `format_options = "null"` to emit raw, unformatted statements.
With the bundled pg-delta engine, diff SQL defaults to uppercase keywords, indent 2, a maximum width of 180, trailing commas, and column/key alignment, matching its declarative export. When `-f` writes migrations, execution-aware transaction semantics are preserved as ordered per-unit files; non-transactional units carry a directive that the CLI apply path honors. Flattened review output retains the rendered SQL and preambles, but not the unit boundaries supplied to a migration runner. Configure overrides with `[experimental.pgdelta] format_options`, or set `format_options = "null"` to emit raw, unformatted statements.

While the diff command is able to capture most schema changes, there are cases where it is known to fail. Currently, this could happen if you schema contains:

Expand Down
2 changes: 2 additions & 0 deletions apps/cli/docs/supabase/db/schema-declarative-generate.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ Exports the schema of a live database (local, linked, or custom URL) into SQL fi

The bundled pg-delta engine writes one directory per schema at the root of that directory (`supabase/schemas/public/tables/users.sql`, `supabase/schemas/public/schema.sql`), with cluster-level objects that belong to no schema under a reserved `_cluster/` directory (`supabase/schemas/_cluster/roles.sql`). A schema literally named `_cluster` or `_custom`, in any casing, has its leading underscore percent-encoded (`%5Fcluster/`) so it can never claim a directory the export owns. Hand-authored SQL that pg-delta does not model belongs in `_custom/`, which the export never writes to and never prunes.

Emitted SQL uses the same default format as `db pull` (uppercase keywords, indent 2, width 180, column-aligned). Override with `[experimental.pgdelta] format_options`, or set `format_options = "null"` for raw statements.

Requires `--experimental` flag or `[experimental.pgdelta] enabled = true` in config.
2 changes: 1 addition & 1 deletion apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"@parcel/watcher": "^2.6.0",
"@supabase/api": "workspace:*",
"@supabase/config": "workspace:*",
"@supabase/pg-delta": "1.0.0-alpha.42",
"@supabase/pg-delta": "1.0.0-alpha.46",
"@supabase/pg-topo": "1.0.0-alpha.5",
"@supabase/process-compose": "workspace:*",
"@supabase/stack": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/legacy/cli/legacy-complete.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ describe("legacyRespondToComplete", () => {
// `--debug ""` used to return zero candidates entirely: the leftover-args
// computation counted `--debug` itself as "positional leftover," gating
// out subcommand-name completion the way cobra never does for a
// persistent flag: `__complete --debug ''` lists all 36 root commands.
// persistent flag: `__complete --debug ''` lists all root commands.
const result = legacyRespondToComplete(legacyRoot, ["__complete", "--debug", ""]);
expect(result?.directive).toBe(LegacyCompletionDirective.NoFileComp);
expect(result?.candidates.map((c) => c.name)).toContain("branches");
Expand Down
4 changes: 4 additions & 0 deletions apps/cli/src/legacy/cli/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { legacyLinkCommand } from "../commands/link/link.command.ts";
import { legacyLoginCommand } from "../commands/login/login.command.ts";
import { legacyLogoutCommand } from "../commands/logout/logout.command.ts";
import { legacyMigrationCommand } from "../commands/migration/migration.command.ts";
import { legacyMigrationsCommand } from "../commands/migrations/migrations.command.ts";
import { legacySchemaCommand } from "../commands/schema/schema.command.ts";
import { legacyNetworkBansCommand } from "../commands/network-bans/network-bans.command.ts";
import { legacyNetworkRestrictionsCommand } from "../commands/network-restrictions/network-restrictions.command.ts";
import { legacyOrgsCommand } from "../commands/orgs/orgs.command.ts";
Expand Down Expand Up @@ -78,11 +80,13 @@ export const legacyRoot = Command.make("supabase").pipe(
legacyLoginCommand,
legacyLogoutCommand,
legacyMigrationCommand,
legacyMigrationsCommand,
legacyNetworkBansCommand,
legacyNetworkRestrictionsCommand,
legacyOrgsCommand,
legacyPostgresConfigCommand,
legacyProjectsCommand,
legacySchemaCommand,
legacySecretsCommand,
legacySeedCommand,
legacyServicesCommand,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { declaredSqlExtensions } from "../../../../../shared/schema/prepare-declarative-shadow.ts";
import type { LegacyPgDeltaImplementation } from "../../../../shared/legacy-pgdelta-next-flag.ts";
import { legacySchemaToCsvField } from "../../../../shared/legacy-schema-flags.ts";
import type { LegacyPgDeltaRemovalSummary } from "../../shared/legacy-pgdelta-engine.service.ts";
Expand Down Expand Up @@ -159,18 +160,6 @@ function matchImplicitExtension(message: string): LegacyImplicitExtensionMatch |
};
}

/**
* Masks SQL comments and strings while preserving offsets. Extension declarations
* are DDL, so occurrences inside comments, quoted values, and dollar bodies must
* not suppress compatibility guidance.
*/
function maskSqlNonCode(sql: string): string {
return sql.replaceAll(
/--[^\r\n]*|\/\*[\s\S]*?\*\/|'(?:''|[^'])*'|\$(?:[a-zA-Z_][\w$]*)?\$[\s\S]*?\$(?:[a-zA-Z_][\w$]*)?\$/g,
(matched) => matched.replaceAll(/[^\r\n]/g, " "),
);
}

function maskSqlComments(sql: string): string {
return sql.replaceAll(/--[^\r\n]*|\/\*[\s\S]*?\*\//g, (matched) =>
matched.replaceAll(/[^\r\n]/g, " "),
Expand All @@ -180,16 +169,7 @@ function maskSqlComments(sql: string): string {
export function legacyDeclaredExtensions(
files: readonly LegacyDeclarativeSqlFile[],
): ReadonlySet<string> {
const declared = new Set<string>();
const pattern =
/\bCREATE\s+EXTENSION\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:"([^"]+)"|([a-zA-Z_][\w$-]*))/gi;
for (const file of files) {
for (const match of maskSqlNonCode(file.sql).matchAll(pattern)) {
const extension = match[1] ?? match[2];
if (extension !== undefined) declared.add(extension.toLowerCase());
}
}
return declared;
return declaredSqlExtensions(files);
}

function declaredImplicitExtensions(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,12 @@ describe("legacyDiffDeclarativeToMigrations", () => {
writeFileSync(join(declDir, "ignored.txt"), "ignored");
writeFileSync(
join(declDir, ".pgdelta-export.json"),
JSON.stringify({ formatVersion: 1, redactSecrets: true, scope: "database" }),
JSON.stringify({
formatVersion: 1,
redactSecrets: true,
scope: "database",
loadOrder: ["z.sql", "nested/a.sql"],
}),
);
const calls: LegacyPgDeltaDeclarativePlanInput[] = [];
const engine = Layer.succeed(
Expand Down Expand Up @@ -240,7 +245,11 @@ describe("legacyDiffDeclarativeToMigrations", () => {
{ name: "nested/a.sql", sql: "select 'a';" },
{ name: "z.sql", sql: "select 'z';" },
]);
expect(calls[0]?.manifest).toEqual({ redactSecrets: true, scope: "database" });
expect(calls[0]?.manifest).toEqual({
redactSecrets: true,
scope: "database",
loadOrder: ["z.sql", "nested/a.sql"],
});
expect(calls[0]?.debug).toBe(true);
expect(calls[0]?.noCache).toBe(true);
expect(calls[0]?.strictCoverage).toBe(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ function setup() {
Effect.fail(new LegacyDeclarativeShadowDbError({ message: "stop after routing" })),
),
),
provisionPlatform: () =>
Effect.fail(new LegacyDeclarativeShadowDbError({ message: "stop after routing" })),
provisionDeclarative: () =>
Effect.fail(new LegacyDeclarativeShadowDbError({ message: "stop after routing" })),
provisionPlan: (opts) =>
Effect.sync(() => {
state.plan += 1;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { Clock, Effect, FileSystem, Layer, Path } from "effect";
import type { Pool } from "pg";

import {
filesForDeclarativeShadowLoad,
prepareDeclarativeShadow,
} from "../../../../shared/schema/prepare-declarative-shadow.ts";
import { SchemaEngineError } from "../../../../shared/schema/schema-errors.ts";
import { Output } from "../../../../shared/output/output.service.ts";
import {
legacyLayeredParseEnv,
Expand Down Expand Up @@ -42,7 +47,9 @@ function legacyPgDeltaNextConnectSuggestion(cause: unknown): string | undefined

export const legacyPgDeltaNextEngineError = (cause: unknown) => {
if (cause instanceof LegacyPgDeltaEngineError) return cause;
const suggestion = legacyPgDeltaNextConnectSuggestion(cause);
const suggestion =
legacyPgDeltaNextConnectSuggestion(cause) ??
(cause instanceof SchemaEngineError ? cause.suggestion : undefined);
const diagnostics = cause instanceof LegacyPgDeltaNextError ? cause.diagnostics : undefined;
return new LegacyPgDeltaEngineError({
message:
Expand Down Expand Up @@ -350,10 +357,11 @@ export const legacyPgDeltaNextEngineLayer = Layer.effect(
],
{ concurrency: 2 },
);
yield* prepareDeclarativeShadow(declarativePool, input.files);
const result = yield* adapter.planDeclarativeSchema({
targetPool: migrationsPool,
shadowPool: declarativePool,
files: input.files,
files: filesForDeclarativeShadowLoad(input.files),
allowDrops: true,
...(shadow.allowSameDatabaseIdentity ? { allowSameDatabaseIdentity: true } : {}),
debug: input.debug,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export interface LegacyPgDeltaExportManifest {
readonly baselineDigest?: string;
readonly defaultOwner?: string | null;
readonly files?: ReadonlyArray<string>;
readonly loadOrder?: ReadonlyArray<string>;
}

export interface LegacyPgDeltaRenderedFile {
Expand Down Expand Up @@ -75,7 +76,8 @@ export type LegacyPgDeltaHazardKind =
| "access_exclusive_lock"
| "unmodeled_kind"
| "unmodeled_drift"
| "unresolved_security_label";
| "unresolved_security_label"
| "vault_presence";

interface LegacyPgDeltaActionHazard {
readonly actionIndex: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,17 @@ export const LegacyReadPgDeltaExportManifest = Effect.fnUntraced(function* (
const baselineDigest = readManifestValue(decoded, "baselineDigest");
const defaultOwner = readManifestValue(decoded, "defaultOwner");
const files = readManifestValue(decoded, "files");
const loadOrder = readManifestValue(decoded, "loadOrder");
return {
redactSecrets,
scope,
...(typeof profile === "string" ? { profile } : {}),
...(typeof baselineDigest === "string" ? { baselineDigest } : {}),
...(typeof defaultOwner === "string" || defaultOwner === null ? { defaultOwner } : {}),
...(Array.isArray(files) && files.every((file) => typeof file === "string") ? { files } : {}),
...(Array.isArray(loadOrder) && loadOrder.every((file) => typeof file === "string")
? { loadOrder }
: {}),
} satisfies LegacyPgDeltaExportManifest;
});

Expand Down
Loading
Loading