Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions apps/cli-go/pkg/migration/queries/drop.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ begin
-- schemas
for rec in
select pn.*
from pg_namespace pn
left join pg_depend pd on pd.objid = pn.oid
from pg_catalog.pg_namespace pn
left join pg_catalog.pg_depend pd on pd.objid = pn.oid and pd.classid = 'pg_catalog.pg_namespace'::regclass
where pd.deptype is null
and not pn.nspname like any(array['information\_schema', 'pg\_%', '\_analytics', '\_realtime', '\_supavisor', 'pgbouncer', 'pgmq', 'pgsodium', 'pgtle', 'supabase\_migrations', 'vault', 'extensions', 'public'])
and pn.nspowner::regrole::text != 'supabase_admin'
Expand Down
4 changes: 2 additions & 2 deletions apps/cli-go/pkg/migration/queries/list.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
-- Extension created schemas
-- Supabase managed schemas
select pn.nspname
from pg_namespace pn
left join pg_depend pd on pd.objid = pn.oid
from pg_catalog.pg_namespace pn
left join pg_catalog.pg_depend pd on pd.objid = pn.oid and pd.classid = 'pg_catalog.pg_namespace'::regclass
Comment thread
7ttp marked this conversation as resolved.
where pd.deptype is null
and not pn.nspname like any($1)
and pn.nspowner::regrole::text != 'supabase_admin'
Expand Down
20 changes: 7 additions & 13 deletions apps/cli/src/legacy/commands/db/lint/lint.lint-sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@
* - `LEGACY_CHECK_SCHEMA_SCRIPT` — the per-schema `plpgsql_check_function`
* mass-check.
* - `LEGACY_LIST_SCHEMAS_SQL` + `LEGACY_MANAGED_SCHEMAS` — lists user
* schemas, used when `--schema` is omitted. The `\_` / `pg\_%` escapes
* are preserved exactly — they are `LIKE` patterns.
* schemas, used when `--schema` is omitted. The query is shared with the
* migra bash fallback and defined once in `db/shared/legacy-migra.ts`
* (`legacyListSchemasSql`), re-exported here under this module's
* established constant name. The `\_` / `pg\_%` escapes are preserved
* exactly — they are `LIKE` patterns.
*/

export { legacyListSchemasSql as LEGACY_LIST_SCHEMAS_SQL } from "../shared/legacy-migra.ts";

export const LEGACY_ENABLE_PGSQL_CHECK = "CREATE EXTENSION IF NOT EXISTS plpgsql_check";

export const LEGACY_CHECK_SCHEMA_SCRIPT = `-- Ref: https://github.com/okbob/plpgsql_check#mass-check
Expand All @@ -20,17 +25,6 @@ JOIN pg_catalog.pg_language l ON p.prolang = l.oid
WHERE l.lanname = 'plpgsql' AND p.prorettype <> 2279 AND n.nspname = $1::text;
`;

export const LEGACY_LIST_SCHEMAS_SQL = `-- List user defined schemas, excluding
-- Extension created schemas
-- Supabase managed schemas
select pn.nspname
from pg_namespace pn
left join pg_depend pd on pd.objid = pn.oid
where pd.deptype is null
and not pn.nspname like any($1)
and pn.nspowner::regrole::text != 'supabase_admin'
order by pn.nspname`;

