@rebasepro/server-postgres 0.19.1 → 0.19.2-canary.g08eed46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/{BranchService-ByLfM_Tt.js → BranchService-W3DMfcZZ.js} +2 -2
  2. package/dist/{BranchService-ByLfM_Tt.js.map → BranchService-W3DMfcZZ.js.map} +1 -1
  3. package/dist/PostgresBackendDriver.d.ts +81 -7
  4. package/dist/PostgresBootstrapper.d.ts +10 -0
  5. package/dist/{backup-cli-oM9WQVlV.js → backup-cli-DSpQyqcG.js} +2 -2
  6. package/dist/{backup-cli-oM9WQVlV.js.map → backup-cli-DSpQyqcG.js.map} +1 -1
  7. package/dist/{backup-service-Br3tqwsy.js → backup-service-DA7a6SUV.js} +2 -2
  8. package/dist/{backup-service-Br3tqwsy.js.map → backup-service-DA7a6SUV.js.map} +1 -1
  9. package/dist/{cli-errors-DxN4yzdH.js → cli-errors-C3g_kHBw.js} +137 -18
  10. package/dist/cli-errors-C3g_kHBw.js.map +1 -0
  11. package/dist/cli-errors.d.ts +19 -0
  12. package/dist/cli-helpers.d.ts +47 -0
  13. package/dist/cli.js +233 -19
  14. package/dist/cli.js.map +1 -1
  15. package/dist/{collection-index-BHgT_TKd.js → collection-index-DdnsxiJ_.js} +14 -4
  16. package/dist/{collection-index-BHgT_TKd.js.map → collection-index-DdnsxiJ_.js.map} +1 -1
  17. package/dist/collections/validate-relations.d.ts +7 -0
  18. package/dist/column-plan-helpers-CpILzHJS.js +145 -0
  19. package/dist/column-plan-helpers-CpILzHJS.js.map +1 -0
  20. package/dist/{doctor-D7HWmGzs.js → doctor-D5SrGJ5P.js} +175 -61
  21. package/dist/doctor-D5SrGJ5P.js.map +1 -0
  22. package/dist/{ensure-collection-policies-2c2NV8zX.js → ensure-collection-policies-CHO0moXQ.js} +5 -5
  23. package/dist/{ensure-collection-policies-2c2NV8zX.js.map → ensure-collection-policies-CHO0moXQ.js.map} +1 -1
  24. package/dist/{ensure-collection-tables-D4wJzM4y.js → ensure-collection-tables-Bkvehxdm.js} +318 -312
  25. package/dist/ensure-collection-tables-Bkvehxdm.js.map +1 -0
  26. package/dist/{ensure-tables-CIojK-Rx.js → ensure-tables-CYMtuuOd.js} +2 -2
  27. package/dist/{ensure-tables-CIojK-Rx.js.map → ensure-tables-CYMtuuOd.js.map} +1 -1
  28. package/dist/generate-drizzle-schema-logic-Dfyf_MRu.js +228 -0
  29. package/dist/generate-drizzle-schema-logic-Dfyf_MRu.js.map +1 -0
  30. package/dist/{generate-drizzle-schema-CwEgVyjO.js → generate-drizzle-schema-vSK-VdYT.js} +3 -3
  31. package/dist/generate-drizzle-schema-vSK-VdYT.js.map +1 -0
  32. package/dist/generate-postgres-ddl-logic-CR2xcS7e.js +475 -0
  33. package/dist/generate-postgres-ddl-logic-CR2xcS7e.js.map +1 -0
  34. package/dist/{generated-schema-staleness-DQdfJl2A.js → generated-schema-staleness-DRQe2BpC.js} +81 -3
  35. package/dist/{generated-schema-staleness-DQdfJl2A.js.map → generated-schema-staleness-DRQe2BpC.js.map} +1 -1
  36. package/dist/history/HistoryService.d.ts +23 -4
  37. package/dist/index.es.js +1991 -580
  38. package/dist/index.es.js.map +1 -1
  39. package/dist/introspect-db-logic-C6LQdTxj.js.map +1 -1
  40. package/dist/plan-schema-Hgl62S-w.js +1531 -0
  41. package/dist/plan-schema-Hgl62S-w.js.map +1 -0
  42. package/dist/{policy-drift-kNJ6X2Uj.js → policy-drift-B0GDRh7_.js} +2 -2
  43. package/dist/{policy-drift-kNJ6X2Uj.js.map → policy-drift-B0GDRh7_.js.map} +1 -1
  44. package/dist/{rls-bootstrap-sql-DLDnUjtx.js → rls-bootstrap-sql-BlzsOUtz.js} +111 -8
  45. package/dist/{rls-bootstrap-sql-DLDnUjtx.js.map → rls-bootstrap-sql-BlzsOUtz.js.map} +1 -1
  46. package/dist/{rls-enforcement-DeYEjYLt.js → rls-enforcement-BV12vBwQ.js} +2 -1
  47. package/dist/rls-enforcement-BV12vBwQ.js.map +1 -0
  48. package/dist/schema/catalogue-schema.d.ts +97 -0
  49. package/dist/schema/collection-index.d.ts +9 -0
  50. package/dist/schema/column-plan-helpers.d.ts +125 -0
  51. package/dist/schema/config-relations.d.ts +32 -0
  52. package/dist/schema/doctor-cli.js +4 -4
  53. package/dist/schema/doctor.d.ts +9 -0
  54. package/dist/schema/dynamic-tables.d.ts +27 -6
  55. package/dist/schema/ensure-collection-tables.d.ts +31 -268
  56. package/dist/schema/generate-drizzle-schema-logic.d.ts +26 -8
  57. package/dist/schema/generate-drizzle-schema.js +1 -1
  58. package/dist/schema/generate-postgres-ddl-logic.d.ts +84 -168
  59. package/dist/schema/generate-postgres-ddl.js +8 -2
  60. package/dist/schema/generate-postgres-ddl.js.map +1 -1
  61. package/dist/schema/generated-column-conflicts.d.ts +84 -0
  62. package/dist/schema/generated-schema-diff.d.ts +56 -0
  63. package/dist/schema/generated-schema-staleness.d.ts +33 -0
  64. package/dist/schema/introspect-db-logic.d.ts +2 -0
  65. package/dist/schema/plan/diff-plan.d.ts +220 -0
  66. package/dist/schema/plan/plan-schema.d.ts +84 -0
  67. package/dist/schema/plan/render-ddl.d.ts +62 -0
  68. package/dist/schema/plan/render-drizzle.d.ts +31 -0
  69. package/dist/schema/plan/types.d.ts +410 -0
  70. package/dist/schema/plan/updated-at-trigger.d.ts +22 -0
  71. package/dist/schema/relation-names.d.ts +28 -0
  72. package/dist/schema/search-column.d.ts +9 -0
  73. package/dist/security/rls-enforcement.d.ts +17 -0
  74. package/dist/services/FetchService.d.ts +176 -36
  75. package/dist/services/PersistService.d.ts +17 -4
  76. package/dist/services/RelationService.d.ts +11 -3
  77. package/dist/services/collection-helpers.d.ts +16 -2
  78. package/dist/services/dataService.d.ts +29 -2
  79. package/dist/services/field-op-sql.d.ts +16 -0
  80. package/dist/services/field-viewer.d.ts +19 -0
  81. package/dist/services/realtimeService.d.ts +35 -1
  82. package/dist/services/row-pipeline.d.ts +29 -5
  83. package/dist/services/soft-delete.d.ts +73 -0
  84. package/dist/utils/pg-error-utils.d.ts +41 -1
  85. package/dist/{vector-index-DkWxm2de.js → vector-index-CMJKb3C9.js} +3 -14
  86. package/dist/{vector-index-DkWxm2de.js.map → vector-index-CMJKb3C9.js.map} +1 -1
  87. package/package.json +18 -10
  88. package/dist/cli-errors-DxN4yzdH.js.map +0 -1
  89. package/dist/doctor-D7HWmGzs.js.map +0 -1
  90. package/dist/ensure-collection-tables-D4wJzM4y.js.map +0 -1
  91. package/dist/generate-drizzle-schema-CwEgVyjO.js.map +0 -1
  92. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js +0 -528
  93. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js.map +0 -1
  94. package/dist/generate-postgres-ddl-logic-CtnpIBVv.js +0 -1312
  95. package/dist/generate-postgres-ddl-logic-CtnpIBVv.js.map +0 -1
  96. package/dist/rls-enforcement-DeYEjYLt.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"backup-service-Br3tqwsy.js","names":[],"sources":["../src/backup/pg-tools.ts","../src/backup/retention.ts","../src/backup/backup-logic.ts","../src/backup/backup-service.ts"],"sourcesContent":["/**\n * Pure helpers for the backup/restore commands.\n *\n * Everything in this file is side-effect free so it can be unit-tested\n * without a live Postgres server, matching the constraint that CI must\n * not require a database.\n */\n\nimport { forLibpq } from \"../utils/connection-string\";\n\n/**\n * A parsed backup destination. `--out` (and the scheduled-backup config)\n * accepts either a local filesystem path or an object-storage URL.\n */\nexport type BackupDestination =\n | { kind: \"local\"; path: string }\n | { kind: \"s3\"; bucket: string; prefix: string }\n | { kind: \"gcs\"; bucket: string; prefix: string };\n\n/**\n * Extract the database name from a Postgres connection string.\n * Returns `null` when the URL has no database path (e.g. bare host).\n */\nexport function parseDbNameFromUrl(connectionString: string): string | null {\n try {\n const parsed = new URL(connectionString);\n const name = parsed.pathname.replace(/^\\//, \"\").trim();\n return name.length > 0 ? name : null;\n } catch {\n // Fall back to a permissive regex for non-URL DSNs.\n const match = connectionString.match(/\\/([^/?]+)(\\?|$)/);\n return match && match[1] ? match[1] : null;\n }\n}\n\n/**\n * Swap the database name in a connection string, preserving credentials,\n * host, port and query params. Used by `--target-db` / `--create-db` so a\n * restore can target a fresh database instead of clobbering the live one.\n */\nexport function withDatabaseName(connectionString: string, dbName: string): string {\n try {\n const parsed = new URL(connectionString);\n parsed.pathname = `/${dbName}`;\n return parsed.toString();\n } catch {\n return connectionString.replace(/\\/([^/?]+)(\\?|$)/, `/${dbName}$2`);\n }\n}\n\n/**\n * Parse the major version out of `pg_dump --version` / `pg_restore --version`\n * output, e.g. `\"pg_dump (PostgreSQL) 16.2\"` → `16`. Handles the pre-10\n * `9.6.x` scheme (returns `9`) and beta strings like `\"17beta1\"`.\n */\nexport function parsePgToolMajor(versionOutput: string): number | null {\n const match = versionOutput.match(/(\\d+)(?:\\.(\\d+))?/);\n if (!match) return null;\n const major = Number(match[1]);\n if (!Number.isFinite(major)) return null;\n // Postgres 9.x used the first two numbers as the major (9.6, 9.4…).\n if (major === 9 && match[2] !== undefined) {\n return 9;\n }\n return major;\n}\n\n/**\n * Convert `SELECT current_setting('server_version_num')` (e.g. `160002`)\n * into a major version (`16`). Also accepts pre-10 encodings like `90603`\n * → `9`.\n */\nexport function serverVersionNumToMajor(versionNum: number | string): number | null {\n const num = typeof versionNum === \"string\" ? Number(versionNum) : versionNum;\n if (!Number.isFinite(num) || num <= 0) return null;\n if (num < 100000) {\n // 9.x scheme: 90603 = 9.6.3\n return Math.floor(num / 10000);\n }\n return Math.floor(num / 10000);\n}\n\nexport interface VersionCompatibility {\n compatible: boolean;\n reason?: string;\n}\n\n/**\n * pg_dump / pg_restore must be **at least** as new as the server they talk\n * to. A newer client against an older server is supported; an older client\n * against a newer server is not and produces corrupt or rejected output.\n */\nexport function checkToolServerCompatibility(\n toolMajor: number | null,\n serverMajor: number | null\n): VersionCompatibility {\n if (toolMajor === null) {\n return { compatible: false, reason: \"Could not determine the client tool version.\" };\n }\n if (serverMajor === null) {\n return { compatible: false, reason: \"Could not determine the Postgres server version.\" };\n }\n if (toolMajor < serverMajor) {\n return {\n compatible: false,\n reason:\n `Client tool is Postgres ${toolMajor} but the server is Postgres ${serverMajor}. ` +\n `pg_dump/pg_restore must be the same major version as the server or newer. ` +\n `Install Postgres ${serverMajor} client tools.`\n };\n }\n return { compatible: true };\n}\n\n/**\n * Build a deterministic, sortable backup file name:\n * `rebase-<db>-<YYYYMMDD>T<HHMMSS>Z.dump`\n *\n * The UTC timestamp is embedded so retention pruning can recover the\n * creation time from the object key alone, without extra metadata.\n */\nexport function buildBackupFilename(dbName: string, date: Date = new Date()): string {\n const iso = date.toISOString(); // 2026-07-14T09:12:03.123Z\n const stamp = iso.replace(/\\.\\d+Z$/, \"Z\").replace(/[-:]/g, \"\");\n const safeDb = dbName.replace(/[^a-zA-Z0-9_-]/g, \"_\");\n return `rebase-${safeDb}-${stamp}.dump`;\n}\n\n/**\n * Recover the creation timestamp encoded in a backup file name by\n * {@link buildBackupFilename}. Returns `null` for names that don't match,\n * so foreign objects in a shared prefix are never pruned.\n */\nexport function parseBackupTimestamp(fileName: string): Date | null {\n const base = fileName.split(\"/\").pop() ?? fileName;\n const match = base.match(/-(\\d{8})T(\\d{6})Z\\.dump$/);\n if (!match) return null;\n const [, ymd, hms] = match;\n const iso =\n `${ymd.slice(0, 4)}-${ymd.slice(4, 6)}-${ymd.slice(6, 8)}` +\n `T${hms.slice(0, 2)}:${hms.slice(2, 4)}:${hms.slice(4, 6)}Z`;\n const date = new Date(iso);\n return Number.isNaN(date.getTime()) ? null : date;\n}\n\n/**\n * Parse a destination string into a structured {@link BackupDestination}.\n * `s3://bucket/prefix` and `gs://bucket/prefix` map to object storage;\n * anything else is treated as a local path.\n */\nexport function parseBackupDestination(out: string): BackupDestination {\n const s3 = out.match(/^s3:\\/\\/([^/]+)\\/?(.*)$/);\n if (s3) {\n return { kind: \"s3\", bucket: s3[1], prefix: stripTrailingSlash(s3[2]) };\n }\n const gcs = out.match(/^gs:\\/\\/([^/]+)\\/?(.*)$/);\n if (gcs) {\n return { kind: \"gcs\", bucket: gcs[1], prefix: stripTrailingSlash(gcs[2]) };\n }\n return { kind: \"local\", path: out };\n}\n\nfunction stripTrailingSlash(s: string): string {\n return s.replace(/\\/+$/, \"\");\n}\n\n/**\n * Join a storage prefix and a file name without producing a leading or\n * doubled slash.\n */\nexport function joinStorageKey(prefix: string, fileName: string): string {\n const clean = prefix.replace(/^\\/+|\\/+$/g, \"\");\n return clean.length > 0 ? `${clean}/${fileName}` : fileName;\n}\n\n/**\n * The identity `pg_dump` reads rows as, when row security is left on.\n *\n * Not optional, and that is the whole design. `pg_dump --enable-row-security`\n * on its own is the dangerous command in this file: it turns the \"query would\n * be affected by row-level security policy\" *error* into a dump that exits 0\n * and is silently missing every row the dumping role's policies exclude. A\n * backup that looks fine and restores most of your data is worse than one that\n * refused to run.\n *\n * So the flag is unreachable without a subject to evaluate the policies\n * against. Rebase's generated policies read `app.uid` and `app.user_roles`;\n * supplying an admin role satisfies the `admin_full_access` rule and the dump\n * sees everything that rule sees.\n */\nexport interface RowSecurityIdentity {\n /** Written to `app.uid`. Any non-empty value — it is only an audit trail. */\n uid: string;\n /** Written to `app.user_roles`. Must include a role the policies admit. */\n roles: string[];\n}\n\n/**\n * `PGOPTIONS` carrying an identity, for a libpq tool that has no other way to\n * set a GUC.\n *\n * A backslash escape rather than quoting, which is what libpq's `-c` parsing\n * takes: a space inside a value ends the option otherwise, so a role list is\n * comma-joined and never spaced.\n */\nexport function buildRowSecurityPgOptions(identity: RowSecurityIdentity): string {\n const escape = (value: string) => value.replace(/([\\\\ ])/g, \"\\\\$1\");\n return [\n `-c app.uid=${escape(identity.uid)}`,\n `-c app.user_id=${escape(identity.uid)}`,\n `-c app.user_roles=${escape(identity.roles.join(\",\"))}`\n ].join(\" \");\n}\n\n/**\n * Assemble the `pg_dump` argument vector. Uses the custom format (`-Fc`),\n * which is compressed and restorable selectively via `pg_restore`.\n */\nexport function buildPgDumpArgs(opts: {\n connectionString: string;\n outFile: string;\n /** Extra schemas/tables to exclude, e.g. Atlas revision tables. */\n excludeSchemas?: string[];\n /** Number of parallel jobs (directory format only; ignored for -Fc). */\n noOwner?: boolean;\n /**\n * Dump with row security on, as this identity. Omit — which is the default\n * — and `pg_dump` errors rather than skipping rows it cannot see.\n */\n rowSecurity?: RowSecurityIdentity;\n}): string[] {\n const args = [\"--format=custom\", \"--no-password\", `--file=${opts.outFile}`];\n if (opts.noOwner) {\n args.push(\"--no-owner\");\n }\n if (opts.rowSecurity) {\n args.push(\"--enable-row-security\");\n }\n for (const schema of opts.excludeSchemas ?? []) {\n args.push(`--exclude-schema=${schema}`);\n }\n args.push(forLibpq(opts.connectionString));\n return args;\n}\n\n/**\n * Whether a `pg_dump` failure is the row-security one, and what to do about it.\n *\n * The error text names the table and nothing else, so the first read of it is\n * \"why would a backup be affected by RLS at all?\" — the answer being that the\n * dumping role is not the tables' owner and has no `BYPASSRLS`, which is the\n * normal state of the `postgres` user on Cloud SQL, RDS and every other managed\n * Postgres. Nothing about that is visible from the message.\n *\n * Returns `null` for any other failure, so the caller reports it unchanged.\n */\nexport function diagnoseRowSecurityDumpFailure(error: unknown): string | null {\n const text = [\n (error as { stderr?: unknown })?.stderr,\n (error as { message?: unknown })?.message\n ].map(part => (typeof part === \"string\" ? part : \"\")).join(\"\\n\");\n\n if (!/row-level security policy/i.test(text)) return null;\n\n const table = text.match(/for table \"([^\"]+)\"/)?.[1];\n\n return [\n `pg_dump cannot read ${table ? `\"${table}\"` : \"one of the tables\"} because row-level security applies to it.`,\n \"\",\n \" The dumping role is neither the table's owner nor `BYPASSRLS`, which is the normal\",\n \" state of the `postgres` user on Cloud SQL, RDS and other managed Postgres — there is\",\n \" no superuser to hand out.\",\n \"\",\n \" Two ways out:\",\n \"\",\n \" • Grant the dumping role BYPASSRLS, or make it the owner, and run this again. The\",\n \" dump then contains every row, which is what a backup should mean.\",\n \"\",\n \" • Re-run with --enable-row-security to dump as an admin subject instead. Rebase\",\n \" sets `app.uid`/`app.user_roles` so the generated `admin_full_access` policy\",\n \" admits the dump. Read the warning it prints: the result contains exactly the\",\n \" rows those policies admit, and any table whose policies do not include an\",\n \" admin rule comes out short — with no error.\",\n \"\",\n \" Do not reach for a bare `pg_dump --enable-row-security` by hand. Without the\",\n \" settings above it succeeds and silently omits rows.\"\n ].join(\"\\n\");\n}\n\n/**\n * Assemble the `pg_restore` argument vector for a custom-format dump.\n */\nexport function buildPgRestoreArgs(opts: {\n connectionString: string;\n inputFile: string;\n /** Drop objects before recreating them (destructive but idempotent). */\n clean?: boolean;\n /**\n * Abort on the first error instead of logging and continuing. Defaults\n * ON: a restore that silently skips failed GRANT/RLS statements (because\n * a role is missing) \"succeeds\" with RLS un-enforced — a security hole.\n * Fail loudly instead so the operator knows the restore is incomplete.\n */\n exitOnError?: boolean;\n noOwner?: boolean;\n}): string[] {\n const args = [\"--format=custom\", \"--no-password\", `--dbname=${forLibpq(opts.connectionString)}`];\n if (opts.clean) {\n args.push(\"--clean\", \"--if-exists\");\n }\n if (opts.noOwner) {\n args.push(\"--no-owner\");\n }\n // Default to --exit-on-error unless explicitly disabled.\n if (opts.exitOnError !== false) {\n args.push(\"--exit-on-error\");\n }\n args.push(opts.inputFile);\n return args;\n}\n\n/**\n * Assemble the `pg_restore --list` argument vector. Reading a dump's table\n * of contents parses the whole archive without touching a database, so it is\n * a cheap integrity check that the file isn't truncated or corrupt.\n */\nexport function buildPgRestoreListArgs(inputFile: string): string[] {\n return [\"--list\", inputFile];\n}\n\n/**\n * Assemble the `pg_dumpall --globals-only` argument vector. Roles (and other\n * cluster-wide objects) live outside any single database, so a per-database\n * `pg_dump` omits them. Without the `rebase_user` role the RLS GRANT\n * statements in the main dump fail on restore and RLS is silently lost — so\n * every backup captures the globals into a sidecar `.globals.sql`.\n *\n * `--no-role-passwords` keeps role secrets out of the artifact (backups may\n * be shipped off-box); roles are recreated password-less and re-secured by\n * the operator.\n */\nexport function buildPgDumpallGlobalsArgs(opts: {\n connectionString: string;\n outFile: string;\n}): string[] {\n return [\n \"--globals-only\",\n \"--no-role-passwords\",\n \"--no-password\",\n `--file=${opts.outFile}`,\n `--dbname=${forLibpq(opts.connectionString)}`\n ];\n}\n\n/**\n * Derive the globals sidecar path/key for a given `.dump` file. Keeps the\n * two artifacts adjacent so listing, uploading and pruning can find one from\n * the other. A name that doesn't end in `.dump` is returned unchanged with a\n * `.globals.sql` suffix appended.\n */\nexport function globalsFileForDump(dumpPath: string): string {\n return dumpPath.endsWith(\".dump\")\n ? dumpPath.slice(0, -\".dump\".length) + \".globals.sql\"\n : dumpPath + \".globals.sql\";\n}\n\n/**\n * Split a `pg_dumpall --globals-only` script into individual statements.\n * Used when replaying globals on restore so each `CREATE ROLE` / `GRANT`\n * can run independently and a benign \"role already exists\" on one doesn't\n * abort the rest. Drops `--` comment lines and blank statements.\n */\nexport function splitGlobalsStatements(sql: string): string[] {\n const withoutComments = sql\n .split(\"\\n\")\n // `--` comments, and psql meta-commands. pg_dumpall 15+ wraps its output\n // in `\\restrict <token>` / `\\unrestrict <token>`, which are instructions\n // to psql, not SQL. This replay sends statements over a connection\n // instead, so they arrived at the server as `\\restrict …` and came back\n // as `syntax error at or near \"\\\"` — reported to the user as two skipped\n // globals per restore, which reads like roles failed to apply when\n // nothing did. They carry no state worth replaying: dropping them is\n // exactly what a SQL-level consumer should do.\n .filter((line) => {\n const trimmed = line.trim();\n return !trimmed.startsWith(\"--\") && !trimmed.startsWith(\"\\\\\");\n })\n .join(\"\\n\");\n return withoutComments\n .split(\";\")\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n}\n\n/**\n * Resolve the Postgres connection string the backup commands should use,\n * mirroring the precedence the branch command already relies on.\n */\nexport function resolveConnectionString(\n env: Record<string, string | undefined>\n): string | null {\n return env.DATABASE_URL || env.ADMIN_CONNECTION_STRING || null;\n}\n","/**\n * Retention pruning for scheduled backups. Pure logic — no I/O — so it can\n * be exhaustively unit-tested without touching a storage backend.\n */\nimport { parseBackupTimestamp } from \"./pg-tools\";\n\nexport interface BackupObject {\n /** Storage key / file name. */\n key: string;\n /**\n * Creation time. Optional — when absent it is recovered from the\n * timestamp encoded in the key by `buildBackupFilename`.\n */\n createdAt?: Date;\n /**\n * Size on disk, when the lister knows it (local destinations always do).\n *\n * Reported so an empty artifact cannot masquerade as a backup in\n * `rebase db backups list`. Deliberately NOT consulted by\n * {@link selectBackupsToPrune}: retention decides by age, and a rule that\n * silently skipped small files would make a genuinely tiny dump immortal.\n * Visibility is the fix here; the artifacts themselves are now removed at\n * the point of failure by `discardPartialDump`.\n */\n sizeBytes?: number;\n}\n\nexport interface RetentionOptions {\n /** Delete backups older than this many days. `0`/negative disables age pruning. */\n retentionDays?: number;\n /**\n * Always keep at least this many of the most recent backups, even if\n * they are older than `retentionDays`. Guards against wiping every\n * backup after a long outage. Default: 0 (no minimum).\n */\n keepMinimum?: number;\n /** Reference \"now\" — injectable for deterministic tests. */\n now?: Date;\n}\n\n/**\n * Decide which backups to delete under the given retention policy.\n *\n * Rules, applied in order:\n * 1. Objects whose key isn't a recognised rebase backup (no parseable\n * timestamp and no `createdAt`) are never selected — foreign files in a\n * shared bucket are left untouched.\n * 2. The `keepMinimum` newest backups are always retained.\n * 3. Of the remainder, any older than `retentionDays` are selected for\n * deletion.\n *\n * Returns the keys to delete, oldest first.\n */\nexport function selectBackupsToPrune(\n backups: BackupObject[],\n options: RetentionOptions\n): string[] {\n const retentionDays = options.retentionDays ?? 0;\n const keepMinimum = Math.max(0, options.keepMinimum ?? 0);\n const now = options.now ?? new Date();\n\n if (retentionDays <= 0) {\n return [];\n }\n\n // Resolve a timestamp for every object; drop the ones we can't date.\n const dated = backups\n .map((b) => {\n const createdAt = b.createdAt ?? parseBackupTimestamp(b.key);\n return createdAt ? { key: b.key, createdAt } : null;\n })\n .filter((b): b is { key: string; createdAt: Date } => b !== null);\n\n // Newest first so the first `keepMinimum` are the protected ones.\n dated.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());\n\n const cutoff = now.getTime() - retentionDays * 24 * 60 * 60 * 1000;\n\n const toDelete = dated\n .slice(keepMinimum)\n .filter((b) => b.createdAt.getTime() < cutoff);\n\n // Return oldest first for predictable, readable logs.\n toDelete.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());\n return toDelete.map((b) => b.key);\n}\n","/**\n * Pure-ish orchestration for the backup/restore paths, kept free of `execa`\n * and `pg` value imports so it can be unit-tested under jest (the impure\n * edges — spawning processes, opening connections — live in\n * `backup-service.ts`, which is vitest/runtime only).\n *\n * The functions here take their side-effecting dependency as an argument\n * (a statement runner, an object deleter) so tests can inject fakes.\n */\nimport { globalsFileForDump, splitGlobalsStatements } from \"./pg-tools\";\n\n/**\n * Replay a `pg_dumpall --globals-only` script one statement at a time,\n * tolerating per-statement failures. On a same-cluster restore the roles\n * usually already exist (`CREATE ROLE` → \"already exists\") and on a managed\n * provider an `ALTER ROLE <superuser>` may be refused; neither should abort\n * recreation of the roles that *are* missing. Returns how many statements\n * applied vs were skipped.\n *\n * `runStatement` executes one SQL statement and rejects on error.\n */\nexport async function applyGlobalsWith(\n runStatement: (sql: string) => Promise<void>,\n globalsSql: string,\n log: (message: string) => void = () => {}\n): Promise<{ applied: number; skipped: number }> {\n let applied = 0;\n let skipped = 0;\n for (const statement of splitGlobalsStatements(globalsSql)) {\n try {\n await runStatement(statement);\n applied++;\n } catch (err) {\n skipped++;\n const firstLine = statement.split(\"\\n\")[0].slice(0, 80);\n log(` • Skipped global: ${firstLine} (${err instanceof Error ? err.message : String(err)})`);\n }\n }\n return { applied, skipped };\n}\n\n/**\n * Delete each pruned dump together with its `.globals.sql` roles sidecar, so\n * pruning never orphans the roles file. The sidecar is best-effort — older\n * backups predate it, so a missing-sidecar failure is swallowed while a\n * failure deleting the dump itself propagates.\n *\n * `deleteObject` removes one key and rejects if it cannot (e.g. not found).\n */\nexport async function pruneWith(\n keys: string[],\n deleteObject: (key: string) => Promise<void>\n): Promise<void> {\n for (const key of keys) {\n await deleteObject(key);\n try {\n await deleteObject(globalsFileForDump(key));\n } catch {\n // Sidecar may not exist for older backups — ignore.\n }\n }\n}\n\n/**\n * Remove a dump artifact abandoned by a failed tool run.\n *\n * `pg_dump` creates its `--file=` target before it finishes connecting, so any\n * failure — a URL libpq rejects, a dropped connection, a full disk — leaves a\n * 0-byte file behind. That corpse is not inert: `rebase db backups list` used\n * to show it as an ordinary entry, and `selectBackupsToPrune` ranks by\n * timestamp alone, so it occupies a protected `keepMinimum` slot and can push\n * a real backup out of retention.\n *\n * Best-effort on purpose: the caller is already throwing, and a failed unlink\n * must not replace the real diagnosis with an ENOENT/EPERM from the cleanup.\n *\n * Lives here rather than in `backup-service.ts` for the reason at the top of\n * this file — that module's top-level `execa` import makes it unloadable under\n * jest, so anything placed there cannot be unit-tested.\n *\n * `remove` deletes one file and throws if it cannot; `exists` reports presence.\n */\nexport function discardPartialDumpWith(\n exists: (file: string) => boolean,\n remove: (file: string) => void,\n file: string\n): void {\n try {\n if (exists(file)) remove(file);\n } catch {\n // Ignored: see above.\n }\n}\n","/**\n * Backup / restore orchestration: thin, well-typed wrappers around\n * `pg_dump` and `pg_restore`, plus binary detection, version pre-flight,\n * and upload/prune against a storage backend.\n *\n * The pure argument- and policy-logic lives in `pg-tools.ts` and\n * `retention.ts`; this module is the impure edge (spawns processes, talks\n * to Postgres and storage) and is intentionally kept small.\n */\nimport fs from \"fs\";\nimport os from \"os\";\nimport path from \"path\";\nimport { execa } from \"execa\";\nimport type { StorageController } from \"@rebasepro/server\";\nimport { resolveLocalBin } from \"../cli-helpers\";\nimport {\n BackupDestination,\n buildBackupFilename,\n buildPgDumpArgs,\n buildPgDumpallGlobalsArgs,\n buildRowSecurityPgOptions,\n diagnoseRowSecurityDumpFailure,\n type RowSecurityIdentity,\n buildPgRestoreArgs,\n buildPgRestoreListArgs,\n checkToolServerCompatibility,\n globalsFileForDump,\n joinStorageKey,\n parsePgToolMajor,\n parseBackupTimestamp,\n serverVersionNumToMajor,\n VersionCompatibility\n} from \"./pg-tools\";\nimport { BackupObject, RetentionOptions, selectBackupsToPrune } from \"./retention\";\nimport { applyGlobalsWith, discardPartialDumpWith, pruneWith } from \"./backup-logic\";\n\n/**\n * Remove a dump artifact abandoned by a failed tool run. See\n * {@link discardPartialDumpWith} for why this exists and why the logic lives\n * in the execa-free module.\n */\nexport function discardPartialDump(file: string): void {\n discardPartialDumpWith(\n (f) => fs.existsSync(f),\n (f) => fs.unlinkSync(f),\n file\n );\n}\n\nexport class BackupToolError extends Error {\n constructor(message: string, readonly hint?: string) {\n super(message);\n this.name = \"BackupToolError\";\n }\n}\n\n/** Locate `pg_dump` / `pg_restore` / `pg_dumpall`, honouring an env override. */\nexport function resolvePgBinary(\n tool: \"pg_dump\" | \"pg_restore\" | \"pg_dumpall\",\n env: Record<string, string | undefined> = process.env\n): string | null {\n const overrideVar =\n tool === \"pg_dump\" ? env.PG_DUMP_PATH\n : tool === \"pg_restore\" ? env.PG_RESTORE_PATH\n : env.PG_DUMPALL_PATH;\n if (overrideVar && fs.existsSync(overrideVar)) return overrideVar;\n return resolveLocalBin(tool);\n}\n\n/** Run `<bin> --version` and extract the major version. */\nexport async function detectToolMajor(bin: string): Promise<number | null> {\n try {\n const { stdout } = await execa(bin, [\"--version\"]);\n return parsePgToolMajor(stdout);\n } catch {\n return null;\n }\n}\n\n/** Query the server for its major version via `server_version_num`. */\nexport async function getServerVersionMajor(connectionString: string): Promise<number | null> {\n const { Client } = await import(\"pg\");\n const client = new Client({ connectionString });\n await client.connect();\n try {\n const res = await client.query(\"SHOW server_version_num\");\n const raw = res.rows?.[0]?.server_version_num;\n return serverVersionNumToMajor(raw);\n } finally {\n await client.end();\n }\n}\n\nexport interface PreflightResult extends VersionCompatibility {\n bin: string;\n toolMajor: number | null;\n serverMajor: number | null;\n}\n\n/**\n * Verify the requested client tool exists and its major version is\n * compatible with the live server. Throws {@link BackupToolError} — with a\n * doctor-style hint — when the binary is missing.\n */\nexport async function preflight(\n tool: \"pg_dump\" | \"pg_restore\",\n connectionString: string,\n env: Record<string, string | undefined> = process.env\n): Promise<PreflightResult> {\n const bin = resolvePgBinary(tool, env);\n if (!bin) {\n throw new BackupToolError(\n `Could not find the '${tool}' binary.`,\n `Install the PostgreSQL client tools (e.g. 'brew install libpq' or 'apt-get install postgresql-client'), ` +\n `or set ${tool === \"pg_dump\" ? \"PG_DUMP_PATH\" : \"PG_RESTORE_PATH\"} to its full path.`\n );\n }\n const [toolMajor, serverMajor] = await Promise.all([\n detectToolMajor(bin),\n getServerVersionMajor(connectionString)\n ]);\n const compat = checkToolServerCompatibility(toolMajor, serverMajor);\n return { bin, toolMajor, serverMajor, ...compat };\n}\n\nexport interface BackupResult {\n /** Absolute path of the produced dump file on local disk. */\n localFile: string;\n fileName: string;\n sizeBytes: number;\n /**\n * Absolute path of the `.globals.sql` sidecar holding cluster-wide roles\n * (present unless globals capture was disabled or unavailable).\n */\n globalsFile?: string;\n globalsSizeBytes?: number;\n}\n\n/**\n * Produce a custom-format dump on local disk. When `outDir` is omitted the\n * file is written to the OS temp directory (used by the upload path, which\n * cleans it up afterwards).\n *\n * Alongside the `-Fc` dump it writes a `<name>.globals.sql` sidecar via\n * `pg_dumpall --globals-only` so the roles the dump's GRANT/RLS statements\n * depend on can be recreated on restore. Set `includeGlobals: false` to skip\n * it (e.g. when the caller has no privilege to read cluster globals).\n */\nexport async function createDump(opts: {\n connectionString: string;\n dbName: string;\n outDir?: string;\n fileName?: string;\n excludeSchemas?: string[];\n noOwner?: boolean;\n inheritStdio?: boolean;\n includeGlobals?: boolean;\n env?: Record<string, string | undefined>;\n /**\n * Dump with row security left on, reading as this identity.\n *\n * The escape hatch for a managed Postgres, where the dumping role owns\n * nothing and has no `BYPASSRLS`. Off by default, and deliberately so: with\n * row security on, `pg_dump` stops erroring on rows it cannot see and\n * simply omits them. See {@link RowSecurityIdentity}.\n */\n rowSecurity?: RowSecurityIdentity;\n}): Promise<BackupResult> {\n const env = opts.env ?? process.env;\n const bin = resolvePgBinary(\"pg_dump\", env);\n if (!bin) {\n throw new BackupToolError(\n \"Could not find the 'pg_dump' binary.\",\n \"Install the PostgreSQL client tools or set PG_DUMP_PATH.\"\n );\n }\n\n const fileName = opts.fileName ?? buildBackupFilename(opts.dbName);\n const outDir = opts.outDir ?? fs.mkdtempSync(path.join(os.tmpdir(), \"rebase-backup-\"));\n fs.mkdirSync(outDir, { recursive: true });\n const localFile = path.join(outDir, fileName);\n\n const args = buildPgDumpArgs({\n connectionString: opts.connectionString,\n outFile: localFile,\n excludeSchemas: opts.excludeSchemas,\n noOwner: opts.noOwner,\n rowSecurity: opts.rowSecurity\n });\n\n // `PGOPTIONS` is the only way to set a GUC on a tool that takes no SQL.\n // Built from the same object that added `--enable-row-security`, so the\n // flag cannot travel without the identity that makes it safe.\n const dumpEnv: Record<string, string> = { ...(env as Record<string, string>) };\n if (opts.rowSecurity) {\n dumpEnv.PGOPTIONS = [env.PGOPTIONS, buildRowSecurityPgOptions(opts.rowSecurity)]\n .filter(Boolean).join(\" \");\n }\n\n try {\n await execa(bin, args, {\n stdio: opts.inheritStdio ? \"inherit\" : \"pipe\",\n env: dumpEnv\n });\n } catch (error) {\n // pg_dump creates its `--file=` target before it finishes connecting,\n // so any failure here — a URL libpq rejects, a dropped connection, a\n // full disk — leaves a 0-byte file behind that nothing else removed.\n // That corpse is not inert: `rebase db backups list` shows it as an\n // ordinary entry, and `selectBackupsToPrune` ranks by timestamp alone,\n // so it occupies a protected `keepMinimum` slot and can push a real\n // backup out of retention. A missing backup is honest; an empty file\n // that reads as a backup is not.\n discardPartialDump(localFile);\n // The RLS failure names a table and no cause. Replace it with the\n // cause and the two ways out; anything else is re-thrown untouched.\n const diagnosis = diagnoseRowSecurityDumpFailure(error);\n if (!diagnosis) throw error;\n throw new BackupToolError(\n diagnosis,\n \"Run `rebase db backup --help` for the flag, and read what it says about partial dumps.\"\n );\n }\n\n const sizeBytes = fs.existsSync(localFile) ? fs.statSync(localFile).size : 0;\n\n const result: BackupResult = { localFile, fileName, sizeBytes };\n\n if (opts.includeGlobals !== false) {\n const dumpallBin = resolvePgBinary(\"pg_dumpall\", env);\n if (!dumpallBin) {\n throw new BackupToolError(\n \"Could not find the 'pg_dumpall' binary needed to capture cluster roles.\",\n \"Install the PostgreSQL client tools or set PG_DUMPALL_PATH. \" +\n \"To take a role-incomplete backup anyway, pass includeGlobals: false.\"\n );\n }\n const globalsFile = globalsFileForDump(localFile);\n try {\n await execa(\n dumpallBin,\n buildPgDumpallGlobalsArgs({ connectionString: opts.connectionString, outFile: globalsFile }),\n { stdio: opts.inheritStdio ? \"inherit\" : \"pipe\", env: { ...(env as Record<string, string>) } }\n );\n } catch (error) {\n // Same reasoning as the pg_dump catch above, and the dump goes with\n // it: the pair is uploaded and pruned together, so a dump whose\n // roles sidecar is missing restores without the roles its GRANT and\n // RLS statements need — the exact failure the sidecar exists to\n // prevent. Leaving half a pair on disk would look like a backup.\n discardPartialDump(globalsFile);\n discardPartialDump(localFile);\n throw error;\n }\n result.globalsFile = globalsFile;\n result.globalsSizeBytes = fs.existsSync(globalsFile) ? fs.statSync(globalsFile).size : 0;\n }\n\n return result;\n}\n\n/**\n * Cheap integrity check on a freshly written dump: it must be non-empty and\n * `pg_restore --list` must parse its table of contents without error. Used\n * before pruning older backups so a corrupt-but-exit-0 dump never becomes\n * the reason the last good backup is deleted.\n */\nexport async function validateDump(\n localFile: string,\n env: Record<string, string | undefined> = process.env\n): Promise<{ ok: boolean; reason?: string }> {\n if (!fs.existsSync(localFile)) {\n return { ok: false, reason: `Dump file does not exist: ${localFile}` };\n }\n if (fs.statSync(localFile).size === 0) {\n return { ok: false, reason: \"Dump file is empty (0 bytes).\" };\n }\n const bin = resolvePgBinary(\"pg_restore\", env);\n if (!bin) {\n // Can't verify without pg_restore; treat as inconclusive-but-fail so\n // pruning doesn't proceed on an unverified dump.\n return { ok: false, reason: \"Could not find 'pg_restore' to verify the dump.\" };\n }\n try {\n await execa(bin, buildPgRestoreListArgs(localFile), {\n stdio: \"pipe\",\n env: { ...(env as Record<string, string>) }\n });\n return { ok: true };\n } catch (err) {\n return { ok: false, reason: `pg_restore --list failed: ${err instanceof Error ? err.message : String(err)}` };\n }\n}\n\n/**\n * Replay a `pg_dumpall --globals-only` script to recreate cluster roles\n * before a restore, so the dump's GRANT/RLS statements (which reference\n * `rebase_user` and any owner roles) actually apply. Runs statement by\n * statement and tolerates per-statement failures — on a same-cluster restore\n * the roles usually already exist (`CREATE ROLE` → \"already exists\"), and on\n * a managed provider an `ALTER ROLE <superuser>` may be refused; neither\n * should abort role recreation. Returns how many statements applied vs were\n * skipped.\n */\nexport async function applyGlobals(\n connectionString: string,\n globalsSql: string,\n log: (message: string) => void = () => {}\n): Promise<{ applied: number; skipped: number }> {\n const { Client } = await import(\"pg\");\n const client = new Client({ connectionString });\n await client.connect();\n try {\n return await applyGlobalsWith(\n async (sql) => { await client.query(sql); },\n globalsSql,\n log\n );\n } finally {\n await client.end();\n }\n}\n\n/**\n * Restore a custom-format dump into the database named by\n * `connectionString`. Destructive when `clean` is set (drops objects\n * first). Never called automatically — the CLI gates it behind explicit\n * confirmation.\n *\n * Runs with `--exit-on-error` by default: a restore that logs-and-continues\n * past a failed GRANT (because a role was missing) reports success with RLS\n * un-enforced. Callers should recreate roles first (see {@link applyGlobals})\n * and only set `exitOnError: false` deliberately.\n */\nexport async function restoreDump(opts: {\n connectionString: string;\n inputFile: string;\n clean?: boolean;\n noOwner?: boolean;\n exitOnError?: boolean;\n inheritStdio?: boolean;\n env?: Record<string, string | undefined>;\n}): Promise<void> {\n const env = opts.env ?? process.env;\n const bin = resolvePgBinary(\"pg_restore\", env);\n if (!bin) {\n throw new BackupToolError(\n \"Could not find the 'pg_restore' binary.\",\n \"Install the PostgreSQL client tools or set PG_RESTORE_PATH.\"\n );\n }\n const args = buildPgRestoreArgs({\n connectionString: opts.connectionString,\n inputFile: opts.inputFile,\n clean: opts.clean,\n noOwner: opts.noOwner,\n exitOnError: opts.exitOnError\n });\n await execa(bin, args, {\n stdio: opts.inheritStdio ? \"inherit\" : \"pipe\",\n env: { ...(env as Record<string, string>) }\n });\n}\n\n/**\n * Create a database (if absent) by connecting to the maintenance\n * `postgres` database. Used by `restore --create-db`.\n */\nexport async function ensureDatabaseExists(\n adminConnectionString: string,\n dbName: string\n): Promise<boolean> {\n const { Client } = await import(\"pg\");\n const parsed = new URL(adminConnectionString);\n parsed.pathname = \"/postgres\";\n const client = new Client({ connectionString: parsed.toString() });\n await client.connect();\n try {\n const existing = await client.query(\"SELECT 1 FROM pg_database WHERE datname = $1\", [dbName]);\n if (existing.rowCount && existing.rowCount > 0) {\n return false;\n }\n // Identifier can't be parameterised; guard against injection.\n if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(dbName)) {\n throw new BackupToolError(`Invalid database name: \"${dbName}\"`);\n }\n await client.query(`CREATE DATABASE \"${dbName}\"`);\n return true;\n } finally {\n await client.end();\n }\n}\n\n/**\n * Upload a local dump to object storage under the destination's prefix.\n * Backups may contain secrets/PII, so the object is written with a\n * private/octet-stream content type; bucket-level ACLs must stay private.\n */\nexport async function uploadBackup(\n storage: StorageController,\n localFile: string,\n dest: Extract<BackupDestination, { kind: \"s3\" | \"gcs\" }>\n): Promise<{ key: string; storageUrl: string }> {\n const fileName = path.basename(localFile);\n const key = joinStorageKey(dest.prefix, fileName);\n const buffer = fs.readFileSync(localFile);\n const file = new File([buffer], fileName, { type: \"application/octet-stream\" });\n const result = await storage.putObject({\n file,\n key,\n bucket: dest.bucket,\n metadata: { \"rebase-backup\": \"1\" }\n });\n return { key, storageUrl: result.storageUrl };\n}\n\n/**\n * List existing backups at a destination. For local destinations reads the\n * directory; for object storage lists the prefix via the controller.\n */\nexport async function listBackups(\n dest: BackupDestination,\n storage?: StorageController\n): Promise<BackupObject[]> {\n if (dest.kind === \"local\") {\n if (!fs.existsSync(dest.path)) return [];\n const stat = fs.statSync(dest.path);\n const dir = stat.isDirectory() ? dest.path : path.dirname(dest.path);\n return fs\n .readdirSync(dir)\n .filter((f) => f.endsWith(\".dump\"))\n .map((f) => {\n const full = path.join(dir, f);\n const stats = fs.statSync(full);\n const createdAt = parseBackupTimestamp(f) ?? stats.mtime;\n return { key: full, createdAt, sizeBytes: stats.size };\n })\n .sort((a, b) => (b.createdAt?.getTime() ?? 0) - (a.createdAt?.getTime() ?? 0));\n }\n\n if (!storage) {\n throw new BackupToolError(\n `Listing ${dest.kind} backups requires a configured storage backend.`\n );\n }\n const result = await storage.listObjects(dest.prefix ? `${dest.prefix}/` : \"\", {\n bucket: dest.bucket,\n maxResults: 1000\n });\n return result.items\n .map((item) => item.fullPath)\n .filter((key) => key.endsWith(\".dump\"))\n .map((key) => ({ key, createdAt: parseBackupTimestamp(key) ?? undefined }))\n .sort((a, b) => (b.createdAt?.getTime() ?? 0) - (a.createdAt?.getTime() ?? 0));\n}\n\n/**\n * Apply a retention policy to a destination, deleting the backups selected\n * by {@link selectBackupsToPrune}. Returns the keys that were removed.\n */\nexport async function pruneBackups(\n dest: BackupDestination,\n options: RetentionOptions,\n storage?: StorageController\n): Promise<string[]> {\n const backups = await listBackups(dest, storage);\n const toDelete = selectBackupsToPrune(backups, options);\n\n // Delete each dump together with its `.globals.sql` sidecar (pruneWith\n // handles the pairing + best-effort sidecar). The deleter rejects on a\n // missing key so pruneWith can swallow an absent sidecar.\n const deleteObject =\n dest.kind === \"local\"\n ? async (key: string) => {\n if (!fs.existsSync(key)) throw new Error(`not found: ${key}`);\n fs.unlinkSync(key);\n }\n : async (key: string) => {\n if (!storage) throw new BackupToolError(\"Storage backend required to prune object backups.\");\n await storage.deleteObject(key, dest.bucket);\n };\n\n await pruneWith(toDelete, deleteObject);\n return toDelete;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"backup-service-DA7a6SUV.js","names":[],"sources":["../src/backup/pg-tools.ts","../src/backup/retention.ts","../src/backup/backup-logic.ts","../src/backup/backup-service.ts"],"sourcesContent":["/**\n * Pure helpers for the backup/restore commands.\n *\n * Everything in this file is side-effect free so it can be unit-tested\n * without a live Postgres server, matching the constraint that CI must\n * not require a database.\n */\n\nimport { forLibpq } from \"../utils/connection-string\";\n\n/**\n * A parsed backup destination. `--out` (and the scheduled-backup config)\n * accepts either a local filesystem path or an object-storage URL.\n */\nexport type BackupDestination =\n | { kind: \"local\"; path: string }\n | { kind: \"s3\"; bucket: string; prefix: string }\n | { kind: \"gcs\"; bucket: string; prefix: string };\n\n/**\n * Extract the database name from a Postgres connection string.\n * Returns `null` when the URL has no database path (e.g. bare host).\n */\nexport function parseDbNameFromUrl(connectionString: string): string | null {\n try {\n const parsed = new URL(connectionString);\n const name = parsed.pathname.replace(/^\\//, \"\").trim();\n return name.length > 0 ? name : null;\n } catch {\n // Fall back to a permissive regex for non-URL DSNs.\n const match = connectionString.match(/\\/([^/?]+)(\\?|$)/);\n return match && match[1] ? match[1] : null;\n }\n}\n\n/**\n * Swap the database name in a connection string, preserving credentials,\n * host, port and query params. Used by `--target-db` / `--create-db` so a\n * restore can target a fresh database instead of clobbering the live one.\n */\nexport function withDatabaseName(connectionString: string, dbName: string): string {\n try {\n const parsed = new URL(connectionString);\n parsed.pathname = `/${dbName}`;\n return parsed.toString();\n } catch {\n return connectionString.replace(/\\/([^/?]+)(\\?|$)/, `/${dbName}$2`);\n }\n}\n\n/**\n * Parse the major version out of `pg_dump --version` / `pg_restore --version`\n * output, e.g. `\"pg_dump (PostgreSQL) 16.2\"` → `16`. Handles the pre-10\n * `9.6.x` scheme (returns `9`) and beta strings like `\"17beta1\"`.\n */\nexport function parsePgToolMajor(versionOutput: string): number | null {\n const match = versionOutput.match(/(\\d+)(?:\\.(\\d+))?/);\n if (!match) return null;\n const major = Number(match[1]);\n if (!Number.isFinite(major)) return null;\n // Postgres 9.x used the first two numbers as the major (9.6, 9.4…).\n if (major === 9 && match[2] !== undefined) {\n return 9;\n }\n return major;\n}\n\n/**\n * Convert `SELECT current_setting('server_version_num')` (e.g. `160002`)\n * into a major version (`16`). Also accepts pre-10 encodings like `90603`\n * → `9`.\n */\nexport function serverVersionNumToMajor(versionNum: number | string): number | null {\n const num = typeof versionNum === \"string\" ? Number(versionNum) : versionNum;\n if (!Number.isFinite(num) || num <= 0) return null;\n if (num < 100000) {\n // 9.x scheme: 90603 = 9.6.3\n return Math.floor(num / 10000);\n }\n return Math.floor(num / 10000);\n}\n\nexport interface VersionCompatibility {\n compatible: boolean;\n reason?: string;\n}\n\n/**\n * pg_dump / pg_restore must be **at least** as new as the server they talk\n * to. A newer client against an older server is supported; an older client\n * against a newer server is not and produces corrupt or rejected output.\n */\nexport function checkToolServerCompatibility(\n toolMajor: number | null,\n serverMajor: number | null\n): VersionCompatibility {\n if (toolMajor === null) {\n return { compatible: false, reason: \"Could not determine the client tool version.\" };\n }\n if (serverMajor === null) {\n return { compatible: false, reason: \"Could not determine the Postgres server version.\" };\n }\n if (toolMajor < serverMajor) {\n return {\n compatible: false,\n reason:\n `Client tool is Postgres ${toolMajor} but the server is Postgres ${serverMajor}. ` +\n `pg_dump/pg_restore must be the same major version as the server or newer. ` +\n `Install Postgres ${serverMajor} client tools.`\n };\n }\n return { compatible: true };\n}\n\n/**\n * Build a deterministic, sortable backup file name:\n * `rebase-<db>-<YYYYMMDD>T<HHMMSS>Z.dump`\n *\n * The UTC timestamp is embedded so retention pruning can recover the\n * creation time from the object key alone, without extra metadata.\n */\nexport function buildBackupFilename(dbName: string, date: Date = new Date()): string {\n const iso = date.toISOString(); // 2026-07-14T09:12:03.123Z\n const stamp = iso.replace(/\\.\\d+Z$/, \"Z\").replace(/[-:]/g, \"\");\n const safeDb = dbName.replace(/[^a-zA-Z0-9_-]/g, \"_\");\n return `rebase-${safeDb}-${stamp}.dump`;\n}\n\n/**\n * Recover the creation timestamp encoded in a backup file name by\n * {@link buildBackupFilename}. Returns `null` for names that don't match,\n * so foreign objects in a shared prefix are never pruned.\n */\nexport function parseBackupTimestamp(fileName: string): Date | null {\n const base = fileName.split(\"/\").pop() ?? fileName;\n const match = base.match(/-(\\d{8})T(\\d{6})Z\\.dump$/);\n if (!match) return null;\n const [, ymd, hms] = match;\n const iso =\n `${ymd.slice(0, 4)}-${ymd.slice(4, 6)}-${ymd.slice(6, 8)}` +\n `T${hms.slice(0, 2)}:${hms.slice(2, 4)}:${hms.slice(4, 6)}Z`;\n const date = new Date(iso);\n return Number.isNaN(date.getTime()) ? null : date;\n}\n\n/**\n * Parse a destination string into a structured {@link BackupDestination}.\n * `s3://bucket/prefix` and `gs://bucket/prefix` map to object storage;\n * anything else is treated as a local path.\n */\nexport function parseBackupDestination(out: string): BackupDestination {\n const s3 = out.match(/^s3:\\/\\/([^/]+)\\/?(.*)$/);\n if (s3) {\n return { kind: \"s3\", bucket: s3[1], prefix: stripTrailingSlash(s3[2]) };\n }\n const gcs = out.match(/^gs:\\/\\/([^/]+)\\/?(.*)$/);\n if (gcs) {\n return { kind: \"gcs\", bucket: gcs[1], prefix: stripTrailingSlash(gcs[2]) };\n }\n return { kind: \"local\", path: out };\n}\n\nfunction stripTrailingSlash(s: string): string {\n return s.replace(/\\/+$/, \"\");\n}\n\n/**\n * Join a storage prefix and a file name without producing a leading or\n * doubled slash.\n */\nexport function joinStorageKey(prefix: string, fileName: string): string {\n const clean = prefix.replace(/^\\/+|\\/+$/g, \"\");\n return clean.length > 0 ? `${clean}/${fileName}` : fileName;\n}\n\n/**\n * The identity `pg_dump` reads rows as, when row security is left on.\n *\n * Not optional, and that is the whole design. `pg_dump --enable-row-security`\n * on its own is the dangerous command in this file: it turns the \"query would\n * be affected by row-level security policy\" *error* into a dump that exits 0\n * and is silently missing every row the dumping role's policies exclude. A\n * backup that looks fine and restores most of your data is worse than one that\n * refused to run.\n *\n * So the flag is unreachable without a subject to evaluate the policies\n * against. Rebase's generated policies read `app.uid` and `app.user_roles`;\n * supplying an admin role satisfies the `admin_full_access` rule and the dump\n * sees everything that rule sees.\n */\nexport interface RowSecurityIdentity {\n /** Written to `app.uid`. Any non-empty value — it is only an audit trail. */\n uid: string;\n /** Written to `app.user_roles`. Must include a role the policies admit. */\n roles: string[];\n}\n\n/**\n * `PGOPTIONS` carrying an identity, for a libpq tool that has no other way to\n * set a GUC.\n *\n * A backslash escape rather than quoting, which is what libpq's `-c` parsing\n * takes: a space inside a value ends the option otherwise, so a role list is\n * comma-joined and never spaced.\n */\nexport function buildRowSecurityPgOptions(identity: RowSecurityIdentity): string {\n const escape = (value: string) => value.replace(/([\\\\ ])/g, \"\\\\$1\");\n return [\n `-c app.uid=${escape(identity.uid)}`,\n `-c app.user_id=${escape(identity.uid)}`,\n `-c app.user_roles=${escape(identity.roles.join(\",\"))}`\n ].join(\" \");\n}\n\n/**\n * Assemble the `pg_dump` argument vector. Uses the custom format (`-Fc`),\n * which is compressed and restorable selectively via `pg_restore`.\n */\nexport function buildPgDumpArgs(opts: {\n connectionString: string;\n outFile: string;\n /** Extra schemas/tables to exclude, e.g. Atlas revision tables. */\n excludeSchemas?: string[];\n /** Number of parallel jobs (directory format only; ignored for -Fc). */\n noOwner?: boolean;\n /**\n * Dump with row security on, as this identity. Omit — which is the default\n * — and `pg_dump` errors rather than skipping rows it cannot see.\n */\n rowSecurity?: RowSecurityIdentity;\n}): string[] {\n const args = [\"--format=custom\", \"--no-password\", `--file=${opts.outFile}`];\n if (opts.noOwner) {\n args.push(\"--no-owner\");\n }\n if (opts.rowSecurity) {\n args.push(\"--enable-row-security\");\n }\n for (const schema of opts.excludeSchemas ?? []) {\n args.push(`--exclude-schema=${schema}`);\n }\n args.push(forLibpq(opts.connectionString));\n return args;\n}\n\n/**\n * Whether a `pg_dump` failure is the row-security one, and what to do about it.\n *\n * The error text names the table and nothing else, so the first read of it is\n * \"why would a backup be affected by RLS at all?\" — the answer being that the\n * dumping role is not the tables' owner and has no `BYPASSRLS`, which is the\n * normal state of the `postgres` user on Cloud SQL, RDS and every other managed\n * Postgres. Nothing about that is visible from the message.\n *\n * Returns `null` for any other failure, so the caller reports it unchanged.\n */\nexport function diagnoseRowSecurityDumpFailure(error: unknown): string | null {\n const text = [\n (error as { stderr?: unknown })?.stderr,\n (error as { message?: unknown })?.message\n ].map(part => (typeof part === \"string\" ? part : \"\")).join(\"\\n\");\n\n if (!/row-level security policy/i.test(text)) return null;\n\n const table = text.match(/for table \"([^\"]+)\"/)?.[1];\n\n return [\n `pg_dump cannot read ${table ? `\"${table}\"` : \"one of the tables\"} because row-level security applies to it.`,\n \"\",\n \" The dumping role is neither the table's owner nor `BYPASSRLS`, which is the normal\",\n \" state of the `postgres` user on Cloud SQL, RDS and other managed Postgres — there is\",\n \" no superuser to hand out.\",\n \"\",\n \" Two ways out:\",\n \"\",\n \" • Grant the dumping role BYPASSRLS, or make it the owner, and run this again. The\",\n \" dump then contains every row, which is what a backup should mean.\",\n \"\",\n \" • Re-run with --enable-row-security to dump as an admin subject instead. Rebase\",\n \" sets `app.uid`/`app.user_roles` so the generated `admin_full_access` policy\",\n \" admits the dump. Read the warning it prints: the result contains exactly the\",\n \" rows those policies admit, and any table whose policies do not include an\",\n \" admin rule comes out short — with no error.\",\n \"\",\n \" Do not reach for a bare `pg_dump --enable-row-security` by hand. Without the\",\n \" settings above it succeeds and silently omits rows.\"\n ].join(\"\\n\");\n}\n\n/**\n * Assemble the `pg_restore` argument vector for a custom-format dump.\n */\nexport function buildPgRestoreArgs(opts: {\n connectionString: string;\n inputFile: string;\n /** Drop objects before recreating them (destructive but idempotent). */\n clean?: boolean;\n /**\n * Abort on the first error instead of logging and continuing. Defaults\n * ON: a restore that silently skips failed GRANT/RLS statements (because\n * a role is missing) \"succeeds\" with RLS un-enforced — a security hole.\n * Fail loudly instead so the operator knows the restore is incomplete.\n */\n exitOnError?: boolean;\n noOwner?: boolean;\n}): string[] {\n const args = [\"--format=custom\", \"--no-password\", `--dbname=${forLibpq(opts.connectionString)}`];\n if (opts.clean) {\n args.push(\"--clean\", \"--if-exists\");\n }\n if (opts.noOwner) {\n args.push(\"--no-owner\");\n }\n // Default to --exit-on-error unless explicitly disabled.\n if (opts.exitOnError !== false) {\n args.push(\"--exit-on-error\");\n }\n args.push(opts.inputFile);\n return args;\n}\n\n/**\n * Assemble the `pg_restore --list` argument vector. Reading a dump's table\n * of contents parses the whole archive without touching a database, so it is\n * a cheap integrity check that the file isn't truncated or corrupt.\n */\nexport function buildPgRestoreListArgs(inputFile: string): string[] {\n return [\"--list\", inputFile];\n}\n\n/**\n * Assemble the `pg_dumpall --globals-only` argument vector. Roles (and other\n * cluster-wide objects) live outside any single database, so a per-database\n * `pg_dump` omits them. Without the `rebase_user` role the RLS GRANT\n * statements in the main dump fail on restore and RLS is silently lost — so\n * every backup captures the globals into a sidecar `.globals.sql`.\n *\n * `--no-role-passwords` keeps role secrets out of the artifact (backups may\n * be shipped off-box); roles are recreated password-less and re-secured by\n * the operator.\n */\nexport function buildPgDumpallGlobalsArgs(opts: {\n connectionString: string;\n outFile: string;\n}): string[] {\n return [\n \"--globals-only\",\n \"--no-role-passwords\",\n \"--no-password\",\n `--file=${opts.outFile}`,\n `--dbname=${forLibpq(opts.connectionString)}`\n ];\n}\n\n/**\n * Derive the globals sidecar path/key for a given `.dump` file. Keeps the\n * two artifacts adjacent so listing, uploading and pruning can find one from\n * the other. A name that doesn't end in `.dump` is returned unchanged with a\n * `.globals.sql` suffix appended.\n */\nexport function globalsFileForDump(dumpPath: string): string {\n return dumpPath.endsWith(\".dump\")\n ? dumpPath.slice(0, -\".dump\".length) + \".globals.sql\"\n : dumpPath + \".globals.sql\";\n}\n\n/**\n * Split a `pg_dumpall --globals-only` script into individual statements.\n * Used when replaying globals on restore so each `CREATE ROLE` / `GRANT`\n * can run independently and a benign \"role already exists\" on one doesn't\n * abort the rest. Drops `--` comment lines and blank statements.\n */\nexport function splitGlobalsStatements(sql: string): string[] {\n const withoutComments = sql\n .split(\"\\n\")\n // `--` comments, and psql meta-commands. pg_dumpall 15+ wraps its output\n // in `\\restrict <token>` / `\\unrestrict <token>`, which are instructions\n // to psql, not SQL. This replay sends statements over a connection\n // instead, so they arrived at the server as `\\restrict …` and came back\n // as `syntax error at or near \"\\\"` — reported to the user as two skipped\n // globals per restore, which reads like roles failed to apply when\n // nothing did. They carry no state worth replaying: dropping them is\n // exactly what a SQL-level consumer should do.\n .filter((line) => {\n const trimmed = line.trim();\n return !trimmed.startsWith(\"--\") && !trimmed.startsWith(\"\\\\\");\n })\n .join(\"\\n\");\n return withoutComments\n .split(\";\")\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n}\n\n/**\n * Resolve the Postgres connection string the backup commands should use,\n * mirroring the precedence the branch command already relies on.\n */\nexport function resolveConnectionString(\n env: Record<string, string | undefined>\n): string | null {\n return env.DATABASE_URL || env.ADMIN_CONNECTION_STRING || null;\n}\n","/**\n * Retention pruning for scheduled backups. Pure logic — no I/O — so it can\n * be exhaustively unit-tested without touching a storage backend.\n */\nimport { parseBackupTimestamp } from \"./pg-tools\";\n\nexport interface BackupObject {\n /** Storage key / file name. */\n key: string;\n /**\n * Creation time. Optional — when absent it is recovered from the\n * timestamp encoded in the key by `buildBackupFilename`.\n */\n createdAt?: Date;\n /**\n * Size on disk, when the lister knows it (local destinations always do).\n *\n * Reported so an empty artifact cannot masquerade as a backup in\n * `rebase db backups list`. Deliberately NOT consulted by\n * {@link selectBackupsToPrune}: retention decides by age, and a rule that\n * silently skipped small files would make a genuinely tiny dump immortal.\n * Visibility is the fix here; the artifacts themselves are now removed at\n * the point of failure by `discardPartialDump`.\n */\n sizeBytes?: number;\n}\n\nexport interface RetentionOptions {\n /** Delete backups older than this many days. `0`/negative disables age pruning. */\n retentionDays?: number;\n /**\n * Always keep at least this many of the most recent backups, even if\n * they are older than `retentionDays`. Guards against wiping every\n * backup after a long outage. Default: 0 (no minimum).\n */\n keepMinimum?: number;\n /** Reference \"now\" — injectable for deterministic tests. */\n now?: Date;\n}\n\n/**\n * Decide which backups to delete under the given retention policy.\n *\n * Rules, applied in order:\n * 1. Objects whose key isn't a recognised rebase backup (no parseable\n * timestamp and no `createdAt`) are never selected — foreign files in a\n * shared bucket are left untouched.\n * 2. The `keepMinimum` newest backups are always retained.\n * 3. Of the remainder, any older than `retentionDays` are selected for\n * deletion.\n *\n * Returns the keys to delete, oldest first.\n */\nexport function selectBackupsToPrune(\n backups: BackupObject[],\n options: RetentionOptions\n): string[] {\n const retentionDays = options.retentionDays ?? 0;\n const keepMinimum = Math.max(0, options.keepMinimum ?? 0);\n const now = options.now ?? new Date();\n\n if (retentionDays <= 0) {\n return [];\n }\n\n // Resolve a timestamp for every object; drop the ones we can't date.\n const dated = backups\n .map((b) => {\n const createdAt = b.createdAt ?? parseBackupTimestamp(b.key);\n return createdAt ? { key: b.key, createdAt } : null;\n })\n .filter((b): b is { key: string; createdAt: Date } => b !== null);\n\n // Newest first so the first `keepMinimum` are the protected ones.\n dated.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());\n\n const cutoff = now.getTime() - retentionDays * 24 * 60 * 60 * 1000;\n\n const toDelete = dated\n .slice(keepMinimum)\n .filter((b) => b.createdAt.getTime() < cutoff);\n\n // Return oldest first for predictable, readable logs.\n toDelete.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());\n return toDelete.map((b) => b.key);\n}\n","/**\n * Pure-ish orchestration for the backup/restore paths, kept free of `execa`\n * and `pg` value imports so it can be unit-tested under jest (the impure\n * edges — spawning processes, opening connections — live in\n * `backup-service.ts`, which is vitest/runtime only).\n *\n * The functions here take their side-effecting dependency as an argument\n * (a statement runner, an object deleter) so tests can inject fakes.\n */\nimport { globalsFileForDump, splitGlobalsStatements } from \"./pg-tools\";\n\n/**\n * Replay a `pg_dumpall --globals-only` script one statement at a time,\n * tolerating per-statement failures. On a same-cluster restore the roles\n * usually already exist (`CREATE ROLE` → \"already exists\") and on a managed\n * provider an `ALTER ROLE <superuser>` may be refused; neither should abort\n * recreation of the roles that *are* missing. Returns how many statements\n * applied vs were skipped.\n *\n * `runStatement` executes one SQL statement and rejects on error.\n */\nexport async function applyGlobalsWith(\n runStatement: (sql: string) => Promise<void>,\n globalsSql: string,\n log: (message: string) => void = () => {}\n): Promise<{ applied: number; skipped: number }> {\n let applied = 0;\n let skipped = 0;\n for (const statement of splitGlobalsStatements(globalsSql)) {\n try {\n await runStatement(statement);\n applied++;\n } catch (err) {\n skipped++;\n const firstLine = statement.split(\"\\n\")[0].slice(0, 80);\n log(` • Skipped global: ${firstLine} (${err instanceof Error ? err.message : String(err)})`);\n }\n }\n return { applied, skipped };\n}\n\n/**\n * Delete each pruned dump together with its `.globals.sql` roles sidecar, so\n * pruning never orphans the roles file. The sidecar is best-effort — older\n * backups predate it, so a missing-sidecar failure is swallowed while a\n * failure deleting the dump itself propagates.\n *\n * `deleteObject` removes one key and rejects if it cannot (e.g. not found).\n */\nexport async function pruneWith(\n keys: string[],\n deleteObject: (key: string) => Promise<void>\n): Promise<void> {\n for (const key of keys) {\n await deleteObject(key);\n try {\n await deleteObject(globalsFileForDump(key));\n } catch {\n // Sidecar may not exist for older backups — ignore.\n }\n }\n}\n\n/**\n * Remove a dump artifact abandoned by a failed tool run.\n *\n * `pg_dump` creates its `--file=` target before it finishes connecting, so any\n * failure — a URL libpq rejects, a dropped connection, a full disk — leaves a\n * 0-byte file behind. That corpse is not inert: `rebase db backups list` used\n * to show it as an ordinary entry, and `selectBackupsToPrune` ranks by\n * timestamp alone, so it occupies a protected `keepMinimum` slot and can push\n * a real backup out of retention.\n *\n * Best-effort on purpose: the caller is already throwing, and a failed unlink\n * must not replace the real diagnosis with an ENOENT/EPERM from the cleanup.\n *\n * Lives here rather than in `backup-service.ts` for the reason at the top of\n * this file — that module's top-level `execa` import makes it unloadable under\n * jest, so anything placed there cannot be unit-tested.\n *\n * `remove` deletes one file and throws if it cannot; `exists` reports presence.\n */\nexport function discardPartialDumpWith(\n exists: (file: string) => boolean,\n remove: (file: string) => void,\n file: string\n): void {\n try {\n if (exists(file)) remove(file);\n } catch {\n // Ignored: see above.\n }\n}\n","/**\n * Backup / restore orchestration: thin, well-typed wrappers around\n * `pg_dump` and `pg_restore`, plus binary detection, version pre-flight,\n * and upload/prune against a storage backend.\n *\n * The pure argument- and policy-logic lives in `pg-tools.ts` and\n * `retention.ts`; this module is the impure edge (spawns processes, talks\n * to Postgres and storage) and is intentionally kept small.\n */\nimport fs from \"fs\";\nimport os from \"os\";\nimport path from \"path\";\nimport { execa } from \"execa\";\nimport type { StorageController } from \"@rebasepro/server\";\nimport { resolveLocalBin } from \"../cli-helpers\";\nimport {\n BackupDestination,\n buildBackupFilename,\n buildPgDumpArgs,\n buildPgDumpallGlobalsArgs,\n buildRowSecurityPgOptions,\n diagnoseRowSecurityDumpFailure,\n type RowSecurityIdentity,\n buildPgRestoreArgs,\n buildPgRestoreListArgs,\n checkToolServerCompatibility,\n globalsFileForDump,\n joinStorageKey,\n parsePgToolMajor,\n parseBackupTimestamp,\n serverVersionNumToMajor,\n VersionCompatibility\n} from \"./pg-tools\";\nimport { BackupObject, RetentionOptions, selectBackupsToPrune } from \"./retention\";\nimport { applyGlobalsWith, discardPartialDumpWith, pruneWith } from \"./backup-logic\";\n\n/**\n * Remove a dump artifact abandoned by a failed tool run. See\n * {@link discardPartialDumpWith} for why this exists and why the logic lives\n * in the execa-free module.\n */\nexport function discardPartialDump(file: string): void {\n discardPartialDumpWith(\n (f) => fs.existsSync(f),\n (f) => fs.unlinkSync(f),\n file\n );\n}\n\nexport class BackupToolError extends Error {\n constructor(message: string, readonly hint?: string) {\n super(message);\n this.name = \"BackupToolError\";\n }\n}\n\n/** Locate `pg_dump` / `pg_restore` / `pg_dumpall`, honouring an env override. */\nexport function resolvePgBinary(\n tool: \"pg_dump\" | \"pg_restore\" | \"pg_dumpall\",\n env: Record<string, string | undefined> = process.env\n): string | null {\n const overrideVar =\n tool === \"pg_dump\" ? env.PG_DUMP_PATH\n : tool === \"pg_restore\" ? env.PG_RESTORE_PATH\n : env.PG_DUMPALL_PATH;\n if (overrideVar && fs.existsSync(overrideVar)) return overrideVar;\n return resolveLocalBin(tool);\n}\n\n/** Run `<bin> --version` and extract the major version. */\nexport async function detectToolMajor(bin: string): Promise<number | null> {\n try {\n const { stdout } = await execa(bin, [\"--version\"]);\n return parsePgToolMajor(stdout);\n } catch {\n return null;\n }\n}\n\n/** Query the server for its major version via `server_version_num`. */\nexport async function getServerVersionMajor(connectionString: string): Promise<number | null> {\n const { Client } = await import(\"pg\");\n const client = new Client({ connectionString });\n await client.connect();\n try {\n const res = await client.query(\"SHOW server_version_num\");\n const raw = res.rows?.[0]?.server_version_num;\n return serverVersionNumToMajor(raw);\n } finally {\n await client.end();\n }\n}\n\nexport interface PreflightResult extends VersionCompatibility {\n bin: string;\n toolMajor: number | null;\n serverMajor: number | null;\n}\n\n/**\n * Verify the requested client tool exists and its major version is\n * compatible with the live server. Throws {@link BackupToolError} — with a\n * doctor-style hint — when the binary is missing.\n */\nexport async function preflight(\n tool: \"pg_dump\" | \"pg_restore\",\n connectionString: string,\n env: Record<string, string | undefined> = process.env\n): Promise<PreflightResult> {\n const bin = resolvePgBinary(tool, env);\n if (!bin) {\n throw new BackupToolError(\n `Could not find the '${tool}' binary.`,\n `Install the PostgreSQL client tools (e.g. 'brew install libpq' or 'apt-get install postgresql-client'), ` +\n `or set ${tool === \"pg_dump\" ? \"PG_DUMP_PATH\" : \"PG_RESTORE_PATH\"} to its full path.`\n );\n }\n const [toolMajor, serverMajor] = await Promise.all([\n detectToolMajor(bin),\n getServerVersionMajor(connectionString)\n ]);\n const compat = checkToolServerCompatibility(toolMajor, serverMajor);\n return { bin, toolMajor, serverMajor, ...compat };\n}\n\nexport interface BackupResult {\n /** Absolute path of the produced dump file on local disk. */\n localFile: string;\n fileName: string;\n sizeBytes: number;\n /**\n * Absolute path of the `.globals.sql` sidecar holding cluster-wide roles\n * (present unless globals capture was disabled or unavailable).\n */\n globalsFile?: string;\n globalsSizeBytes?: number;\n}\n\n/**\n * Produce a custom-format dump on local disk. When `outDir` is omitted the\n * file is written to the OS temp directory (used by the upload path, which\n * cleans it up afterwards).\n *\n * Alongside the `-Fc` dump it writes a `<name>.globals.sql` sidecar via\n * `pg_dumpall --globals-only` so the roles the dump's GRANT/RLS statements\n * depend on can be recreated on restore. Set `includeGlobals: false` to skip\n * it (e.g. when the caller has no privilege to read cluster globals).\n */\nexport async function createDump(opts: {\n connectionString: string;\n dbName: string;\n outDir?: string;\n fileName?: string;\n excludeSchemas?: string[];\n noOwner?: boolean;\n inheritStdio?: boolean;\n includeGlobals?: boolean;\n env?: Record<string, string | undefined>;\n /**\n * Dump with row security left on, reading as this identity.\n *\n * The escape hatch for a managed Postgres, where the dumping role owns\n * nothing and has no `BYPASSRLS`. Off by default, and deliberately so: with\n * row security on, `pg_dump` stops erroring on rows it cannot see and\n * simply omits them. See {@link RowSecurityIdentity}.\n */\n rowSecurity?: RowSecurityIdentity;\n}): Promise<BackupResult> {\n const env = opts.env ?? process.env;\n const bin = resolvePgBinary(\"pg_dump\", env);\n if (!bin) {\n throw new BackupToolError(\n \"Could not find the 'pg_dump' binary.\",\n \"Install the PostgreSQL client tools or set PG_DUMP_PATH.\"\n );\n }\n\n const fileName = opts.fileName ?? buildBackupFilename(opts.dbName);\n const outDir = opts.outDir ?? fs.mkdtempSync(path.join(os.tmpdir(), \"rebase-backup-\"));\n fs.mkdirSync(outDir, { recursive: true });\n const localFile = path.join(outDir, fileName);\n\n const args = buildPgDumpArgs({\n connectionString: opts.connectionString,\n outFile: localFile,\n excludeSchemas: opts.excludeSchemas,\n noOwner: opts.noOwner,\n rowSecurity: opts.rowSecurity\n });\n\n // `PGOPTIONS` is the only way to set a GUC on a tool that takes no SQL.\n // Built from the same object that added `--enable-row-security`, so the\n // flag cannot travel without the identity that makes it safe.\n const dumpEnv: Record<string, string> = { ...(env as Record<string, string>) };\n if (opts.rowSecurity) {\n dumpEnv.PGOPTIONS = [env.PGOPTIONS, buildRowSecurityPgOptions(opts.rowSecurity)]\n .filter(Boolean).join(\" \");\n }\n\n try {\n await execa(bin, args, {\n stdio: opts.inheritStdio ? \"inherit\" : \"pipe\",\n env: dumpEnv\n });\n } catch (error) {\n // pg_dump creates its `--file=` target before it finishes connecting,\n // so any failure here — a URL libpq rejects, a dropped connection, a\n // full disk — leaves a 0-byte file behind that nothing else removed.\n // That corpse is not inert: `rebase db backups list` shows it as an\n // ordinary entry, and `selectBackupsToPrune` ranks by timestamp alone,\n // so it occupies a protected `keepMinimum` slot and can push a real\n // backup out of retention. A missing backup is honest; an empty file\n // that reads as a backup is not.\n discardPartialDump(localFile);\n // The RLS failure names a table and no cause. Replace it with the\n // cause and the two ways out; anything else is re-thrown untouched.\n const diagnosis = diagnoseRowSecurityDumpFailure(error);\n if (!diagnosis) throw error;\n throw new BackupToolError(\n diagnosis,\n \"Run `rebase db backup --help` for the flag, and read what it says about partial dumps.\"\n );\n }\n\n const sizeBytes = fs.existsSync(localFile) ? fs.statSync(localFile).size : 0;\n\n const result: BackupResult = { localFile, fileName, sizeBytes };\n\n if (opts.includeGlobals !== false) {\n const dumpallBin = resolvePgBinary(\"pg_dumpall\", env);\n if (!dumpallBin) {\n throw new BackupToolError(\n \"Could not find the 'pg_dumpall' binary needed to capture cluster roles.\",\n \"Install the PostgreSQL client tools or set PG_DUMPALL_PATH. \" +\n \"To take a role-incomplete backup anyway, pass includeGlobals: false.\"\n );\n }\n const globalsFile = globalsFileForDump(localFile);\n try {\n await execa(\n dumpallBin,\n buildPgDumpallGlobalsArgs({ connectionString: opts.connectionString, outFile: globalsFile }),\n { stdio: opts.inheritStdio ? \"inherit\" : \"pipe\", env: { ...(env as Record<string, string>) } }\n );\n } catch (error) {\n // Same reasoning as the pg_dump catch above, and the dump goes with\n // it: the pair is uploaded and pruned together, so a dump whose\n // roles sidecar is missing restores without the roles its GRANT and\n // RLS statements need — the exact failure the sidecar exists to\n // prevent. Leaving half a pair on disk would look like a backup.\n discardPartialDump(globalsFile);\n discardPartialDump(localFile);\n throw error;\n }\n result.globalsFile = globalsFile;\n result.globalsSizeBytes = fs.existsSync(globalsFile) ? fs.statSync(globalsFile).size : 0;\n }\n\n return result;\n}\n\n/**\n * Cheap integrity check on a freshly written dump: it must be non-empty and\n * `pg_restore --list` must parse its table of contents without error. Used\n * before pruning older backups so a corrupt-but-exit-0 dump never becomes\n * the reason the last good backup is deleted.\n */\nexport async function validateDump(\n localFile: string,\n env: Record<string, string | undefined> = process.env\n): Promise<{ ok: boolean; reason?: string }> {\n if (!fs.existsSync(localFile)) {\n return { ok: false, reason: `Dump file does not exist: ${localFile}` };\n }\n if (fs.statSync(localFile).size === 0) {\n return { ok: false, reason: \"Dump file is empty (0 bytes).\" };\n }\n const bin = resolvePgBinary(\"pg_restore\", env);\n if (!bin) {\n // Can't verify without pg_restore; treat as inconclusive-but-fail so\n // pruning doesn't proceed on an unverified dump.\n return { ok: false, reason: \"Could not find 'pg_restore' to verify the dump.\" };\n }\n try {\n await execa(bin, buildPgRestoreListArgs(localFile), {\n stdio: \"pipe\",\n env: { ...(env as Record<string, string>) }\n });\n return { ok: true };\n } catch (err) {\n return { ok: false, reason: `pg_restore --list failed: ${err instanceof Error ? err.message : String(err)}` };\n }\n}\n\n/**\n * Replay a `pg_dumpall --globals-only` script to recreate cluster roles\n * before a restore, so the dump's GRANT/RLS statements (which reference\n * `rebase_user` and any owner roles) actually apply. Runs statement by\n * statement and tolerates per-statement failures — on a same-cluster restore\n * the roles usually already exist (`CREATE ROLE` → \"already exists\"), and on\n * a managed provider an `ALTER ROLE <superuser>` may be refused; neither\n * should abort role recreation. Returns how many statements applied vs were\n * skipped.\n */\nexport async function applyGlobals(\n connectionString: string,\n globalsSql: string,\n log: (message: string) => void = () => {}\n): Promise<{ applied: number; skipped: number }> {\n const { Client } = await import(\"pg\");\n const client = new Client({ connectionString });\n await client.connect();\n try {\n return await applyGlobalsWith(\n async (sql) => { await client.query(sql); },\n globalsSql,\n log\n );\n } finally {\n await client.end();\n }\n}\n\n/**\n * Restore a custom-format dump into the database named by\n * `connectionString`. Destructive when `clean` is set (drops objects\n * first). Never called automatically — the CLI gates it behind explicit\n * confirmation.\n *\n * Runs with `--exit-on-error` by default: a restore that logs-and-continues\n * past a failed GRANT (because a role was missing) reports success with RLS\n * un-enforced. Callers should recreate roles first (see {@link applyGlobals})\n * and only set `exitOnError: false` deliberately.\n */\nexport async function restoreDump(opts: {\n connectionString: string;\n inputFile: string;\n clean?: boolean;\n noOwner?: boolean;\n exitOnError?: boolean;\n inheritStdio?: boolean;\n env?: Record<string, string | undefined>;\n}): Promise<void> {\n const env = opts.env ?? process.env;\n const bin = resolvePgBinary(\"pg_restore\", env);\n if (!bin) {\n throw new BackupToolError(\n \"Could not find the 'pg_restore' binary.\",\n \"Install the PostgreSQL client tools or set PG_RESTORE_PATH.\"\n );\n }\n const args = buildPgRestoreArgs({\n connectionString: opts.connectionString,\n inputFile: opts.inputFile,\n clean: opts.clean,\n noOwner: opts.noOwner,\n exitOnError: opts.exitOnError\n });\n await execa(bin, args, {\n stdio: opts.inheritStdio ? \"inherit\" : \"pipe\",\n env: { ...(env as Record<string, string>) }\n });\n}\n\n/**\n * Create a database (if absent) by connecting to the maintenance\n * `postgres` database. Used by `restore --create-db`.\n */\nexport async function ensureDatabaseExists(\n adminConnectionString: string,\n dbName: string\n): Promise<boolean> {\n const { Client } = await import(\"pg\");\n const parsed = new URL(adminConnectionString);\n parsed.pathname = \"/postgres\";\n const client = new Client({ connectionString: parsed.toString() });\n await client.connect();\n try {\n const existing = await client.query(\"SELECT 1 FROM pg_database WHERE datname = $1\", [dbName]);\n if (existing.rowCount && existing.rowCount > 0) {\n return false;\n }\n // Identifier can't be parameterised; guard against injection.\n if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(dbName)) {\n throw new BackupToolError(`Invalid database name: \"${dbName}\"`);\n }\n await client.query(`CREATE DATABASE \"${dbName}\"`);\n return true;\n } finally {\n await client.end();\n }\n}\n\n/**\n * Upload a local dump to object storage under the destination's prefix.\n * Backups may contain secrets/PII, so the object is written with a\n * private/octet-stream content type; bucket-level ACLs must stay private.\n */\nexport async function uploadBackup(\n storage: StorageController,\n localFile: string,\n dest: Extract<BackupDestination, { kind: \"s3\" | \"gcs\" }>\n): Promise<{ key: string; storageUrl: string }> {\n const fileName = path.basename(localFile);\n const key = joinStorageKey(dest.prefix, fileName);\n const buffer = fs.readFileSync(localFile);\n const file = new File([buffer], fileName, { type: \"application/octet-stream\" });\n const result = await storage.putObject({\n file,\n key,\n bucket: dest.bucket,\n metadata: { \"rebase-backup\": \"1\" }\n });\n return { key, storageUrl: result.storageUrl };\n}\n\n/**\n * List existing backups at a destination. For local destinations reads the\n * directory; for object storage lists the prefix via the controller.\n */\nexport async function listBackups(\n dest: BackupDestination,\n storage?: StorageController\n): Promise<BackupObject[]> {\n if (dest.kind === \"local\") {\n if (!fs.existsSync(dest.path)) return [];\n const stat = fs.statSync(dest.path);\n const dir = stat.isDirectory() ? dest.path : path.dirname(dest.path);\n return fs\n .readdirSync(dir)\n .filter((f) => f.endsWith(\".dump\"))\n .map((f) => {\n const full = path.join(dir, f);\n const stats = fs.statSync(full);\n const createdAt = parseBackupTimestamp(f) ?? stats.mtime;\n return { key: full, createdAt, sizeBytes: stats.size };\n })\n .sort((a, b) => (b.createdAt?.getTime() ?? 0) - (a.createdAt?.getTime() ?? 0));\n }\n\n if (!storage) {\n throw new BackupToolError(\n `Listing ${dest.kind} backups requires a configured storage backend.`\n );\n }\n const result = await storage.listObjects(dest.prefix ? `${dest.prefix}/` : \"\", {\n bucket: dest.bucket,\n maxResults: 1000\n });\n return result.items\n .map((item) => item.fullPath)\n .filter((key) => key.endsWith(\".dump\"))\n .map((key) => ({ key, createdAt: parseBackupTimestamp(key) ?? undefined }))\n .sort((a, b) => (b.createdAt?.getTime() ?? 0) - (a.createdAt?.getTime() ?? 0));\n}\n\n/**\n * Apply a retention policy to a destination, deleting the backups selected\n * by {@link selectBackupsToPrune}. Returns the keys that were removed.\n */\nexport async function pruneBackups(\n dest: BackupDestination,\n options: RetentionOptions,\n storage?: StorageController\n): Promise<string[]> {\n const backups = await listBackups(dest, storage);\n const toDelete = selectBackupsToPrune(backups, options);\n\n // Delete each dump together with its `.globals.sql` sidecar (pruneWith\n // handles the pairing + best-effort sidecar). The deleter rejects on a\n // missing key so pruneWith can swallow an absent sidecar.\n const deleteObject =\n dest.kind === \"local\"\n ? async (key: string) => {\n if (!fs.existsSync(key)) throw new Error(`not found: ${key}`);\n fs.unlinkSync(key);\n }\n : async (key: string) => {\n if (!storage) throw new BackupToolError(\"Storage backend required to prune object backups.\");\n await storage.deleteObject(key, dest.bucket);\n };\n\n await pruneWith(toDelete, deleteObject);\n return toDelete;\n}\n"],"mappings":"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"}
