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..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 @@ -1,8 +1,11 @@ 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 { 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"; @@ -10,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: { @@ -23,86 +26,170 @@ 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", () => { - 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, - ], - [ - '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({ @@ -118,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"); @@ -176,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(() => { @@ -212,3 +259,133 @@ 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\\\\''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", () => { + const longSetting = `ALTER ROLE "app_user" SET "myapp.large" TO '${"x".repeat(512 * 1024)}';\n`; + for (const dryRun of [false, true]) { + it.live( + `preserves long 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(`${longSetting}${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(`${longSetting}${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.run.ts b/apps/cli/src/command-internal/pg-dump.run.ts index 8232be1e4a..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,6 +250,13 @@ const transformDumpLine = ( return dropComments(allowedConfig.test(line) ? line : `-- ${line}`); if (new RegExp(`GRANT ".*" TO "(${reservedRoles})"`, "u").test(line)) return dropComments(`-- ${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)); if (line.startsWith("-- ") && line.includes(" GRANT ")) return dropComments(line); @@ -304,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 }); @@ -312,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) @@ -327,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 cbce14c3df..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\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 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';