/**
* Postgres-managed schemas excluded from the user-schema listing. These are
* `LIKE` patterns bound as the `$1` text[] parameter — the `\_` / `pg\_%`
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/legacy/commands/db/reset/SIDE_EFFECTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ child) is fully native as of CLI-1958.

| Statement | When |
| ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- |
| `drop.sql` `DO` block (drops user schemas/extensions/public objects, truncates auth/migrations) | always, first |
| `legacyDropObjectsSql` `DO` block (drops user schemas/extensions/public objects, truncates auth/migrations) | always, first |
| `SELECT vault.update_secret(...)` / `vault.create_secret(...)` | when `[db.vault]` has syncable secrets |
| schema-file statements (no history bookkeeping, no `RESET ALL` between files) | `--experimental` + no resolved version + pg-delta not enabled (see Notes) |
| migration statements + `schema_migrations` history insert (per file, transactional; pipeline-incompatible statements run standalone — see Notes) | otherwise, when `[db.migrations].enabled`, for migrations `≤ --version` |
Expand Down
161 changes: 9 additions & 152 deletions apps/cli/src/legacy/commands/db/shared/legacy-drop-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,168 +2,25 @@ import { Effect } from "effect";

import type { LegacyDbExecError } from "../../../shared/legacy-db-connection.errors.ts";
import type { LegacyDbSession } from "../../../shared/legacy-db-connection.service.ts";
import { legacyDropObjectsSql } from "../../../shared/legacy-drop-objects.ts";

/**
* Verbatim port of Go's embedded `pkg/migration/queries/drop.sql`
* (`DropUserSchemas`). A single PL/pgSQL `DO` block that drops user schemas,
* extensions, public-schema objects, and non-managed publications, then
* truncates the auth / supabase_functions / supabase_migrations tables. Run as a
* single simple-query statement, matching Go's one-statement `ExecBatch`.
*/
const DROP_OBJECTS = `do $$ declare
rec record;
begin
-- schemas
for rec in
select pn.*
from pg_namespace pn
left join pg_depend pd on pd.objid = pn.oid
where pd.deptype is null
and not pn.nspname like any(array['information\\_schema', 'pg\\_%', '\\_analytics', '\\_realtime', '\\_supavisor', 'pgbouncer', 'pgmq', 'pgsodium', 'pgtle', 'supabase\\_migrations', 'vault', 'extensions', 'public'])
and pn.nspowner::regrole::text != 'supabase_admin'
loop
-- If an extension uses a schema it doesn't create, dropping the schema will cascade to also
-- drop the extension. But if an extension creates its own schema, dropping the schema will
-- throw an error. Hence, we drop schemas first while excluding those created by extensions.
raise notice 'dropping schema: %', rec.nspname;
execute format('drop schema if exists %I cascade', rec.nspname);
end loop;

-- extensions
for rec in
select *
from pg_extension p
where p.extname not in ('pg_graphql', 'pg_net', 'pg_stat_statements', 'pgcrypto', 'pgjwt', 'pgsodium', 'plpgsql', 'supabase_vault', 'uuid-ossp')
loop
raise notice 'dropping extension: %', rec.extname;
execute format('drop extension if exists %I cascade', rec.extname);
end loop;

-- functions
for rec in
select *
from pg_proc p
where p.pronamespace::regnamespace::name = 'public'
loop
-- supports aggregate, function, and procedure
raise notice 'dropping function: %.%', rec.pronamespace::regnamespace::name, rec.proname;
execute format('drop routine if exists %I.%I(%s) cascade', rec.pronamespace::regnamespace::name, rec.proname, pg_catalog.pg_get_function_identity_arguments(rec.oid));
end loop;

-- views (necessary for views referencing objects in Supabase-managed schemas)
for rec in
select *
from pg_class c
where
c.relnamespace::regnamespace::name = 'public'
and c.relkind = 'v'
loop
raise notice 'dropping view: %.%', rec.relnamespace::regnamespace::name, rec.relname;
execute format('drop view if exists %I.%I cascade', rec.relnamespace::regnamespace::name, rec.relname);
end loop;

-- materialized views (necessary for materialized views referencing objects in Supabase-managed schemas)
for rec in
select *
from pg_class c
where
c.relnamespace::regnamespace::name = 'public'
and c.relkind = 'm'
loop
raise notice 'dropping materialized view: %.%', rec.relnamespace::regnamespace::name, rec.relname;
execute format('drop materialized view if exists %I.%I cascade', rec.relnamespace::regnamespace::name, rec.relname);
end loop;

-- tables (cascade to dependent objects)
for rec in
select *
from pg_class c
where
c.relnamespace::regnamespace::name = 'public'
and c.relkind not in ('c', 'S', 'v', 'm')
order by c.relkind desc
loop
-- supports all table like relations, except views, complex types, and sequences
raise notice 'dropping table: %.%', rec.relnamespace::regnamespace::name, rec.relname;
execute format('drop table if exists %I.%I cascade', rec.relnamespace::regnamespace::name, rec.relname);
end loop;

-- truncate tables in auth, webhooks, and migrations schema
for rec in
select *
from pg_class c
where
(c.relnamespace::regnamespace::name = 'auth' and c.relname != 'schema_migrations'
or c.relnamespace::regnamespace::name = 'supabase_functions' and c.relname != 'migrations'
or c.relnamespace::regnamespace::name = 'supabase_migrations')
and c.relkind = 'r'
loop
raise notice 'truncating table: %.%', rec.relnamespace::regnamespace::name, rec.relname;
execute format('truncate %I.%I cascade', rec.relnamespace::regnamespace::name, rec.relname);
end loop;

-- sequences
for rec in
select *
from pg_class c
where
c.relnamespace::regnamespace::name = 'public'
and c.relkind = 's'
loop
raise notice 'dropping sequence: %.%', rec.relnamespace::regnamespace::name, rec.relname;
execute format('drop sequence if exists %I.%I cascade', rec.relnamespace::regnamespace::name, rec.relname);
end loop;

-- types
for rec in
select *
from pg_type t
where
t.typnamespace::regnamespace::name = 'public'
and typtype != 'b'
loop
raise notice 'dropping type: %.%', rec.typnamespace::regnamespace::name, rec.typname;
execute format('drop type if exists %I.%I cascade', rec.typnamespace::regnamespace::name, rec.typname);
end loop;

-- policies
for rec in
select *
from pg_policies p
loop
raise notice 'dropping policy: %', rec.policyname;
execute format('drop policy if exists %I on %I.%I cascade', rec.policyname, rec.schemaname, rec.tablename);
end loop;

-- publications
for rec in
select *
from pg_publication p
where
not p.pubname like any(array['supabase\\_realtime%', 'realtime\\_messages%'])
loop
raise notice 'dropping publication: %', rec.pubname;
execute format('drop publication if exists %I', rec.pubname);
end loop;
end $$;`;

