From 20928bfcefb81860af952330e5e50244bcd20456 Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:24:43 +0530 Subject: [PATCH 1/7] fix(cli): drop reserved-role parameter grants in db dump --role-only --- .../pg-dump.run.integration.test.ts | 24 +++++++++++++++++++ apps/cli/src/command-internal/pg-dump.run.ts | 2 ++ .../src/command-internal/pg-dump.scripts.ts | 2 +- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/apps/cli/src/command-internal/pg-dump.run.integration.test.ts b/apps/cli/src/command-internal/pg-dump.run.integration.test.ts index 37b85cfeb6..93015f4f06 100644 --- a/apps/cli/src/command-internal/pg-dump.run.integration.test.ts +++ b/apps/cli/src/command-internal/pg-dump.run.integration.test.ts @@ -67,6 +67,30 @@ describe("managed pg_dump runner", () => { { RESERVED_ROLES: "dashboard_user", EXTRA_SED: "/^--/d" }, undefined, ], + [ + 'GRANT SET ON PARAMETER "log_min_messages" TO "supabase_realtime_admin";', + "pg_dumpall --roles-only", + { RESERVED_ROLES: "supabase_realtime_admin" }, + '-- GRANT SET ON PARAMETER "log_min_messages" TO "supabase_realtime_admin";', + ], + [ + 'GRANT ALL ON PARAMETER "log_min_messages" TO "supabase_admin" WITH GRANT OPTION;', + "pg_dumpall --roles-only", + { RESERVED_ROLES: "supabase_.*" }, + '-- GRANT ALL ON PARAMETER "log_min_messages" TO "supabase_admin" WITH GRANT OPTION;', + ], + [ + 'GRANT SET ON PARAMETER "log_min_messages" TO "app_user";', + "pg_dumpall --roles-only", + { RESERVED_ROLES: "supabase_.*" }, + 'GRANT SET ON PARAMETER "log_min_messages" TO "app_user";', + ], + [ + `ALTER ROLE "app_user" SET "myapp.restore_sql" TO 'GRANT SET ON PARAMETER "log_min_messages" TO "postgres";';`, + "pg_dumpall --roles-only", + { RESERVED_ROLES: "postgres" }, + `ALTER ROLE "app_user" SET "myapp.restore_sql" TO 'GRANT SET ON PARAMETER "log_min_messages" TO "postgres";';`, + ], [ 'CREATE EXTENSION IF NOT EXISTS "pg_tle" WITH SCHEMA "pgtle";', "pg_dump --schema-only", diff --git a/apps/cli/src/command-internal/pg-dump.run.ts b/apps/cli/src/command-internal/pg-dump.run.ts index 8232be1e4a..f8bc73cc1b 100644 --- a/apps/cli/src/command-internal/pg-dump.run.ts +++ b/apps/cli/src/command-internal/pg-dump.run.ts @@ -249,6 +249,8 @@ const transformDumpLine = ( return dropComments(allowedConfig.test(line) ? line : `-- ${line}`); if (new RegExp(`GRANT ".*" TO "(${reservedRoles})"`, "u").test(line)) return dropComments(`-- ${line}`); + if (new RegExp(`^GRANT [A-Z ,]+ ON PARAMETER ".*" TO "(${reservedRoles})"`, "u").test(line)) + return dropComments(`-- ${line}`); if (line.startsWith("-- ") && allowedConfig.test(line.slice(3))) return dropComments(line.slice(3)); if (line.startsWith("-- ") && line.includes(" GRANT ")) return dropComments(line); diff --git a/apps/cli/src/command-internal/pg-dump.scripts.ts b/apps/cli/src/command-internal/pg-dump.scripts.ts index cbce14c3df..03c3f6f061 100644 --- a/apps/cli/src/command-internal/pg-dump.scripts.ts +++ b/apps/cli/src/command-internal/pg-dump.scripts.ts @@ -8,4 +8,4 @@ export const dumpDataScript = '#!/usr/bin/env bash\nset -euo pipefail\n\nexport PGHOST="$PGHOST"\nexport PGPORT="$PGPORT"\nexport PGUSER="$PGUSER"\nexport PGPASSWORD="$PGPASSWORD"\nexport PGDATABASE="$PGDATABASE"\n\n# Disable triggers so that data dump can be restored exactly as it is\necho "SET session_replication_role = replica;\n"\n\n# Explanation of pg_dump flags:\n#\n# --exclude-schema omit data from internal schemas as they are maintained by platform\n# --exclude-table omit data from migration history tables as they are managed by platform\n# --column-inserts only column insert syntax is supported, ie. no copy from stdin\n# --schema \'*\' include all other schemas by default\n#\n# Explanation of sed substitutions:\n#\n# - do not emit psql meta commands\n#\n# Never delete SQL comments because multiline records may begin with them.\npg_dump \\\n --data-only \\\n --quote-all-identifier \\\n --role "postgres" \\\n --exclude-schema "${EXCLUDED_SCHEMAS:-}" \\\n --exclude-table "auth.schema_migrations" \\\n --exclude-table "storage.migrations" \\\n --exclude-table "supabase_functions.migrations" \\\n --schema "$INCLUDED_SCHEMAS" \\\n ${EXTRA_FLAGS:-} \\\n| sed -E \'s/^\\\\(un)?restrict .*$/-- &/\'\n\n# Reset session config generated by pg_dump\necho "RESET ALL;"\n'; export const dumpRoleScript = - '#!/usr/bin/env bash\nset -euo pipefail\n\nexport PGHOST="$PGHOST"\nexport PGPORT="$PGPORT"\nexport PGUSER="$PGUSER"\nexport PGPASSWORD="$PGPASSWORD"\nexport PGDATABASE="$PGDATABASE"\n\n# Explanation of pg_dumpall flags:\n#\n# --roles-only only include create, alter, and grant role statements\n#\n# Explanation of sed substitutions:\n#\n# - do not emit psql meta commands\n# - do not create or alter reserved roles as they are blocked by supautils\n# - explicitly allow altering safe attributes, ie. statement_timeout, pgrst.*\n# - discard role attributes that require superuser, ie. nosuperuser, noreplication\n# - do not alter membership grants by supabase_admin role\npg_dumpall \\\n --roles-only \\\n --role "postgres" \\\n --quote-all-identifier \\\n --no-role-passwords \\\n --no-comments \\\n| sed -E \'s/^\\\\(un)?restrict .*$/-- &/\' \\\n| sed -E "s/^CREATE ROLE \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "s/^ALTER ROLE \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "s/ (NOSUPERUSER|NOREPLICATION)//g" \\\n| sed -E "s/^-- (.* SET \\"($ALLOWED_CONFIGS)\\" .*)/\\1/" \\\n| sed -E "s/GRANT \\".*\\" TO \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "${EXTRA_SED:-}" \\\n| uniq\n\n# Reset session config generated by pg_dump\necho "RESET ALL;"\n'; + '#!/usr/bin/env bash\nset -euo pipefail\n\nexport PGHOST="$PGHOST"\nexport PGPORT="$PGPORT"\nexport PGUSER="$PGUSER"\nexport PGPASSWORD="$PGPASSWORD"\nexport PGDATABASE="$PGDATABASE"\n\n# Explanation of pg_dumpall flags:\n#\n# --roles-only only include create, alter, and grant role statements\n#\n# Explanation of sed substitutions:\n#\n# - do not emit psql meta commands\n# - do not create or alter reserved roles as they are blocked by supautils\n# - explicitly allow altering safe attributes, ie. statement_timeout, pgrst.*\n# - discard role attributes that require superuser, ie. nosuperuser, noreplication\n# - do not alter membership grants by supabase_admin role\n# - do not grant parameter privileges to reserved roles\npg_dumpall \\\n --roles-only \\\n --role "postgres" \\\n --quote-all-identifier \\\n --no-role-passwords \\\n --no-comments \\\n| sed -E \'s/^\\\\(un)?restrict .*$/-- &/\' \\\n| sed -E "s/^CREATE ROLE \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "s/^ALTER ROLE \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "s/ (NOSUPERUSER|NOREPLICATION)//g" \\\n| sed -E "s/^-- (.* SET \\"($ALLOWED_CONFIGS)\\" .*)/\\1/" \\\n| sed -E "s/GRANT \\".*\\" TO \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "s/^GRANT [A-Z ,]+ ON PARAMETER \\".*\\" TO \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "${EXTRA_SED:-}" \\\n| uniq\n\n# Reset session config generated by pg_dump\necho "RESET ALL;"\n'; From 8c88f42fedda44c664b8c143ebe7cea074e09d64 Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Sat, 24 Oct 2026 17:01:52 +0530 Subject: [PATCH 2/7] test(cli): nit --- .../pg-dump.run.integration.test.ts | 221 ++++++++++-------- 1 file changed, 130 insertions(+), 91 deletions(-) diff --git a/apps/cli/src/command-internal/pg-dump.run.integration.test.ts b/apps/cli/src/command-internal/pg-dump.run.integration.test.ts index 93015f4f06..47821214d8 100644 --- a/apps/cli/src/command-internal/pg-dump.run.integration.test.ts +++ b/apps/cli/src/command-internal/pg-dump.run.integration.test.ts @@ -1,8 +1,10 @@ import { describe, expect, it } from "@effect/vitest"; import { BunServices } from "@effect/platform-bun"; -import { Effect, Layer, Option } from "effect"; +import { Effect, Layer, Option, Stream } from "effect"; +import { ChildProcess, ChildProcessSpawner } from "effect/process"; import type { Stack } from "@supabase/stack/effect"; import { streamPgDumpWithClient, transformManagedDumpLine } from "./pg-dump.run.ts"; +import { dumpRoleScript } from "./pg-dump.scripts.ts"; import { NetworkIdFlag } from "./global-flags.ts"; import { DockerRun } from "./docker-run.service.ts"; import { BundledPostgresClient } from "./bundled-postgres-client.ts"; @@ -32,101 +34,138 @@ const stackFixture = (output: string) => { }; describe("managed pg_dump runner", () => { - it("keeps the managed dump filters equivalent to the CLI scripts", () => { - const cases = [ - [ - 'CREATE TABLE "public" (id integer);', - "pg_dump --schema-only", - {}, - 'CREATE TABLE IF NOT EXISTS "public" (id integer);', - ], - ["\\unrestrict 123", "pg_dump --data-only", {}, "-- \\unrestrict 123"], - ["--user-data", "pg_dump --data-only", { EXTRA_SED: "/^--/d" }, "--user-data"], - ["\\restrict 123", "pg_dumpall --roles-only", { EXTRA_SED: "/^--/d" }, undefined], - [ - 'CREATE ROLE "postgres";', - "pg_dumpall --roles-only", - { RESERVED_ROLES: "postgres" }, - '-- CREATE ROLE "postgres";', - ], - [ - 'ALTER ROLE "postgres" SET "statement_timeout" TO "5s";', - "pg_dumpall --roles-only", - { ALLOWED_CONFIGS: "statement_timeout" }, - 'ALTER ROLE "postgres" SET "statement_timeout" TO "5s";', - ], - [ - '-- ALTER ROLE "postgres" SET "pgrst.db_schemas" TO "public";', - "pg_dumpall --roles-only", - { ALLOWED_CONFIGS: "pgrst.*", EXTRA_SED: "/^--/d" }, - 'ALTER ROLE "postgres" SET "pgrst.db_schemas" TO "public";', - ], - [ - 'GRANT "member" TO "dashboard_user";', - "pg_dumpall --roles-only", - { RESERVED_ROLES: "dashboard_user", EXTRA_SED: "/^--/d" }, - undefined, - ], - [ - 'GRANT SET ON PARAMETER "log_min_messages" TO "supabase_realtime_admin";', - "pg_dumpall --roles-only", - { RESERVED_ROLES: "supabase_realtime_admin" }, - '-- GRANT SET ON PARAMETER "log_min_messages" TO "supabase_realtime_admin";', - ], - [ - 'GRANT ALL ON PARAMETER "log_min_messages" TO "supabase_admin" WITH GRANT OPTION;', - "pg_dumpall --roles-only", - { RESERVED_ROLES: "supabase_.*" }, - '-- GRANT ALL ON PARAMETER "log_min_messages" TO "supabase_admin" WITH GRANT OPTION;', - ], - [ - 'GRANT SET ON PARAMETER "log_min_messages" TO "app_user";', - "pg_dumpall --roles-only", - { RESERVED_ROLES: "supabase_.*" }, - 'GRANT SET ON PARAMETER "log_min_messages" TO "app_user";', - ], - [ - `ALTER ROLE "app_user" SET "myapp.restore_sql" TO 'GRANT SET ON PARAMETER "log_min_messages" TO "postgres";';`, - "pg_dumpall --roles-only", - { RESERVED_ROLES: "postgres" }, - `ALTER ROLE "app_user" SET "myapp.restore_sql" TO 'GRANT SET ON PARAMETER "log_min_messages" TO "postgres";';`, - ], - [ - 'CREATE EXTENSION IF NOT EXISTS "pg_tle" WITH SCHEMA "pgtle";', - "pg_dump --schema-only", - {}, - 'CREATE EXTENSION IF NOT EXISTS "pg_tle";', - ], - [ - 'GRANT SELECT ON TABLE "public"."storage" TO "anon";', - "pg_dump --schema-only", - { EXCLUDED_SCHEMAS: "storage" }, - 'GRANT SELECT ON TABLE "public"."storage" TO "anon";', - ], - [ - 'REVOKE SELECT ON TABLE "storage" FROM "anon";', - "pg_dump --schema-only", - { EXCLUDED_SCHEMAS: "storage" }, - '-- REVOKE SELECT ON TABLE "storage" FROM "anon";', - ], - [ - 'GRANT ALL ON FOREIGN DATA WRAPPER "wrapper" TO "authenticated";', - "pg_dump --schema-only", - {}, - 'GRANT ALL ON FOREIGN DATA WRAPPER "wrapper" TO "authenticated";', - ], - [ - 'GRANT ALL ON FOREIGN DATA WRAPPER "wrapper" TO "postgres" WITH GRANT OPTION;', - "pg_dump --schema-only", - {}, - '-- GRANT ALL ON FOREIGN DATA WRAPPER "wrapper" TO "postgres" WITH GRANT OPTION;', - ], - ] as const; + const cases = [ + [ + 'CREATE TABLE "public" (id integer);', + "pg_dump --schema-only", + {}, + 'CREATE TABLE IF NOT EXISTS "public" (id integer);', + ], + ["\\unrestrict 123", "pg_dump --data-only", {}, "-- \\unrestrict 123"], + ["--user-data", "pg_dump --data-only", { EXTRA_SED: "/^--/d" }, "--user-data"], + ["\\restrict 123", "pg_dumpall --roles-only", { EXTRA_SED: "/^--/d" }, undefined], + [ + 'CREATE ROLE "postgres";', + "pg_dumpall --roles-only", + { RESERVED_ROLES: "postgres" }, + '-- CREATE ROLE "postgres";', + ], + [ + 'ALTER ROLE "postgres" SET "statement_timeout" TO "5s";', + "pg_dumpall --roles-only", + { ALLOWED_CONFIGS: "statement_timeout" }, + 'ALTER ROLE "postgres" SET "statement_timeout" TO "5s";', + ], + [ + '-- ALTER ROLE "postgres" SET "pgrst.db_schemas" TO "public";', + "pg_dumpall --roles-only", + { ALLOWED_CONFIGS: "pgrst.*", EXTRA_SED: "/^--/d" }, + 'ALTER ROLE "postgres" SET "pgrst.db_schemas" TO "public";', + ], + [ + 'GRANT "member" TO "dashboard_user";', + "pg_dumpall --roles-only", + { RESERVED_ROLES: "dashboard_user", EXTRA_SED: "/^--/d" }, + undefined, + ], + [ + 'GRANT SET ON PARAMETER "log_min_messages" TO "supabase_realtime_admin";', + "pg_dumpall --roles-only", + { RESERVED_ROLES: "supabase_realtime_admin" }, + '-- GRANT SET ON PARAMETER "log_min_messages" TO "supabase_realtime_admin";', + ], + [ + 'GRANT ALL ON PARAMETER "log_min_messages" TO "supabase_admin" WITH GRANT OPTION;', + "pg_dumpall --roles-only", + { RESERVED_ROLES: "supabase_.*" }, + '-- GRANT ALL ON PARAMETER "log_min_messages" TO "supabase_admin" WITH GRANT OPTION;', + ], + [ + 'GRANT SET ON PARAMETER "log_min_messages" TO "app_user";', + "pg_dumpall --roles-only", + { RESERVED_ROLES: "supabase_.*" }, + 'GRANT SET ON PARAMETER "log_min_messages" TO "app_user";', + ], + [ + `ALTER ROLE "app_user" SET "myapp.restore_sql" TO 'GRANT SET ON PARAMETER "log_min_messages" TO "postgres";';`, + "pg_dumpall --roles-only", + { RESERVED_ROLES: "postgres" }, + `ALTER ROLE "app_user" SET "myapp.restore_sql" TO 'GRANT SET ON PARAMETER "log_min_messages" TO "postgres";';`, + ], + [ + 'CREATE EXTENSION IF NOT EXISTS "pg_tle" WITH SCHEMA "pgtle";', + "pg_dump --schema-only", + {}, + 'CREATE EXTENSION IF NOT EXISTS "pg_tle";', + ], + [ + 'GRANT SELECT ON TABLE "public"."storage" TO "anon";', + "pg_dump --schema-only", + { EXCLUDED_SCHEMAS: "storage" }, + 'GRANT SELECT ON TABLE "public"."storage" TO "anon";', + ], + [ + 'REVOKE SELECT ON TABLE "storage" FROM "anon";', + "pg_dump --schema-only", + { EXCLUDED_SCHEMAS: "storage" }, + '-- REVOKE SELECT ON TABLE "storage" FROM "anon";', + ], + [ + 'GRANT ALL ON FOREIGN DATA WRAPPER "wrapper" TO "authenticated";', + "pg_dump --schema-only", + {}, + 'GRANT ALL ON FOREIGN DATA WRAPPER "wrapper" TO "authenticated";', + ], + [ + 'GRANT ALL ON FOREIGN DATA WRAPPER "wrapper" TO "postgres" WITH GRANT OPTION;', + "pg_dump --schema-only", + {}, + '-- GRANT ALL ON FOREIGN DATA WRAPPER "wrapper" TO "postgres" WITH GRANT OPTION;', + ], + ] as const; + it("keeps the managed dump filters equivalent to the CLI scripts", () => { for (const [input, script, env, expected] of cases) expect(transformManagedDumpLine(input, script, env)).toBe(expected); }); + it.live.skipIf(process.platform === "win32")("filters role dumps through the CLI script", () => + Effect.gen(function* () { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + for (const [input, script, env, expected] of cases) { + if (!script.includes("pg_dumpall")) continue; + const child = yield* spawner.spawn( + ChildProcess.make( + "bash", + ["-c", `pg_dumpall() { printf '%s\\n' "$DUMP_INPUT"; }\n${dumpRoleScript}`], + { + env: { + PGHOST: "127.0.0.1", + PGPORT: "5432", + PGUSER: "postgres", + PGPASSWORD: "", + PGDATABASE: "postgres", + RESERVED_ROLES: "", + ALLOWED_CONFIGS: "", + EXTRA_SED: "", + ...env, + DUMP_INPUT: input, + }, + stdin: "ignore", + stderr: "inherit", + }, + ), + ); + const [exitCode, stdout] = yield* Effect.all( + [child.exitCode, Stream.mkString(Stream.decodeText(child.stdout))], + { concurrency: "unbounded" }, + ); + expect(exitCode).toBe(0); + expect(stdout).toBe(`${expected === undefined ? "" : `${expected}\n`}RESET ALL;\n`); + } + }).pipe(Effect.scoped, Effect.provide(BunServices.layer)), + ); + it.live("keeps data dump filters and quoted extra flags in the Stack tool arguments", () => { const fixture = stackFixture("\\unrestrict 123\n"); return streamPgDumpWithClient({ From 3566db8276353e12373fe80ad655471642e5357a Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Sat, 24 Oct 2026 17:25:49 +0530 Subject: [PATCH 3/7] fix(cli): preserve quoted content in role dumps --- .../pg-dump.run.integration.test.ts | 162 +++++++++++++----- apps/cli/src/command-internal/pg-dump.run.ts | 31 +++- .../src/command-internal/pg-dump.scripts.ts | 2 +- 3 files changed, 146 insertions(+), 49 deletions(-) diff --git a/apps/cli/src/command-internal/pg-dump.run.integration.test.ts b/apps/cli/src/command-internal/pg-dump.run.integration.test.ts index 47821214d8..d7e9171cc6 100644 --- a/apps/cli/src/command-internal/pg-dump.run.integration.test.ts +++ b/apps/cli/src/command-internal/pg-dump.run.integration.test.ts @@ -5,6 +5,7 @@ import { ChildProcess, ChildProcessSpawner } from "effect/process"; import type { Stack } from "@supabase/stack/effect"; import { streamPgDumpWithClient, transformManagedDumpLine } from "./pg-dump.run.ts"; import { dumpRoleScript } from "./pg-dump.scripts.ts"; +import { expandScript } from "./pg-dump.env.ts"; import { NetworkIdFlag } from "./global-flags.ts"; import { DockerRun } from "./docker-run.service.ts"; import { BundledPostgresClient } from "./bundled-postgres-client.ts"; @@ -12,7 +13,7 @@ import { RuntimeInfo } from "../shared/runtime/runtime-info.service.ts"; import { selection } from "../../tests/helpers/pins.ts"; import { mockOutput } from "../../tests/helpers/mocks.ts"; -const stackFixture = (output: string) => { +const stackFixture = (output: string | ReadonlyArray) => { const calls: Array<{ readonly args: ReadonlyArray; readonly command: string }> = []; const stack = { commands: { @@ -25,12 +26,35 @@ const stackFixture = (output: string) => { ) => Effect.gen(function* () { calls.push({ command: _tool.command, args: options.args ?? [] }); - yield* options.stdout(new TextEncoder().encode(output)); + yield* Effect.forEach( + typeof output === "string" ? [new TextEncoder().encode(output)] : output, + options.stdout, + { discard: true }, + ); return { jobId: "job", exitCode: 0 }; }), }, } as unknown as Stack; - return { stack, calls }; + const layer = Layer.mergeAll( + BunServices.layer, + Layer.succeed(NetworkIdFlag, Option.none()), + Layer.succeed(RuntimeInfo, { + cwd: "/tmp", + platform: "linux", + arch: "x64", + homeDir: "/tmp", + execPath: "/usr/bin/bun", + pid: 1, + }), + Layer.succeed(BundledPostgresClient, { run: () => Effect.die("unused") }), + Layer.succeed(DockerRun, { + run: () => Effect.die("unused"), + runCapture: () => Effect.die("unused"), + runStream: () => Effect.die("unused"), + }), + mockOutput().layer, + ); + return { stack, calls, layer }; }; describe("managed pg_dump runner", () => { @@ -181,27 +205,7 @@ describe("managed pg_dump runner", () => { onStdout: () => Effect.void, client: { kind: "stack", stack: fixture.stack, command: "pg_dump", pin: selection.database }, }).pipe( - Effect.provide( - Layer.mergeAll( - BunServices.layer, - Layer.succeed(NetworkIdFlag, Option.none()), - Layer.succeed(RuntimeInfo, { - cwd: "/tmp", - platform: "linux", - arch: "x64", - homeDir: "/tmp", - execPath: "/usr/bin/bun", - pid: 1, - }), - Layer.succeed(BundledPostgresClient, { run: () => Effect.die("unused") }), - Layer.succeed(DockerRun, { - run: () => Effect.die("unused"), - runCapture: () => Effect.die("unused"), - runStream: () => Effect.die("unused"), - }), - mockOutput().layer, - ), - ), + Effect.provide(fixture.layer), Effect.tap(() => Effect.sync(() => { expect(fixture.calls[0]?.args).toContain("--exclude-table"); @@ -239,27 +243,7 @@ describe("managed pg_dump runner", () => { }), client: { kind: "stack", stack: fixture.stack, command: "pg_dump", pin: selection.database }, }).pipe( - Effect.provide( - Layer.mergeAll( - BunServices.layer, - Layer.succeed(NetworkIdFlag, Option.none()), - Layer.succeed(RuntimeInfo, { - cwd: "/tmp", - platform: "linux", - arch: "x64", - homeDir: "/tmp", - execPath: "/usr/bin/bun", - pid: 1, - }), - Layer.succeed(BundledPostgresClient, { run: () => Effect.die("unused") }), - Layer.succeed(DockerRun, { - run: () => Effect.die("unused"), - runCapture: () => Effect.die("unused"), - runStream: () => Effect.die("unused"), - }), - mockOutput().layer, - ), - ), + Effect.provide(fixture.layer), Effect.asVoid, Effect.tap(() => Effect.sync(() => { @@ -275,3 +259,91 @@ describe("managed pg_dump runner", () => { ); }); }); + +describe("role dump parameter grants", () => { + const comment = "-- A comment with an unmatched quote: '\n"; + const settings = `ALTER ROLE "app'role" SET "pgrst.db_schemas" TO 'public'; +ALTER ROLE "app_user" SET "pgrst.multiline_sql" TO 'SELECT 1; +GRANT SET ON PARAMETER "log_min_messages" TO "postgres"; +SELECT 2;'; +ALTER ROLE "app_user" SET "pgrst.escaped_sql" TO E'backslash\\\\ and ''quote'' +GRANT SET ON PARAMETER "log_min_messages" TO "postgres"; +last line'; +`; + const reservedGrant = 'GRANT SET ON PARAMETER "log_min_messages" TO "supabase_realtime_admin";'; + const customGrant = 'GRANT SET ON PARAMETER "log_lock_waits" TO "app_user";'; + const quotedCustomGrant = 'GRANT SET ON PARAMETER "log_lock_waits" TO "postgres""custom";'; + const input = `${comment}${settings}${reservedGrant}\n${customGrant}\n${quotedCustomGrant}\n`; + + for (const keepComments of [false, true]) { + const env = { + PGHOST: "unused", + PGPORT: "5432", + PGUSER: "postgres", + PGPASSWORD: "", + PGDATABASE: "postgres", + RESERVED_ROLES: "postgres|supabase_.*", + ALLOWED_CONFIGS: "pgrst.*", + EXTRA_SED: keepComments ? "" : "/^--/d", + }; + const expected = `${keepComments ? comment : ""}${settings}${keepComments ? `-- ${reservedGrant}\n` : ""}${customGrant}\n${quotedCustomGrant}\nRESET ALL;\n`; + + it.live( + `preserves quoted settings across Stack chunks with keep-comments=${keepComments}`, + () => { + const bytes = new TextEncoder().encode(input); + const fixture = stackFixture( + Array.from({ length: Math.ceil(bytes.length / 7) }, (_, i) => + bytes.slice(i * 7, (i + 1) * 7), + ), + ); + return Effect.gen(function* () { + let output = ""; + const result = yield* streamPgDumpWithClient({ + image: "unused", + script: dumpRoleScript, + env, + onStdout: (chunk) => + Effect.sync(() => { + output += new TextDecoder().decode(chunk); + }), + client: { + kind: "stack", + stack: fixture.stack, + command: "pg_dumpall", + pin: selection.database, + }, + }); + expect(result.exitCode).toBe(0); + expect(output).toBe(expected); + }).pipe(Effect.provide(fixture.layer)); + }, + ); + + describe.skipIf(process.platform === "win32")("Bash script", () => { + for (const dryRun of [false, true]) { + it.live( + `preserves quoted settings with keep-comments=${keepComments}, dry-run=${dryRun}`, + () => + Effect.gen(function* () { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const script = dryRun ? expandScript(dumpRoleScript, env) : dumpRoleScript; + const child = yield* spawner.spawn( + ChildProcess.make("bash", ["-c", `pg_dumpall() { cat; }\n${script}`], { + env, + stdin: Stream.make(new TextEncoder().encode(input)), + stderr: "inherit", + }), + ); + const [exitCode, output] = yield* Effect.all( + [child.exitCode, Stream.mkString(Stream.decodeText(child.stdout))], + { concurrency: "unbounded" }, + ); + expect(exitCode).toBe(0); + expect(output).toBe(expected); + }).pipe(Effect.scoped, Effect.provide(BunServices.layer)), + ); + } + }); + } +}); diff --git a/apps/cli/src/command-internal/pg-dump.run.ts b/apps/cli/src/command-internal/pg-dump.run.ts index f8bc73cc1b..4a412d32e2 100644 --- a/apps/cli/src/command-internal/pg-dump.run.ts +++ b/apps/cli/src/command-internal/pg-dump.run.ts @@ -232,6 +232,7 @@ const transformDumpLine = ( line: string, script: string, env: Readonly>, + inQuotedSql = false, ): string | undefined => { const applyExtraSed = (value: string | undefined): string | undefined => env["EXTRA_SED"] === "/^--/d" && value?.startsWith("--") ? undefined : value; @@ -249,7 +250,12 @@ const transformDumpLine = ( return dropComments(allowedConfig.test(line) ? line : `-- ${line}`); if (new RegExp(`GRANT ".*" TO "(${reservedRoles})"`, "u").test(line)) return dropComments(`-- ${line}`); - if (new RegExp(`^GRANT [A-Z ,]+ ON PARAMETER ".*" TO "(${reservedRoles})"`, "u").test(line)) + if ( + !inQuotedSql && + new RegExp(`^GRANT [A-Z ,]+ ON PARAMETER ".*" TO "(${reservedRoles})"([ ;]|$)`, "u").test( + line, + ) + ) return dropComments(`-- ${line}`); if (line.startsWith("-- ") && allowedConfig.test(line.slice(3))) return dropComments(line.slice(3)); @@ -306,6 +312,25 @@ const stackDumpStdout = ( const encoder = new TextEncoder(); let carry = ""; let previous: string | undefined; + let quote = ""; + const transformLine = (line: string) => { + const inQuotedSql = quote !== ""; + // pg_dumpall doubles quotes in literals and identifiers, including E-strings. + if (script.includes("pg_dumpall")) { + for (let i = 0; i < line.length; i++) { + const char = line[i]; + const next = line[i + 1]; + if (quote !== "") { + if (char === quote) { + if (next === quote) i++; + else quote = ""; + } + } else if (char === "'" || char === '"') quote = char; + else if (char === "-" && next === "-") break; + } + } + return transformDumpLine(line, script, env, inQuotedSql); + }; const onChunk = (bytes: Uint8Array) => Effect.suspend(() => { carry += decoder.decode(bytes, { stream: true }); @@ -314,7 +339,7 @@ const stackDumpStdout = ( return Effect.forEach( lines, (line) => { - const transformed = transformDumpLine(line, script, env); + const transformed = transformLine(line); if ( transformed === undefined || (script.includes("pg_dumpall") && transformed === previous) @@ -329,7 +354,7 @@ const stackDumpStdout = ( const flush = () => Effect.suspend(() => { carry += decoder.decode(); - const final = transformDumpLine(carry, script, env); + const final = transformLine(carry); return final === undefined || final.length === 0 || (script.includes("pg_dumpall") && final === previous) diff --git a/apps/cli/src/command-internal/pg-dump.scripts.ts b/apps/cli/src/command-internal/pg-dump.scripts.ts index 03c3f6f061..fdb5c783b7 100644 --- a/apps/cli/src/command-internal/pg-dump.scripts.ts +++ b/apps/cli/src/command-internal/pg-dump.scripts.ts @@ -8,4 +8,4 @@ export const dumpDataScript = '#!/usr/bin/env bash\nset -euo pipefail\n\nexport PGHOST="$PGHOST"\nexport PGPORT="$PGPORT"\nexport PGUSER="$PGUSER"\nexport PGPASSWORD="$PGPASSWORD"\nexport PGDATABASE="$PGDATABASE"\n\n# Disable triggers so that data dump can be restored exactly as it is\necho "SET session_replication_role = replica;\n"\n\n# Explanation of pg_dump flags:\n#\n# --exclude-schema omit data from internal schemas as they are maintained by platform\n# --exclude-table omit data from migration history tables as they are managed by platform\n# --column-inserts only column insert syntax is supported, ie. no copy from stdin\n# --schema \'*\' include all other schemas by default\n#\n# Explanation of sed substitutions:\n#\n# - do not emit psql meta commands\n#\n# Never delete SQL comments because multiline records may begin with them.\npg_dump \\\n --data-only \\\n --quote-all-identifier \\\n --role "postgres" \\\n --exclude-schema "${EXCLUDED_SCHEMAS:-}" \\\n --exclude-table "auth.schema_migrations" \\\n --exclude-table "storage.migrations" \\\n --exclude-table "supabase_functions.migrations" \\\n --schema "$INCLUDED_SCHEMAS" \\\n ${EXTRA_FLAGS:-} \\\n| sed -E \'s/^\\\\(un)?restrict .*$/-- &/\'\n\n# Reset session config generated by pg_dump\necho "RESET ALL;"\n'; export const dumpRoleScript = - '#!/usr/bin/env bash\nset -euo pipefail\n\nexport PGHOST="$PGHOST"\nexport PGPORT="$PGPORT"\nexport PGUSER="$PGUSER"\nexport PGPASSWORD="$PGPASSWORD"\nexport PGDATABASE="$PGDATABASE"\n\n# Explanation of pg_dumpall flags:\n#\n# --roles-only only include create, alter, and grant role statements\n#\n# Explanation of sed substitutions:\n#\n# - do not emit psql meta commands\n# - do not create or alter reserved roles as they are blocked by supautils\n# - explicitly allow altering safe attributes, ie. statement_timeout, pgrst.*\n# - discard role attributes that require superuser, ie. nosuperuser, noreplication\n# - do not alter membership grants by supabase_admin role\n# - do not grant parameter privileges to reserved roles\npg_dumpall \\\n --roles-only \\\n --role "postgres" \\\n --quote-all-identifier \\\n --no-role-passwords \\\n --no-comments \\\n| sed -E \'s/^\\\\(un)?restrict .*$/-- &/\' \\\n| sed -E "s/^CREATE ROLE \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "s/^ALTER ROLE \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "s/ (NOSUPERUSER|NOREPLICATION)//g" \\\n| sed -E "s/^-- (.* SET \\"($ALLOWED_CONFIGS)\\" .*)/\\1/" \\\n| sed -E "s/GRANT \\".*\\" TO \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "s/^GRANT [A-Z ,]+ ON PARAMETER \\".*\\" TO \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "${EXTRA_SED:-}" \\\n| uniq\n\n# Reset session config generated by pg_dump\necho "RESET ALL;"\n'; + '#!/usr/bin/env bash\nset -euo pipefail\n\nexport PGHOST="$PGHOST"\nexport PGPORT="$PGPORT"\nexport PGUSER="$PGUSER"\nexport PGPASSWORD="$PGPASSWORD"\nexport PGDATABASE="$PGDATABASE"\n\n# Explanation of pg_dumpall flags:\n#\n# --roles-only only include create, alter, and grant role statements\n#\n# Explanation of sed substitutions:\n#\n# - do not emit psql meta commands\n# - do not create or alter reserved roles as they are blocked by supautils\n# - explicitly allow altering safe attributes, ie. statement_timeout, pgrst.*\n# - discard role attributes that require superuser, ie. nosuperuser, noreplication\n# - do not alter membership grants by supabase_admin role\n# - do not grant parameter privileges to reserved roles outside quoted SQL\npg_dumpall \\\n --roles-only \\\n --role "postgres" \\\n --quote-all-identifier \\\n --no-role-passwords \\\n --no-comments \\\n| awk -v reserved_roles="$RESERVED_ROLES" \'\n# Parenthesized field references survive the CLI dry-run expander.\nBEGIN { single_quote = sprintf("%c", 39) }\n{\n line = $(0)\n if (quote == "" && line ~ ("^GRANT [A-Z ,]+ ON PARAMETER \\".*\\" TO \\"(" reserved_roles ")\\"([ ;]|$)"))\n $(0) = "-- " line\n print\n for (i = 1; i <= length(line); i++) {\n char = substr(line, i, 1)\n next_char = substr(line, i + 1, 1)\n if (quote != "") {\n if (char == quote) {\n if (next_char == quote) i++\n else quote = ""\n }\n } else if (char == single_quote || char == "\\"") quote = char\n else if (char == "-" && next_char == "-") break\n }\n}\' \\\n| sed -E \'s/^\\\\(un)?restrict .*$/-- &/\' \\\n| sed -E "s/^CREATE ROLE \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "s/^ALTER ROLE \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "s/ (NOSUPERUSER|NOREPLICATION)//g" \\\n| sed -E "s/^-- (.* SET \\"($ALLOWED_CONFIGS)\\" .*)/\\1/" \\\n| sed -E "s/GRANT \\".*\\" TO \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "${EXTRA_SED:-}" \\\n| uniq\n\n# Reset session config generated by pg_dump\necho "RESET ALL;"\n'; From 2ce5542a3455649e83c38b26874516ac649b7d40 Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:23:45 +0530 Subject: [PATCH 4/7] test(cli): cover adjacent escaped role quotes --- apps/cli/src/command-internal/pg-dump.run.integration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/cli/src/command-internal/pg-dump.run.integration.test.ts b/apps/cli/src/command-internal/pg-dump.run.integration.test.ts index d7e9171cc6..1f5d5487e7 100644 --- a/apps/cli/src/command-internal/pg-dump.run.integration.test.ts +++ b/apps/cli/src/command-internal/pg-dump.run.integration.test.ts @@ -266,7 +266,7 @@ describe("role dump parameter grants", () => { ALTER ROLE "app_user" SET "pgrst.multiline_sql" TO 'SELECT 1; GRANT SET ON PARAMETER "log_min_messages" TO "postgres"; SELECT 2;'; -ALTER ROLE "app_user" SET "pgrst.escaped_sql" TO E'backslash\\\\ and ''quote'' +ALTER ROLE "app_user" SET "pgrst.escaped_sql" TO E'backslash\\\\''quote'' GRANT SET ON PARAMETER "log_min_messages" TO "postgres"; last line'; `; From bc93f7d0b739f44c9603db0a61aca7251a7c65c3 Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:26:07 +0530 Subject: [PATCH 5/7] fix(cli): scan role dumps in C locale --- .../pg-dump.run.integration.test.ts | 41 +++++++++++++++++++ .../src/command-internal/pg-dump.scripts.ts | 2 +- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/apps/cli/src/command-internal/pg-dump.run.integration.test.ts b/apps/cli/src/command-internal/pg-dump.run.integration.test.ts index 1f5d5487e7..0db5230d50 100644 --- a/apps/cli/src/command-internal/pg-dump.run.integration.test.ts +++ b/apps/cli/src/command-internal/pg-dump.run.integration.test.ts @@ -343,6 +343,47 @@ last line'; expect(output).toBe(expected); }).pipe(Effect.scoped, Effect.provide(BunServices.layer)), ); + + it.live( + `preserves role setting bytes with keep-comments=${keepComments}, dry-run=${dryRun}`, + () => + Effect.gen(function* () { + const encoder = new TextEncoder(); + const setting = Uint8Array.from([ + ...encoder.encode('ALTER ROLE "app_user" SET "myapp.bytes" TO \'café'), + 0xff, + ...encoder.encode("';\n"), + ]); + const bytes = Uint8Array.from([...setting, ...encoder.encode(`${reservedGrant}\n`)]); + const expectedBytes = Uint8Array.from([ + ...setting, + ...encoder.encode(`${keepComments ? `-- ${reservedGrant}\n` : ""}RESET ALL;\n`), + ]); + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const script = dryRun ? expandScript(dumpRoleScript, env) : dumpRoleScript; + /* Keep sed byte-oriented so its existing locale behavior cannot mask awk's. */ + const child = yield* spawner.spawn( + ChildProcess.make( + "bash", + ["-c", `pg_dumpall() { cat; }\nsed() { LC_ALL=C command sed "$@"; }\n${script}`], + { + env: { + ...env, + LC_ALL: process.platform === "darwin" ? "en_US.UTF-8" : "C.UTF-8", + }, + stdin: Stream.make(bytes), + stderr: "inherit", + }, + ), + ); + const [exitCode, chunks] = yield* Effect.all( + [child.exitCode, Stream.runCollect(child.stdout)], + { concurrency: "unbounded" }, + ); + expect(exitCode).toBe(0); + expect(Uint8Array.from(chunks.flatMap((chunk) => [...chunk]))).toEqual(expectedBytes); + }).pipe(Effect.scoped, Effect.provide(BunServices.layer)), + ); } }); } diff --git a/apps/cli/src/command-internal/pg-dump.scripts.ts b/apps/cli/src/command-internal/pg-dump.scripts.ts index fdb5c783b7..1d9bd0759e 100644 --- a/apps/cli/src/command-internal/pg-dump.scripts.ts +++ b/apps/cli/src/command-internal/pg-dump.scripts.ts @@ -8,4 +8,4 @@ export const dumpDataScript = '#!/usr/bin/env bash\nset -euo pipefail\n\nexport PGHOST="$PGHOST"\nexport PGPORT="$PGPORT"\nexport PGUSER="$PGUSER"\nexport PGPASSWORD="$PGPASSWORD"\nexport PGDATABASE="$PGDATABASE"\n\n# Disable triggers so that data dump can be restored exactly as it is\necho "SET session_replication_role = replica;\n"\n\n# Explanation of pg_dump flags:\n#\n# --exclude-schema omit data from internal schemas as they are maintained by platform\n# --exclude-table omit data from migration history tables as they are managed by platform\n# --column-inserts only column insert syntax is supported, ie. no copy from stdin\n# --schema \'*\' include all other schemas by default\n#\n# Explanation of sed substitutions:\n#\n# - do not emit psql meta commands\n#\n# Never delete SQL comments because multiline records may begin with them.\npg_dump \\\n --data-only \\\n --quote-all-identifier \\\n --role "postgres" \\\n --exclude-schema "${EXCLUDED_SCHEMAS:-}" \\\n --exclude-table "auth.schema_migrations" \\\n --exclude-table "storage.migrations" \\\n --exclude-table "supabase_functions.migrations" \\\n --schema "$INCLUDED_SCHEMAS" \\\n ${EXTRA_FLAGS:-} \\\n| sed -E \'s/^\\\\(un)?restrict .*$/-- &/\'\n\n# Reset session config generated by pg_dump\necho "RESET ALL;"\n'; export const dumpRoleScript = - '#!/usr/bin/env bash\nset -euo pipefail\n\nexport PGHOST="$PGHOST"\nexport PGPORT="$PGPORT"\nexport PGUSER="$PGUSER"\nexport PGPASSWORD="$PGPASSWORD"\nexport PGDATABASE="$PGDATABASE"\n\n# Explanation of pg_dumpall flags:\n#\n# --roles-only only include create, alter, and grant role statements\n#\n# Explanation of sed substitutions:\n#\n# - do not emit psql meta commands\n# - do not create or alter reserved roles as they are blocked by supautils\n# - explicitly allow altering safe attributes, ie. statement_timeout, pgrst.*\n# - discard role attributes that require superuser, ie. nosuperuser, noreplication\n# - do not alter membership grants by supabase_admin role\n# - do not grant parameter privileges to reserved roles outside quoted SQL\npg_dumpall \\\n --roles-only \\\n --role "postgres" \\\n --quote-all-identifier \\\n --no-role-passwords \\\n --no-comments \\\n| awk -v reserved_roles="$RESERVED_ROLES" \'\n# Parenthesized field references survive the CLI dry-run expander.\nBEGIN { single_quote = sprintf("%c", 39) }\n{\n line = $(0)\n if (quote == "" && line ~ ("^GRANT [A-Z ,]+ ON PARAMETER \\".*\\" TO \\"(" reserved_roles ")\\"([ ;]|$)"))\n $(0) = "-- " line\n print\n for (i = 1; i <= length(line); i++) {\n char = substr(line, i, 1)\n next_char = substr(line, i + 1, 1)\n if (quote != "") {\n if (char == quote) {\n if (next_char == quote) i++\n else quote = ""\n }\n } else if (char == single_quote || char == "\\"") quote = char\n else if (char == "-" && next_char == "-") break\n }\n}\' \\\n| sed -E \'s/^\\\\(un)?restrict .*$/-- &/\' \\\n| sed -E "s/^CREATE ROLE \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "s/^ALTER ROLE \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "s/ (NOSUPERUSER|NOREPLICATION)//g" \\\n| sed -E "s/^-- (.* SET \\"($ALLOWED_CONFIGS)\\" .*)/\\1/" \\\n| sed -E "s/GRANT \\".*\\" TO \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "${EXTRA_SED:-}" \\\n| uniq\n\n# Reset session config generated by pg_dump\necho "RESET ALL;"\n'; + '#!/usr/bin/env bash\nset -euo pipefail\n\nexport PGHOST="$PGHOST"\nexport PGPORT="$PGPORT"\nexport PGUSER="$PGUSER"\nexport PGPASSWORD="$PGPASSWORD"\nexport PGDATABASE="$PGDATABASE"\n\n# Explanation of pg_dumpall flags:\n#\n# --roles-only only include create, alter, and grant role statements\n#\n# Explanation of sed substitutions:\n#\n# - do not emit psql meta commands\n# - do not create or alter reserved roles as they are blocked by supautils\n# - explicitly allow altering safe attributes, ie. statement_timeout, pgrst.*\n# - discard role attributes that require superuser, ie. nosuperuser, noreplication\n# - do not alter membership grants by supabase_admin role\n# - do not grant parameter privileges to reserved roles outside quoted SQL\npg_dumpall \\\n --roles-only \\\n --role "postgres" \\\n --quote-all-identifier \\\n --no-role-passwords \\\n --no-comments \\\n| LC_ALL=C awk -v reserved_roles="$RESERVED_ROLES" \'\n# Parenthesized field references survive the CLI dry-run expander.\nBEGIN { single_quote = sprintf("%c", 39) }\n{\n line = $(0)\n if (quote == "" && line ~ ("^GRANT [A-Z ,]+ ON PARAMETER \\".*\\" TO \\"(" reserved_roles ")\\"([ ;]|$)"))\n $(0) = "-- " line\n print\n for (i = 1; i <= length(line); i++) {\n char = substr(line, i, 1)\n next_char = substr(line, i + 1, 1)\n if (quote != "") {\n if (char == quote) {\n if (next_char == quote) i++\n else quote = ""\n }\n } else if (char == single_quote || char == "\\"") quote = char\n else if (char == "-" && next_char == "-") break\n }\n}\' \\\n| sed -E \'s/^\\\\(un)?restrict .*$/-- &/\' \\\n| sed -E "s/^CREATE ROLE \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "s/^ALTER ROLE \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "s/ (NOSUPERUSER|NOREPLICATION)//g" \\\n| sed -E "s/^-- (.* SET \\"($ALLOWED_CONFIGS)\\" .*)/\\1/" \\\n| sed -E "s/GRANT \\".*\\" TO \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "${EXTRA_SED:-}" \\\n| uniq\n\n# Reset session config generated by pg_dump\necho "RESET ALL;"\n'; From 33e85e953cc6ef7710424a80fad2bc245f4e135b Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:48:27 +0530 Subject: [PATCH 6/7] fix(cli): avoid rescanning long role settings --- .../src/command-internal/pg-dump.run.integration.test.ts | 7 ++++--- apps/cli/src/command-internal/pg-dump.scripts.ts | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/cli/src/command-internal/pg-dump.run.integration.test.ts b/apps/cli/src/command-internal/pg-dump.run.integration.test.ts index 0db5230d50..469e0199b2 100644 --- a/apps/cli/src/command-internal/pg-dump.run.integration.test.ts +++ b/apps/cli/src/command-internal/pg-dump.run.integration.test.ts @@ -321,9 +321,10 @@ last line'; ); describe.skipIf(process.platform === "win32")("Bash script", () => { + const longSetting = `ALTER ROLE "app_user" SET "myapp.large" TO '${"x".repeat(512 * 1024)}';\n`; for (const dryRun of [false, true]) { it.live( - `preserves quoted settings with keep-comments=${keepComments}, dry-run=${dryRun}`, + `preserves long quoted settings with keep-comments=${keepComments}, dry-run=${dryRun}`, () => Effect.gen(function* () { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; @@ -331,7 +332,7 @@ last line'; const child = yield* spawner.spawn( ChildProcess.make("bash", ["-c", `pg_dumpall() { cat; }\n${script}`], { env, - stdin: Stream.make(new TextEncoder().encode(input)), + stdin: Stream.make(new TextEncoder().encode(`${longSetting}${input}`)), stderr: "inherit", }), ); @@ -340,7 +341,7 @@ last line'; { concurrency: "unbounded" }, ); expect(exitCode).toBe(0); - expect(output).toBe(expected); + expect(output).toBe(`${longSetting}${expected}`); }).pipe(Effect.scoped, Effect.provide(BunServices.layer)), ); diff --git a/apps/cli/src/command-internal/pg-dump.scripts.ts b/apps/cli/src/command-internal/pg-dump.scripts.ts index 1d9bd0759e..6eef8ceac4 100644 --- a/apps/cli/src/command-internal/pg-dump.scripts.ts +++ b/apps/cli/src/command-internal/pg-dump.scripts.ts @@ -8,4 +8,4 @@ export const dumpDataScript = '#!/usr/bin/env bash\nset -euo pipefail\n\nexport PGHOST="$PGHOST"\nexport PGPORT="$PGPORT"\nexport PGUSER="$PGUSER"\nexport PGPASSWORD="$PGPASSWORD"\nexport PGDATABASE="$PGDATABASE"\n\n# Disable triggers so that data dump can be restored exactly as it is\necho "SET session_replication_role = replica;\n"\n\n# Explanation of pg_dump flags:\n#\n# --exclude-schema omit data from internal schemas as they are maintained by platform\n# --exclude-table omit data from migration history tables as they are managed by platform\n# --column-inserts only column insert syntax is supported, ie. no copy from stdin\n# --schema \'*\' include all other schemas by default\n#\n# Explanation of sed substitutions:\n#\n# - do not emit psql meta commands\n#\n# Never delete SQL comments because multiline records may begin with them.\npg_dump \\\n --data-only \\\n --quote-all-identifier \\\n --role "postgres" \\\n --exclude-schema "${EXCLUDED_SCHEMAS:-}" \\\n --exclude-table "auth.schema_migrations" \\\n --exclude-table "storage.migrations" \\\n --exclude-table "supabase_functions.migrations" \\\n --schema "$INCLUDED_SCHEMAS" \\\n ${EXTRA_FLAGS:-} \\\n| sed -E \'s/^\\\\(un)?restrict .*$/-- &/\'\n\n# Reset session config generated by pg_dump\necho "RESET ALL;"\n'; export const dumpRoleScript = - '#!/usr/bin/env bash\nset -euo pipefail\n\nexport PGHOST="$PGHOST"\nexport PGPORT="$PGPORT"\nexport PGUSER="$PGUSER"\nexport PGPASSWORD="$PGPASSWORD"\nexport PGDATABASE="$PGDATABASE"\n\n# Explanation of pg_dumpall flags:\n#\n# --roles-only only include create, alter, and grant role statements\n#\n# Explanation of sed substitutions:\n#\n# - do not emit psql meta commands\n# - do not create or alter reserved roles as they are blocked by supautils\n# - explicitly allow altering safe attributes, ie. statement_timeout, pgrst.*\n# - discard role attributes that require superuser, ie. nosuperuser, noreplication\n# - do not alter membership grants by supabase_admin role\n# - do not grant parameter privileges to reserved roles outside quoted SQL\npg_dumpall \\\n --roles-only \\\n --role "postgres" \\\n --quote-all-identifier \\\n --no-role-passwords \\\n --no-comments \\\n| LC_ALL=C awk -v reserved_roles="$RESERVED_ROLES" \'\n# Parenthesized field references survive the CLI dry-run expander.\nBEGIN { single_quote = sprintf("%c", 39) }\n{\n line = $(0)\n if (quote == "" && line ~ ("^GRANT [A-Z ,]+ ON PARAMETER \\".*\\" TO \\"(" reserved_roles ")\\"([ ;]|$)"))\n $(0) = "-- " line\n print\n for (i = 1; i <= length(line); i++) {\n char = substr(line, i, 1)\n next_char = substr(line, i + 1, 1)\n if (quote != "") {\n if (char == quote) {\n if (next_char == quote) i++\n else quote = ""\n }\n } else if (char == single_quote || char == "\\"") quote = char\n else if (char == "-" && next_char == "-") break\n }\n}\' \\\n| sed -E \'s/^\\\\(un)?restrict .*$/-- &/\' \\\n| sed -E "s/^CREATE ROLE \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "s/^ALTER ROLE \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "s/ (NOSUPERUSER|NOREPLICATION)//g" \\\n| sed -E "s/^-- (.* SET \\"($ALLOWED_CONFIGS)\\" .*)/\\1/" \\\n| sed -E "s/GRANT \\".*\\" TO \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "${EXTRA_SED:-}" \\\n| uniq\n\n# Reset session config generated by pg_dump\necho "RESET ALL;"\n'; + '#!/usr/bin/env bash\nset -euo pipefail\n\nexport PGHOST="$PGHOST"\nexport PGPORT="$PGPORT"\nexport PGUSER="$PGUSER"\nexport PGPASSWORD="$PGPASSWORD"\nexport PGDATABASE="$PGDATABASE"\n\n# Explanation of pg_dumpall flags:\n#\n# --roles-only only include create, alter, and grant role statements\n#\n# Explanation of sed substitutions:\n#\n# - do not emit psql meta commands\n# - do not create or alter reserved roles as they are blocked by supautils\n# - explicitly allow altering safe attributes, ie. statement_timeout, pgrst.*\n# - discard role attributes that require superuser, ie. nosuperuser, noreplication\n# - do not alter membership grants by supabase_admin role\n# - do not grant parameter privileges to reserved roles outside quoted SQL\npg_dumpall \\\n --roles-only \\\n --role "postgres" \\\n --quote-all-identifier \\\n --no-role-passwords \\\n --no-comments \\\n| LC_ALL=C awk -v reserved_roles="$RESERVED_ROLES" \'\n# Parenthesized field references survive the CLI dry-run expander.\nBEGIN { FS = ""; single_quote = sprintf("%c", 39) }\n{\n line = $(0)\n if (quote == "" && line ~ ("^GRANT [A-Z ,]+ ON PARAMETER \\".*\\" TO \\"(" reserved_roles ")\\"([ ;]|$)"))\n print "-- " line\n else print\n for (i = 1; i <= NF; i++) {\n char = $(i)\n next_char = $(i + 1)\n if (quote != "") {\n if (char == quote) {\n if (next_char == quote) i++\n else quote = ""\n }\n } else if (char == single_quote || char == "\\"") quote = char\n else if (char == "-" && next_char == "-") break\n }\n}\' \\\n| sed -E \'s/^\\\\(un)?restrict .*$/-- &/\' \\\n| sed -E "s/^CREATE ROLE \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "s/^ALTER ROLE \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "s/ (NOSUPERUSER|NOREPLICATION)//g" \\\n| sed -E "s/^-- (.* SET \\"($ALLOWED_CONFIGS)\\" .*)/\\1/" \\\n| sed -E "s/GRANT \\".*\\" TO \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "${EXTRA_SED:-}" \\\n| uniq\n\n# Reset session config generated by pg_dump\necho "RESET ALL;"\n'; From f2d5c1f9943248639fa7a49722ca9f07d64dc1a4 Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:52:29 +0530 Subject: [PATCH 7/7] fix(cli): preserve portable role dump scanning --- apps/cli/src/command-internal/pg-dump.scripts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/cli/src/command-internal/pg-dump.scripts.ts b/apps/cli/src/command-internal/pg-dump.scripts.ts index 6eef8ceac4..2dbb187405 100644 --- a/apps/cli/src/command-internal/pg-dump.scripts.ts +++ b/apps/cli/src/command-internal/pg-dump.scripts.ts @@ -8,4 +8,4 @@ export const dumpDataScript = '#!/usr/bin/env bash\nset -euo pipefail\n\nexport PGHOST="$PGHOST"\nexport PGPORT="$PGPORT"\nexport PGUSER="$PGUSER"\nexport PGPASSWORD="$PGPASSWORD"\nexport PGDATABASE="$PGDATABASE"\n\n# Disable triggers so that data dump can be restored exactly as it is\necho "SET session_replication_role = replica;\n"\n\n# Explanation of pg_dump flags:\n#\n# --exclude-schema omit data from internal schemas as they are maintained by platform\n# --exclude-table omit data from migration history tables as they are managed by platform\n# --column-inserts only column insert syntax is supported, ie. no copy from stdin\n# --schema \'*\' include all other schemas by default\n#\n# Explanation of sed substitutions:\n#\n# - do not emit psql meta commands\n#\n# Never delete SQL comments because multiline records may begin with them.\npg_dump \\\n --data-only \\\n --quote-all-identifier \\\n --role "postgres" \\\n --exclude-schema "${EXCLUDED_SCHEMAS:-}" \\\n --exclude-table "auth.schema_migrations" \\\n --exclude-table "storage.migrations" \\\n --exclude-table "supabase_functions.migrations" \\\n --schema "$INCLUDED_SCHEMAS" \\\n ${EXTRA_FLAGS:-} \\\n| sed -E \'s/^\\\\(un)?restrict .*$/-- &/\'\n\n# Reset session config generated by pg_dump\necho "RESET ALL;"\n'; export const dumpRoleScript = - '#!/usr/bin/env bash\nset -euo pipefail\n\nexport PGHOST="$PGHOST"\nexport PGPORT="$PGPORT"\nexport PGUSER="$PGUSER"\nexport PGPASSWORD="$PGPASSWORD"\nexport PGDATABASE="$PGDATABASE"\n\n# Explanation of pg_dumpall flags:\n#\n# --roles-only only include create, alter, and grant role statements\n#\n# Explanation of sed substitutions:\n#\n# - do not emit psql meta commands\n# - do not create or alter reserved roles as they are blocked by supautils\n# - explicitly allow altering safe attributes, ie. statement_timeout, pgrst.*\n# - discard role attributes that require superuser, ie. nosuperuser, noreplication\n# - do not alter membership grants by supabase_admin role\n# - do not grant parameter privileges to reserved roles outside quoted SQL\npg_dumpall \\\n --roles-only \\\n --role "postgres" \\\n --quote-all-identifier \\\n --no-role-passwords \\\n --no-comments \\\n| LC_ALL=C awk -v reserved_roles="$RESERVED_ROLES" \'\n# Parenthesized field references survive the CLI dry-run expander.\nBEGIN { FS = ""; single_quote = sprintf("%c", 39) }\n{\n line = $(0)\n if (quote == "" && line ~ ("^GRANT [A-Z ,]+ ON PARAMETER \\".*\\" TO \\"(" reserved_roles ")\\"([ ;]|$)"))\n print "-- " line\n else print\n for (i = 1; i <= NF; i++) {\n char = $(i)\n next_char = $(i + 1)\n if (quote != "") {\n if (char == quote) {\n if (next_char == quote) i++\n else quote = ""\n }\n } else if (char == single_quote || char == "\\"") quote = char\n else if (char == "-" && next_char == "-") break\n }\n}\' \\\n| sed -E \'s/^\\\\(un)?restrict .*$/-- &/\' \\\n| sed -E "s/^CREATE ROLE \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "s/^ALTER ROLE \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "s/ (NOSUPERUSER|NOREPLICATION)//g" \\\n| sed -E "s/^-- (.* SET \\"($ALLOWED_CONFIGS)\\" .*)/\\1/" \\\n| sed -E "s/GRANT \\".*\\" TO \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "${EXTRA_SED:-}" \\\n| uniq\n\n# Reset session config generated by pg_dump\necho "RESET ALL;"\n'; + '#!/usr/bin/env bash\nset -euo pipefail\n\nexport PGHOST="$PGHOST"\nexport PGPORT="$PGPORT"\nexport PGUSER="$PGUSER"\nexport PGPASSWORD="$PGPASSWORD"\nexport PGDATABASE="$PGDATABASE"\n\n# Explanation of pg_dumpall flags:\n#\n# --roles-only only include create, alter, and grant role statements\n#\n# Explanation of sed substitutions:\n#\n# - do not emit psql meta commands\n# - do not create or alter reserved roles as they are blocked by supautils\n# - explicitly allow altering safe attributes, ie. statement_timeout, pgrst.*\n# - discard role attributes that require superuser, ie. nosuperuser, noreplication\n# - do not alter membership grants by supabase_admin role\n# - do not grant parameter privileges to reserved roles outside quoted SQL\npg_dumpall \\\n --roles-only \\\n --role "postgres" \\\n --quote-all-identifier \\\n --no-role-passwords \\\n --no-comments \\\n| LC_ALL=C awk -v reserved_roles="$RESERVED_ROLES" \'\n# Parenthesized field references survive the CLI dry-run expander.\nBEGIN { single_quote = sprintf("%c", 39) }\n{\n line = $(0)\n if (quote == "" && line ~ ("^GRANT [A-Z ,]+ ON PARAMETER \\".*\\" TO \\"(" reserved_roles ")\\"([ ;]|$)"))\n $(0) = "-- " line\n print\n count = length(line)\n end = 0\n for (i = 1; i <= count; i++) {\n if (i > end) {\n chunk = substr(line, i, 1025)\n start = i\n end = i + 1023\n }\n char = substr(chunk, i - start + 1, 1)\n next_char = substr(chunk, i - start + 2, 1)\n if (quote != "") {\n if (char == quote) {\n if (next_char == quote) i++\n else quote = ""\n }\n } else if (char == single_quote || char == "\\"") quote = char\n else if (char == "-" && next_char == "-") break\n }\n}\' \\\n| sed -E \'s/^\\\\(un)?restrict .*$/-- &/\' \\\n| sed -E "s/^CREATE ROLE \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "s/^ALTER ROLE \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "s/ (NOSUPERUSER|NOREPLICATION)//g" \\\n| sed -E "s/^-- (.* SET \\"($ALLOWED_CONFIGS)\\" .*)/\\1/" \\\n| sed -E "s/GRANT \\".*\\" TO \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "${EXTRA_SED:-}" \\\n| uniq\n\n# Reset session config generated by pg_dump\necho "RESET ALL;"\n';