@@ -3,6 +3,7 @@ __createRequire(import.meta.url);
3
3
  import { n as outError, r as outWarn } from "./cli-output-CNdMql-L.js";
4
4
  import { t as chalk } from "./source-Br7L7GOI.js";
5
5
  import { logger } from "@rebasepro/server";
6
+ import { fieldKeyForColumn, getTableName } from "@rebasepro/common";
6
7
  //#region src/utils/pg-error-utils.ts
7
8
  /**
8
9
  * Shared PostgreSQL error extraction and user-friendly message formatting.
@@ -150,6 +151,80 @@ function isRowLevelSecurityDenial(error) {
150
151
  return pgError.message.toLowerCase().includes("row-level security policy");
151
152
  }
152
153
  /**
154
+ * The physical column names a Postgres error is about.
155
+ *
156
+ * Postgres puts one in `column` for `23502` and nowhere else, so the rest are
157
+ * read out of the two places that do carry them:
158
+ *
159
+ * - `detail` — `Key (author_id)=(9) is not present in table "authors".` and
160
+ * `Key (email, tenant_id)=(…) already exists.` Only the parenthesised column
161
+ * list is read; the values after `=` are never touched.
162
+ * - `constraint` — `users_email_key`, `posts_author_id_fkey`. The table prefix
163
+ * and the constraint suffix are stripped, leaving the columns joined by `_`,
164
+ * which is ambiguous for multi-column constraints and is therefore only used
165
+ * when `detail` gave nothing.
166
+ *
167
+ * `22P02` carries neither, because an invalid literal fails during parse rather
168
+ * than against a constraint. Its message names the *type*, and an enum type is
169
+ * generated as `<table>_<column>` — which is the one case worth decoding,
170
+ * because a value outside an enum's labels is the most common 22P02 anyone
171
+ * hits and the least legible one.
172
+ */
173
+ function offendingColumns(pgError, collection) {
174
+ if (pgError.column) return [pgError.column];
175
+ const detail = pgError.detail;
176
+ if (detail) {
177
+ const keyed = /Key \(([^)]+)\)=/.exec(detail);
178
+ if (keyed) return keyed[1].split(",").map((part) => part.trim().replace(/^"|"$/g, "")).filter(Boolean);
179
+ }
180
+ if (pgError.code === "22P02") {
181
+ const enumType = /invalid input value for enum ([^:\s]+)/i.exec(pgError.message ?? "");
182
+ if (enumType && collection) {
183
+ const typeName = enumType[1].replace(/^.*\./, "").replace(/"/g, "");
184
+ const table = getTableName(collection);
185
+ if (typeName.startsWith(`${table}_`)) return [typeName.slice(table.length + 1)];
186
+ }
187
+ return [];
188
+ }
189
+ const constraint = pgError.constraint;
190
+ if (constraint && collection) {
191
+ const table = getTableName(collection);
192
+ const stripped = constraint.replace(new RegExp(`^${table}_`), "").replace(/_(pkey|key|fkey|unique|check|excl|idx)$/, "");
193
+ if (stripped && stripped !== constraint) {
194
+ const columns = /* @__PURE__ */ new Set();
195
+ for (const [key, prop] of Object.entries(collection.properties ?? {})) columns.add(prop?.columnName ?? key);
196
+ if (columns.has(stripped)) return [stripped];
197
+ const wire = fieldKeyForColumn(collection, stripped);
198
+ if (collection.properties && wire in collection.properties) return [stripped];
199
+ }
200
+ }
201
+ return [];
202
+ }
203
+ /**
204
+ * The fields a Postgres error is about, for `details.violations`.
205
+ *
206
+ * Only the four SQLSTATEs that describe *the caller's data*: a missing value, a
207
+ * duplicate, a bad literal, a foreign key pointing at nothing. Everything else
208
+ * — a dropped connection, a missing table, a privilege problem — is the
209
+ * deployment's, and naming a field for it would send the reader to the wrong
210
+ * place.
211
+ *
212
+ * Empty when the collection is unknown or the column cannot be recovered. An
213
+ * empty list is the honest answer; inventing a field is worse than saying
214
+ * nothing, because a form will mark the wrong input red.
215
+ */
216
+ function pgFieldViolations(pgError, collection) {
217
+ const code = pgError.code;
218
+ if (code !== "23502" && code !== "23505" && code !== "22P02" && code !== "23503") return [];
219
+ if (!collection) return [];
220
+ const columns = offendingColumns(pgError, collection);
221
+ if (columns.length === 0) return [];
222
+ return columns.map((column) => ({
223
+ field: fieldKeyForColumn(collection, column),
224
+ code
225
+ }));
226
+ }
227
+ /**
153
228
  * Translate a raw PostgreSQL error into a user-friendly message.
154
229
  *
155
230
  * @param pgError - The extracted PostgreSQL error (from {@link extractPgError})
@@ -167,6 +242,7 @@ function pgErrorToFriendlyMessage(pgError, context, options = {}) {
167
242
  const rawMessage = pgError.message || "Unknown database error";
168
243
  const pgMessage = verbose ? rawMessage : "";
169
244
  const code = pgError.code || "UNKNOWN";
245
+ const violations = pgFieldViolations(pgError, options.collection);
170
246
  const suffix = hint ? ` Hint: ${hint}` : "";
171
247
  const tableRef = table ?? context;
172
248
  /** `": <postgres said>"`, or nothing when the details are withheld. */