/**
* Drops all user-created database objects, mirroring Go's
* `migration.DropUserSchemas` (`pkg/migration/drop.go:34-38`): the `drop.sql` `DO`
* block runs as a single transactional statement (no migration-history row).
* Drops all user-created database objects for `db reset`'s remote (`--db-url`)
* path: runs the shared `legacyDropObjectsSql` `DO` block inside an
* explicit transaction, mapping failures through the caller's error
* constructor (no migration-history row).
*/
export const legacyDropUserSchemas = <E>(
session: LegacyDbSession,
mapError: (message: string) => E,
): Effect.Effect<void, E> =>
Effect.gen(function* () {
// Go's `DropUserSchemas` runs only `drop.sql` via `ExecBatch` (drop.go:34-38) —
// no `RESET ALL`. Resetting here would clear caller-supplied DB URL runtime
// params (e.g. `options=-c statement_timeout=…`) before the destructive drop, so
// the remote `db reset --db-url` path must NOT reset (matches Go's ExecBatch).
// No `RESET ALL` before the drop: resetting would clear caller-supplied DB
// URL runtime params (e.g. `options=-c statement_timeout=…`) on the remote
// `db reset --db-url` path before the destructive statement runs.
yield* session.exec("BEGIN");
yield* session
.exec(DROP_OBJECTS)
.exec(legacyDropObjectsSql)
.pipe(Effect.tapError(() => session.exec("ROLLBACK").pipe(Effect.ignore)));
yield* session.exec("COMMIT");
}).pipe(Effect.mapError((error: LegacyDbExecError) => mapError(error.message)));
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";

import { legacyDropObjectsSql } from "../../../shared/legacy-drop-objects.ts";
import { LEGACY_EDGE_RUNTIME_SCRIPT_ERROR_SENTINEL } from "../../../shared/legacy-edge-runtime-script.service.ts";
import {
legacyMigraDiffScript,
legacyMigraDiffShellScript,
} from "./legacy-migra.deno-templates.ts";
import { legacyListSchemasSql } from "./legacy-migra.ts";

// Resolve the Go template sources relative to this file so the byte-equality
// assertion fails loudly if the embedded copies drift from upstream.
Expand All @@ -25,3 +27,25 @@ describe("embedded migra templates", () => {
expect(legacyMigraDiffScript).toContain(LEGACY_EDGE_RUNTIME_SCRIPT_ERROR_SENTINEL);
});
});

describe("embedded user-schema queries", () => {
// An unscoped pg_depend anti-join hid user schemas whose oid collided with a
// row in another catalog (supabase/cli#6375).
it.each([
["legacyListSchemasSql", legacyListSchemasSql],
["legacyDropObjectsSql", legacyDropObjectsSql],
])(
"%s constrains the pg_depend anti-join to pg_namespace rows (supabase/cli#6375)",
(_name, sql) => {
// normalize whitespace so a cosmetic re-wrap of the join cannot fail this
const normalized = sql.replaceAll(/\s+/gu, " ");
const joins = normalized.match(/pd\.objid = pn\.oid/gu) ?? [];
const constrained =
normalized.match(
/pd\.objid = pn\.oid and pd\.classid = 'pg_catalog\.pg_namespace'::regclass/gu,
) ?? [];
expect(joins.length).toBeGreaterThan(0);
expect(constrained).toHaveLength(joins.length);
},
);
});
33 changes: 19 additions & 14 deletions apps/cli/src/legacy/commands/db/shared/legacy-migra.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,20 @@ const LEGACY_LIST_SCHEMAS_EXCLUDE: ReadonlyArray<string> = [
"vault",
];

/** Verbatim from Go's `migration.ListSchemas` (`pkg/migration/queries/list.sql`). */
const LEGACY_LIST_SCHEMAS_SQL = `-- List user defined schemas, excluding
/**
* Lists user-defined schemas, excluding extension-created ones via a
* `pg_depend` anti-join scoped by `classid` to `pg_namespace` rows (an oid
* collision with another catalog must not hide a schema — supabase/cli#6375),
* Supabase-managed names via the `$1` LIKE patterns, and schemas owned by
* `supabase_admin`. Shared by the migra bash fallback and `db lint`
* (`lint.lint-sql.ts`).
*/
export const legacyListSchemasSql = `-- List user defined schemas, excluding
-- Extension created schemas
-- Supabase managed schemas
select pn.nspname
from pg_namespace pn
left join pg_depend pd on pd.objid = pn.oid
from pg_catalog.pg_namespace pn
left join pg_catalog.pg_depend pd on pd.objid = pn.oid and pd.classid = 'pg_catalog.pg_namespace'::regclass
Comment thread
7ttp marked this conversation as resolved.
Comment thread
7ttp marked this conversation as resolved.
where pd.deptype is null
and not pn.nspname like any($1)
and pn.nspowner::regrole::text != 'supabase_admin'
Expand Down Expand Up @@ -162,16 +169,14 @@ const loadTargetUserSchemas = Effect.fnUntraced(function* (
}),
),
);
const rows = yield* session
.query(LEGACY_LIST_SCHEMAS_SQL, [LEGACY_LIST_SCHEMAS_EXCLUDE])
.pipe(
Effect.mapError(
(cause) =>
new LegacyMigraSchemaLoadError({
message: `failed to list schemas: ${cause.message}`,
}),
),
);
const rows = yield* session.query(legacyListSchemasSql, [LEGACY_LIST_SCHEMAS_EXCLUDE]).pipe(
Effect.mapError(
(cause) =>
new LegacyMigraSchemaLoadError({
message: `failed to list schemas: ${cause.message}`,
}),
),
);
return rows.map((row) => String(row["nspname"]));
}),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
### `--output-format text`