@@ -174,50 +250,62 @@ function pgErrorToFriendlyMessage(pgError, context, options = {}) {
174
250
  switch (pgError.code) {
175
251
  case "23503": return {
176
252
  message: detail ? `Foreign key constraint violated: ${detail}${suffix}` : `Cannot complete operation: a foreign key constraint${constraint ? ` (${constraint})` : ""} was violated in "${context}".${suffix}`,
177
- code
253
+ code,
254
+ violations
178
255
  };
179
256
  case "23505": return {
180
257
  message: detail ? `Duplicate value: ${detail}${suffix}` : `Cannot complete operation: a unique constraint${constraint ? ` (${constraint})` : ""} was violated in "${context}".${suffix}`,
181
- code
258
+ code,
259
+ violations
182
260
  };
183
261
  case "23502": return {
184
- message: `Missing required field: "${column ?? "unknown"}" in "${tableRef}" cannot be empty.${suffix}`,
185
- code
262
+ message: `Missing required field: "${violations[0]?.field ?? column ?? "unknown"}" in "${tableRef}" cannot be empty.${suffix}`,
263
+ code,
264
+ violations
186
265
  };
187
266
  case "23514": return {
188
267
  message: `Validation failed: a check constraint${constraint ? ` (${constraint})` : ""} was violated in "${context}".${suffix}`,
189
- code
268
+ code,
269
+ violations
190
270
  };
191
271
  case "22P02": return {
192
272
  message: `Invalid data format in "${context}"${said}.${suffix}`,
193
- code
273
+ code,
274
+ violations
194
275
  };
195
276
  case "22001": return {
196
277
  message: `Value too long for column "${column ?? "unknown"}" in "${tableRef}"${said}.${suffix}`,
197
- code
278
+ code,
279
+ violations
198
280
  };
199
281
  case "22003": return {
200
282
  message: `Numeric value out of range for column "${column ?? "unknown"}" in "${tableRef}"${said}.${suffix}`,
201
- code
283
+ code,
284
+ violations
202
285
  };
203
286
  case "42703": return {
204
287
  message: `Unknown column in "${tableRef}"${said}. Check if your schema is up to date (run migrations).${suffix}`,
205
- code
288
+ code,
289
+ violations
206
290
  };
207
291
  case "42P01": return {
208
292
  message: `Table not found for "${context}"${said}. Check if your schema is up to date (run migrations).${suffix}`,
209
- code
293
+ code,
294
+ violations
210
295
  };
211
296
  case "42501": return rawMessage.toLowerCase().includes("row-level security policy") ? {
212
297
  message: `Not permitted to write this row in "${tableRef}": it does not satisfy the row-level security policy.${suffix}`,
213
- code
298
+ code,
299
+ violations
214
300
  } : {
215
301
  message: `Permission denied on "${tableRef}": the database role this server connects as lacks privileges on it.${suffix}`,
216
- code
302
+ code,
303
+ violations
217
304
  };
218
305
  case "28000": return {
219
306
  message: `Authorization failed for "${context}". Check your database credentials.${suffix}`,
220
- code
307
+ code,
308
+ violations
221
309
  };
222
310
  default: {
223
311
  const parts = [`Database error in "${context}" [${code}]${said}`];
@@ -228,7 +316,8 @@ function pgErrorToFriendlyMessage(pgError, context, options = {}) {
228
316
  if (hint) parts.push(`Hint: ${hint}`);
229
317
  return {
230
318
  message: parts.join(". "),
231
- code
319
+ code,
320
+ violations
232
321
  };
233
322
  }
234
323
  }
@@ -246,7 +335,7 @@ function pgErrorToFriendlyMessage(pgError, context, options = {}) {
246
335
  * @returns An object with `message` (user-friendly) and optional `code`
247
336
  * (the `ApiError` code, or the PG SQLSTATE).
248
337
  */
249
- function sanitizeErrorForClient(error, context) {
338
+ function sanitizeErrorForClient(error, context, options = {}) {
250
339
  const clientError = asClientFacingError(error);
251
340
  if (clientError) {
252
341
  const line = `[API ${clientError.statusCode} ${clientError.code ?? "BAD_REQUEST"}] in "${context}": ${clientError.message}`;
@@ -269,7 +358,12 @@ function sanitizeErrorForClient(error, context) {
269
358
  constraint: pgError.constraint,
270
359
  dataType: pgError.dataType
271
360
  });
272
- return pgErrorToFriendlyMessage(pgError, context);
361
+ const { message, code, violations } = pgErrorToFriendlyMessage(pgError, context, { collection: options.collection });
362
+ return {
363
+ message,
364
+ code,
365
+ ...violations.length > 0 && { violations }
366
+ };
273
367
  }
274
368
  logger.error(`Database error in "${context}" (no PG error extracted)`, {
275
369
  error: error instanceof Error ? error.message : String(error),
@@ -631,6 +725,31 @@ async function countRows(databaseUrl, table) {
631
725
  }
632
726
  }
633
727
  /**
728
+ * A hand-written generated column reading a column the plan wants to retype or
729
+ * drop.
730
+ *
731
+ * Rebase's own generated columns are dropped and rebuilt around the apply — it
732
+ * holds their definition in `search.sql`. This one it does not: PostgreSQL
733
+ * stores a generated expression only in the catalogue, so dropping it to let
734
+ * the apply through would destroy the only copy. Refuse instead, and say what
735
+ * the operator can do about it.
736
+ */
737
+ function formatForeignGeneratedColumnBanner(conflicts) {
738
+ const lines = conflicts.map((c) => {
739
+ const reads = c.blocking.map((b) => `"${b.column}"`).join(", ");
740
+ return ` ${chalk.bold(`${c.schema}.${c.table}.${c.column}`)} reads ${reads}`;
741
+ });
742
+ const first = conflicts[0];
743
+ return "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n ❌ A generated column depends on a column this push changes\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n" + lines.join("\n") + `
744
+
745
+ PostgreSQL refuses to retype or drop a column while a GENERATED
746
+ ALWAYS AS … STORED expression reads it, and an Atlas apply is one
747
+ transaction — so this refusal would roll back every other statement
748
+ in the push with it.
749
+
750
+ ${chalk.yellow("Nothing was applied.")} Rebase rebuilds the generated columns it made\n itself (a collection's ${chalk.bold("search")} block) around the apply. It has no\n definition for this one, and a dropped generated expression is not\n recoverable from anywhere but your own migration.\n\n Drop it, push, and put it back:\n\n ${chalk.gray(`ALTER TABLE "${first.schema}"."${first.table}" DROP COLUMN "${first.column}";`)}\n ${chalk.gray("rebase db push")}\n ${chalk.gray("-- then re-create the column with its original expression")}\n\n Or declare it through a ${chalk.bold("search")} block, and Rebase will own it.\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
751
+ }
752
+ /**
634
753
  * The remedy for an Atlas invocation that failed, or `null`.
635
754
  *
636
755
  * Scoped by the invocation, because the same database state means different
@@ -652,6 +771,6 @@ async function diagnoseAtlasFailure(context) {
652
771
  return null;
653
772
  }
654
773
  //#endregion
655
- export { isEconnrefused as a, classifyConnectFailure as c, isRoleSwitchingPermissionError as d, isRowLevelSecurityDenial as f, sanitizeErrorForClient as h, diagnoseDbError as i, extractCauseMessage as l, reachedDatabase as m, deepestErrorMessage as n, parseHostInfo as o, pgErrorToFriendlyMessage as p, diagnoseAtlasFailure as r, reportCommandFailure as s, checkDatabaseConnectivity as t, extractPgError as u };
774
+ export { formatForeignGeneratedColumnBanner as a, reportCommandFailure as c, extractPgError as d, isRoleSwitchingPermissionError as f, sanitizeErrorForClient as g, reachedDatabase as h, diagnoseDbError as i, classifyConnectFailure as l, pgErrorToFriendlyMessage as m, deepestErrorMessage as n, isEconnrefused as o, isRowLevelSecurityDenial as p, diagnoseAtlasFailure as r, parseHostInfo as s, checkDatabaseConnectivity as t, extractCauseMessage as u };
656
775
 
657
- //# sourceMappingURL=cli-errors-DxN4yzdH.js.map
776
+ //# sourceMappingURL=cli-errors-C3g_kHBw.js.map