Prints `Resetting database to version: <version>` to stderr, then drops every
user schema/object (the bundled `drop.sql` DO-block), upserts `[db.vault]`
user schema/object (the embedded `legacyDropObjectsSql` DO-block), upserts `[db.vault]`
secrets, and re-applies local migrations `<= version` plus seed files (each gated
on `db.migrations.enabled` / `db.seed.enabled`). Nothing is written to stdout.

Expand Down
25 changes: 14 additions & 11 deletions apps/cli/src/legacy/shared/legacy-drop-objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from "../../shared/telemetry/error-actionability.ts";
import type { LegacyDbSession } from "./legacy-db-connection.service.ts";

/** Dropping the user schemas failed (`DropUserSchemas` error). */
/** Dropping the user-created database objects failed. */
export class LegacyMigrationDropError extends Data.TaggedError("LegacyMigrationDropError")<{
readonly message: string;
}> {
Expand All @@ -17,19 +17,23 @@ export class LegacyMigrationDropError extends Data.TaggedError("LegacyMigrationD
}

/**
* The embedded `DO $$ ... $$` block from `pkg/migration/queries/drop.sql`,
* bundled verbatim. `migration.DropUserSchemas` runs this single statement to
* drop every user-created schema/extension/object in `public` and truncate the
* managed `auth` / `supabase_functions` / `supabase_migrations` tables.
* A single PL/pgSQL `DO` block that drops user schemas, non-managed
* extensions, and `public`-schema objects, drops every RLS policy and every
* non-Supabase publication database-wide (not just in `public`), then
* truncates the managed `auth` / `supabase_functions` / `supabase_migrations`
* tables. The schema loop anti-joins `pg_depend` scoped by `classid` to
* `pg_namespace` rows so an oid collision with another catalog cannot hide a
* user schema (supabase/cli#6375). Shared by `migration down` and `db reset`
* (`legacy-drop-schemas.ts`).
*/
const LEGACY_DROP_OBJECTS_SQL = `do $$ declare
export const legacyDropObjectsSql = `do $$ declare
rec record;
begin
-- schemas
for rec in
select pn.*
from pg_namespace pn
left join pg_depend pd on pd.objid = pn.oid
from pg_catalog.pg_namespace pn
left join pg_catalog.pg_depend pd on pd.objid = pn.oid and pd.classid = 'pg_catalog.pg_namespace'::regclass
where pd.deptype is null
and not pn.nspname like any(array['information\\_schema', 'pg\\_%', '\\_analytics', '\\_realtime', '\\_supavisor', 'pgbouncer', 'pgmq', 'pgsodium', 'pgtle', 'supabase\\_migrations', 'vault', 'extensions', 'public'])
and pn.nspowner::regrole::text != 'supabase_admin'
Expand Down Expand Up @@ -161,11 +165,10 @@ end $$;
`;

/**
* Drops every user-created object, matching `migration.DropUserSchemas`:
* one batched DO-block statement (a single
* Drops every user-created object as one DO-block statement (a single
* statement is atomic in Postgres, so no explicit transaction is needed).
*/
export const legacyDropUserSchemas = (session: LegacyDbSession) =>
session
.exec(LEGACY_DROP_OBJECTS_SQL)
.exec(legacyDropObjectsSql)
.pipe(Effect.mapError((cause) => new LegacyMigrationDropError({ message: cause.message })));
Loading