@rebasepro/server-postgres 0.25.0 → 0.26.0

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 (144) hide show
  1. package/dist/{BranchService-iXXZeVYD.js → BranchService-CWBnDhcv.js} +88 -12
  2. package/dist/BranchService-CWBnDhcv.js.map +1 -0
  3. package/dist/PostgresBackendDriver.d.ts +13 -0
  4. package/dist/PostgresBootstrapper.d.ts +7 -0
  5. package/dist/auth/services.d.ts +8 -1
  6. package/dist/auth/verify-auth-tables.d.ts +21 -0
  7. package/dist/backup/backup-cli.d.ts +40 -3
  8. package/dist/backup/backup-service.d.ts +11 -0
  9. package/dist/{backup-service-Bb39plam.js → backup-cli-PoRG-zEq.js} +777 -10
  10. package/dist/backup-cli-PoRG-zEq.js.map +1 -0
  11. package/dist/{boot-ddl-s_3Hkz94.js → boot-ddl-EsgqDkx0.js} +3 -3
  12. package/dist/{boot-ddl-s_3Hkz94.js.map → boot-ddl-EsgqDkx0.js.map} +1 -1
  13. package/dist/branch-argv.d.ts +15 -0
  14. package/dist/branch-diff-CxE4VlxT.js +1396 -0
  15. package/dist/branch-diff-CxE4VlxT.js.map +1 -0
  16. package/dist/branch-diff-report-DpvVf7hR.js +48 -0
  17. package/dist/branch-diff-report-DpvVf7hR.js.map +1 -0
  18. package/dist/branching/branch-diff-report.d.ts +7 -0
  19. package/dist/branching/branch-diff.d.ts +115 -0
  20. package/dist/{cli-console-DQ_K-sdM.js → cli-console-f0k6_TsJ.js} +41 -9
  21. package/dist/cli-console-f0k6_TsJ.js.map +1 -0
  22. package/dist/cli-console.d.ts +17 -1
  23. package/dist/{cli-errors-DwQnNATs.js → cli-errors-DsYyNYzd.js} +90 -11
  24. package/dist/cli-errors-DsYyNYzd.js.map +1 -0
  25. package/dist/cli-errors.d.ts +14 -0
  26. package/dist/cli-flags.d.ts +51 -0
  27. package/dist/{cli-prompt-egtNKkZl.js → cli-prompt-DXG0yk-h.js} +39 -4
  28. package/dist/{cli-prompt-egtNKkZl.js.map → cli-prompt-DXG0yk-h.js.map} +1 -1
  29. package/dist/cli.js +121 -271
  30. package/dist/cli.js.map +1 -1
  31. package/dist/{collections-schema-version-T6cGvrDi.js → collections-schema-version-D7bDI0PJ.js} +13 -9
  32. package/dist/collections-schema-version-D7bDI0PJ.js.map +1 -0
  33. package/dist/{column-plan-helpers-D_V4oaZh.js → column-plan-helpers-IG6uFb1s.js} +2 -2
  34. package/dist/{column-plan-helpers-D_V4oaZh.js.map → column-plan-helpers-IG6uFb1s.js.map} +1 -1
  35. package/dist/{connection-C938nR3E.js → connection-czvI7TZL.js} +57 -10
  36. package/dist/connection-czvI7TZL.js.map +1 -0
  37. package/dist/{databasePoolManager-Yqdw14wY.js → databasePoolManager-3stMNFL-.js} +4 -6
  38. package/dist/databasePoolManager-3stMNFL-.js.map +1 -0
  39. package/dist/db-command-line.d.ts +14 -0
  40. package/dist/db-subcommands.d.ts +11 -0
  41. package/dist/{deferred-index-build-Cis_HoGn.js → deferred-index-build-CqaOv1Xx.js} +8 -8
  42. package/dist/{deferred-index-build-Cis_HoGn.js.map → deferred-index-build-CqaOv1Xx.js.map} +1 -1
  43. package/dist/{doctor-DvJH-IQG.js → doctor-DW_mlTzz.js} +186 -42
  44. package/dist/doctor-DW_mlTzz.js.map +1 -0
  45. package/dist/{ensure-collection-policies-CCqayoUz.js → ensure-collection-policies-DLWgabQ2.js} +13 -10
  46. package/dist/ensure-collection-policies-DLWgabQ2.js.map +1 -0
  47. package/dist/{ensure-collection-tables-DvvImXDe.js → ensure-collection-tables-CFIRq8ni.js} +9 -9
  48. package/dist/{ensure-collection-tables-DvvImXDe.js.map → ensure-collection-tables-CFIRq8ni.js.map} +1 -1
  49. package/dist/{ensure-tables-Dpgwx5sy.js → ensure-tables-4nA-tfwi.js} +5 -5
  50. package/dist/{ensure-tables-Dpgwx5sy.js.map → ensure-tables-4nA-tfwi.js.map} +1 -1
  51. package/dist/{enum-labels-kept-CUlUlRkO.js → enum-labels-kept-PcPW65xy.js} +3 -3
  52. package/dist/{enum-labels-kept-CUlUlRkO.js.map → enum-labels-kept-PcPW65xy.js.map} +1 -1
  53. package/dist/{generate-drizzle-schema-C3EAP9HE.js → generate-drizzle-schema-DC0BOUpn.js} +3 -3
  54. package/dist/{generate-drizzle-schema-C3EAP9HE.js.map → generate-drizzle-schema-DC0BOUpn.js.map} +1 -1
  55. package/dist/{generate-drizzle-schema-logic-DA3zM8zc.js → generate-drizzle-schema-logic-CPzCRD-8.js} +4 -4
  56. package/dist/{generate-drizzle-schema-logic-DA3zM8zc.js.map → generate-drizzle-schema-logic-CPzCRD-8.js.map} +1 -1
  57. package/dist/{generate-files-ClmOUHXj.js → generate-files-BP-4W_gm.js} +6 -6
  58. package/dist/{generate-files-ClmOUHXj.js.map → generate-files-BP-4W_gm.js.map} +1 -1
  59. package/dist/{generate-next-step-DCeS4LrJ.js → generate-next-step-DgK5BlBn.js} +2 -2
  60. package/dist/{generate-next-step-DCeS4LrJ.js.map → generate-next-step-DgK5BlBn.js.map} +1 -1
  61. package/dist/{generate-postgres-ddl-logic-4eUP4t8w.js → generate-postgres-ddl-logic-DPl1oH_z.js} +4 -4
  62. package/dist/{generate-postgres-ddl-logic-4eUP4t8w.js.map → generate-postgres-ddl-logic-DPl1oH_z.js.map} +1 -1
  63. package/dist/generated-schema-diff-eZCklRZQ.js +1025 -0
  64. package/dist/generated-schema-diff-eZCklRZQ.js.map +1 -0
  65. package/dist/{generated-schema-staleness-FlxM2i4i.js → generated-schema-staleness-BvNF3i-s.js} +2 -2
  66. package/dist/{generated-schema-staleness-FlxM2i4i.js.map → generated-schema-staleness-BvNF3i-s.js.map} +1 -1
  67. package/dist/history/HistoryService.d.ts +7 -0
  68. package/dist/index.d.ts +21 -0
  69. package/dist/index.es.js +288 -11
  70. package/dist/index.es.js.map +1 -1
  71. package/dist/{introspect-db-logic-CI5CU_n0.js → introspect-db-logic-DgQVzccW.js} +2 -2
  72. package/dist/{introspect-db-logic-CI5CU_n0.js.map → introspect-db-logic-DgQVzccW.js.map} +1 -1
  73. package/dist/{rls-bootstrap-sql-Bd-hJk08.js → migrate-baseline-B-2Ef4Mv.js} +718 -41
  74. package/dist/migrate-baseline-B-2Ef4Mv.js.map +1 -0
  75. package/dist/migrate-policy-stamps-BQtFHApx.js +89 -0
  76. package/dist/migrate-policy-stamps-BQtFHApx.js.map +1 -0
  77. package/dist/{open-rebase-BKA7Nyoe.js → open-rebase-C8jQ6s5a.js} +324 -1047
  78. package/dist/open-rebase-C8jQ6s5a.js.map +1 -0
  79. package/dist/open-rebase.d.ts +9 -1
  80. package/dist/{pg-error-utils-Bv5n2PLn.js → pg-error-utils-DhiUTlVx.js} +2 -2
  81. package/dist/{pg-error-utils-Bv5n2PLn.js.map → pg-error-utils-DhiUTlVx.js.map} +1 -1
  82. package/dist/{plan-schema-CXOucCw-.js → plan-schema-Czf3q4-Q.js} +4 -4
  83. package/dist/{plan-schema-CXOucCw-.js.map → plan-schema-Czf3q4-Q.js.map} +1 -1
  84. package/dist/{policy-drift-vm0BcvcE.js → policy-drift-X4A-xAzv.js} +3 -3
  85. package/dist/{policy-drift-vm0BcvcE.js.map → policy-drift-X4A-xAzv.js.map} +1 -1
  86. package/dist/pool-disconnects.d.ts +28 -0
  87. package/dist/{push-index-prebuild-DIequ4bV.js → push-index-prebuild-UTvB5pqH.js} +4 -4
  88. package/dist/{push-index-prebuild-DIequ4bV.js.map → push-index-prebuild-UTvB5pqH.js.map} +1 -1
  89. package/dist/push-kept-defaults-BGOlOqRQ.js +137 -0
  90. package/dist/push-kept-defaults-BGOlOqRQ.js.map +1 -0
  91. package/dist/{push-legacy-renames-BqJut4F3.js → push-legacy-renames-BwaKi8xF.js} +4 -4
  92. package/dist/{push-legacy-renames-BqJut4F3.js.map → push-legacy-renames-BwaKi8xF.js.map} +1 -1
  93. package/dist/{push-policy-plan-CvX8hiPb.js → push-policy-plan-DQ2tWceD.js} +5 -5
  94. package/dist/{push-policy-plan-CvX8hiPb.js.map → push-policy-plan-DQ2tWceD.js.map} +1 -1
  95. package/dist/{render-ddl-CQNwZdse.js → render-ddl-4v88y-Gv.js} +4 -4
  96. package/dist/{render-ddl-CQNwZdse.js.map → render-ddl-4v88y-Gv.js.map} +1 -1
  97. package/dist/{rls-enforcement-D7P4-ZYX.js → rls-enforcement-BOHuHVlc.js} +43 -4
  98. package/dist/rls-enforcement-BOHuHVlc.js.map +1 -0
  99. package/dist/schema/destructive-sql.d.ts +16 -0
  100. package/dist/schema/doctor-cli.js +5 -5
  101. package/dist/schema/doctor.d.ts +20 -2
  102. package/dist/schema/ensure-collection-policies.d.ts +4 -1
  103. package/dist/schema/generate-drizzle-schema.js +1 -1
  104. package/dist/schema/generate-postgres-ddl.js +2 -2
  105. package/dist/schema/generated-schema-diff.d.ts +21 -0
  106. package/dist/schema/introspect-db.js +5 -6
  107. package/dist/schema/introspect-db.js.map +1 -1
  108. package/dist/schema/migrate-policy-stamps.d.ts +23 -0
  109. package/dist/schema/migration-freshness.d.ts +69 -0
  110. package/dist/schema/push-kept-defaults.d.ts +57 -0
  111. package/dist/schema/push-policy-plan.d.ts +5 -0
  112. package/dist/schema/rls-bootstrap-sql.d.ts +47 -10
  113. package/dist/security/rls-enforcement.d.ts +18 -0
  114. package/dist/services/BranchService.d.ts +20 -2
  115. package/dist/services/FetchService.d.ts +12 -0
  116. package/dist/services/channel-presence.d.ts +3 -1
  117. package/dist/services/nested-path.d.ts +16 -0
  118. package/dist/services/pg-notify-listener.d.ts +2 -0
  119. package/dist/services/realtimeService.d.ts +18 -1
  120. package/dist/{source-Bv5KPeIx.js → source-fZb92pN8.js} +2 -2
  121. package/dist/{source-Bv5KPeIx.js.map → source-fZb92pN8.js.map} +1 -1
  122. package/dist/{vector-index-BFhyEaiz.js → vector-index-DsQ3V2d2.js} +2 -2
  123. package/dist/{vector-index-BFhyEaiz.js.map → vector-index-DsQ3V2d2.js.map} +1 -1
  124. package/dist/{websocket-MC0zTBar.js → websocket-CKYc3V4N.js} +56 -14
  125. package/dist/websocket-CKYc3V4N.js.map +1 -0
  126. package/package.json +7 -7
  127. package/dist/BranchService-iXXZeVYD.js.map +0 -1
  128. package/dist/backup-cli-B2CAn9fR.js +0 -647
  129. package/dist/backup-cli-B2CAn9fR.js.map +0 -1
  130. package/dist/backup-service-Bb39plam.js.map +0 -1
  131. package/dist/cli-console-DQ_K-sdM.js.map +0 -1
  132. package/dist/cli-errors-DwQnNATs.js.map +0 -1
  133. package/dist/cli-flags-D-b0qL9T.js +0 -206
  134. package/dist/cli-flags-D-b0qL9T.js.map +0 -1
  135. package/dist/collections-schema-version-T6cGvrDi.js.map +0 -1
  136. package/dist/connection-C938nR3E.js.map +0 -1
  137. package/dist/databasePoolManager-Yqdw14wY.js.map +0 -1
  138. package/dist/doctor-DvJH-IQG.js.map +0 -1
  139. package/dist/ensure-collection-policies-CCqayoUz.js.map +0 -1
  140. package/dist/open-rebase-BKA7Nyoe.js.map +0 -1
  141. package/dist/rls-bootstrap-sql-Bd-hJk08.js.map +0 -1
  142. package/dist/rls-enforcement-D7P4-ZYX.js.map +0 -1
  143. package/dist/rolldown-runtime-V7rFbMRe.js +0 -39
  144. package/dist/websocket-MC0zTBar.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"policy-drift-vm0BcvcE.js","names":[],"sources":["../src/security/policy-drift.ts"],"sourcesContent":["/**\n * Compare the RLS policies a database actually has against the ones the\n * collections describe.\n *\n * Policies live in Postgres; the collection config is only their *source*.\n * Nothing reconciled the two, which is how the demo database served empty\n * collections indefinitely: its policies granted `TO authenticated` (a Supabase\n * role name) while requests run as `rebase_user`, so RLS filtered every row.\n * The config was later corrected and the database never noticed — an empty\n * table is indistinguishable from a table with no data.\n *\n * Expected policies are parsed from `generatePostgresPoliciesDdl`, the same\n * function `db push` uses to write `.rebase/sql/policies.sql`, so this compares\n * against exactly what would be applied rather than a reimplementation.\n */\nimport { RLS_UID_SQL, type CollectionConfig } from \"@rebasepro/types\";\n\nimport { generatePostgresPoliciesDdl } from \"../schema/generate-postgres-ddl-logic\";\n\nexport interface PolicyRef {\n schema: string;\n table: string;\n name: string;\n /** Roles in the TO clause. */\n roles: string[];\n /** SELECT / INSERT / UPDATE / DELETE / ALL. */\n command: string;\n /** Whether a USING clause is present at all (not what it says). */\n hasUsing: boolean;\n /** Whether a WITH CHECK clause is present at all (not what it says). */\n hasWithCheck: boolean;\n /**\n * PERMISSIVE or RESTRICTIVE — the `AS` clause.\n *\n * An exact catalogue value on both sides, so it belongs with roles and\n * command rather than with the expression text. It matters more than either:\n * permissive policies are ORed together and restrictive ones ANDed, so a rule\n * declared `mode: \"restrictive\"` whose live policy is PERMISSIVE has had its\n * gate turned from a requirement into an alternative — the maximally\n * permissive way for this to be wrong.\n *\n * The DDL regex captured this from the start and the destructuring threw it\n * away; `pg_policies.permissive` was never selected.\n */\n mode?: \"PERMISSIVE\" | \"RESTRICTIVE\";\n /**\n * The live clause text, when read from `pg_policies`. Present only for live\n * policies (the expected side is parsed from DDL and does not carry it).\n * Used solely for the insecure-tautology scan, not for divergence — Postgres\n * rewrites this text, so it is not safe to diff against expected.\n */\n qual?: string | null;\n withCheck?: string | null;\n}\n\nexport interface PolicyDrift {\n /** Described by the collections, absent from the database. */\n missing: PolicyRef[];\n /** In the database, described by no collection — stale pushes live here. */\n orphaned: PolicyRef[];\n /** Same policy name, different roles or command. */\n diverged: { expected: PolicyRef; actual: PolicyRef; differences: string[] }[];\n /**\n * A live policy whose expression is the known-permissive tautology\n * `rebase.uid() IS NOT NULL` — true for anonymous visitors too, because the\n * user path coerces a blank id to the `'anonymous'` sentinel. This is what\n * `policy.authenticated()` used to compile to, so a database pushed before\n * that fix carries it, and neither the name, roles, command nor clause\n * *presence* differs from the corrected policy — the only thing that changed\n * is the expression text, which this checker otherwise (correctly) ignores.\n * So it is the one drift that hides from every other check here.\n *\n * @see reason a sentence naming the clause and what to do.\n */\n insecure: { policy: PolicyRef; reason: string }[];\n /**\n * A table the collections describe whose RLS switch is off.\n *\n * `ALTER TABLE posts DISABLE ROW LEVEL SECURITY` leaves every row in\n * `pg_policies` untouched, so before this category every expected policy\n * still matched on name, roles, command and clause presence and the checker\n * reported clean — on a table Postgres was applying no filter to at all.\n * Requests run as `rebase_user`, which holds full DML, so the table is wide\n * open while `doctor` certifies it.\n *\n * `forced` reports `relforcerowsecurity`, which is what also subjects the\n * table's *owner* to its policies. Its absence is not drift on its own —\n * Rebase does not connect as the owner in the request path — so it is\n * reported for context rather than raised as a failure.\n */\n rlsDisabled: { schema: string; table: string; forced: boolean }[];\n}\n\nexport interface Queryable {\n query<R>(text: string, values?: unknown[]): Promise<{ rows: R[] }>;\n}\n\n// The trailing group captures whichever clause follows the TO list, which is\n// what tells us the clause is present.\nconst CREATE_POLICY = /CREATE POLICY \"([^\"]+)\" ON \"([^\"]+)\"\\.\"([^\"]+)\"\\s+AS (\\w+)\\s+FOR (\\w+)\\s+TO ([^\\n]+?)(\\s+USING\\s*\\(|\\s+WITH CHECK\\s*\\(|;)/gi;\n\nconst WITH_CHECK_NEXT = /^\\s+WITH CHECK\\s*\\(/i;\n\n/**\n * Index just past the `)` closing a clause whose `(` ends at `open`.\n *\n * Needed because a policy expression nests parens and can contain a quoted\n * literal holding either character, so \"find the next `)`\" would stop early and\n * miss the `WITH CHECK` that follows.\n */\nfunction clauseEnd(ddl: string, open: number): number {\n let depth = 1;\n let inQuote = false;\n for (let i = open; i < ddl.length; i++) {\n const c = ddl[i];\n if (inQuote) {\n // '' is an escaped quote inside a string, not a close.\n if (c === \"'\") {\n if (ddl[i + 1] === \"'\") i++;\n else inQuote = false;\n }\n continue;\n }\n if (c === \"'\") inQuote = true;\n else if (c === \"(\") depth++;\n else if (c === \")\" && --depth === 0) return i + 1;\n }\n return ddl.length;\n}\n\n/**\n * `AS PERMISSIVE` / `AS RESTRICTIVE` from either side, or undefined.\n *\n * The DDL spells it as a bare word; `pg_policies.permissive` is the string\n * `\"PERMISSIVE\"`/`\"RESTRICTIVE\"` on modern Postgres but a boolean on some\n * drivers and older servers, so both are accepted. Anything unrecognised\n * becomes `undefined` and the comparison below skips it rather than inventing\n * drift from a shape we did not anticipate.\n */\nfunction normalizeMode(raw: unknown): \"PERMISSIVE\" | \"RESTRICTIVE\" | undefined {\n if (typeof raw === \"boolean\") return raw ? \"PERMISSIVE\" : \"RESTRICTIVE\";\n if (typeof raw !== \"string\") return undefined;\n const upper = raw.trim().toUpperCase();\n if (upper === \"PERMISSIVE\" || upper === \"TRUE\" || upper === \"T\") return \"PERMISSIVE\";\n if (upper === \"RESTRICTIVE\" || upper === \"FALSE\" || upper === \"F\") return \"RESTRICTIVE\";\n return undefined;\n}\n\n/** Parse the generated DDL rather than rebuilding the shape by hand. */\nexport function parseExpectedPolicies(ddl: string): PolicyRef[] {\n const found: PolicyRef[] = [];\n for (const m of ddl.matchAll(CREATE_POLICY)) {\n const [, name, schema, table, modeRaw, command, rolesRaw, clause] = m;\n const roles = rolesRaw\n .split(\",\")\n .map((r) => r.trim().replace(/^\"|\"$/g, \"\"))\n .filter(Boolean);\n\n // The generator emits USING before WITH CHECK, so what follows the TO\n // list settles USING; WITH CHECK is then whatever follows that clause.\n const hasUsing = /USING/i.test(clause);\n const hasWithCheck = hasUsing\n ? WITH_CHECK_NEXT.test(ddl.slice(clauseEnd(ddl, m.index + m[0].length)))\n : /WITH CHECK/i.test(clause);\n\n found.push({\n schema, table, name, roles,\n command: command.toUpperCase(),\n hasUsing, hasWithCheck,\n mode: normalizeMode(modeRaw)\n });\n }\n return found;\n}\n\nasync function readLivePolicies(client: Queryable, schemas: string[]): Promise<PolicyRef[]> {\n const { rows } = await client.query<{\n schemaname: string; tablename: string; policyname: string; roles: string[] | string; cmd: string;\n qual: string | null; with_check: string | null; permissive: string | boolean | null;\n }>(\n `SELECT schemaname, tablename, policyname, roles, cmd, qual, with_check, permissive\n FROM pg_policies\n WHERE schemaname = ANY($1)`,\n [schemas]\n );\n\n return rows.map((r) => ({\n schema: r.schemaname,\n table: r.tablename,\n name: r.policyname,\n // node-postgres yields text[] as an array; some drivers hand back \"{a,b}\".\n roles: Array.isArray(r.roles)\n ? r.roles\n : String(r.roles ?? \"\").replace(/^\\{|\\}$/g, \"\").split(\",\").filter(Boolean),\n command: (r.cmd ?? \"ALL\").toUpperCase(),\n // Presence only. Postgres rewrites the text, but it does not invent or\n // drop a clause: NULL here means the policy genuinely has none.\n hasUsing: r.qual != null,\n hasWithCheck: r.with_check != null,\n mode: normalizeMode(r.permissive),\n qual: r.qual,\n withCheck: r.with_check\n }));\n}\n\n/**\n * Tables the collections describe whose RLS switch is off.\n *\n * Read from `pg_class` because `pg_policies` cannot answer it: disabling RLS\n * leaves the policy rows in place, so every other comparison in this file keeps\n * matching while Postgres applies none of them.\n *\n * Only tables that expect at least one policy are considered. A table with no\n * declared rules is not asserting anything about RLS, and reporting it would\n * make the check noisy on exactly the projects least able to judge the noise.\n */\nasync function readTablesWithRlsOff(\n client: Queryable,\n tables: { schema: string; table: string }[]\n): Promise<{ schema: string; table: string; forced: boolean }[]> {\n if (tables.length === 0) return [];\n const { rows } = await client.query<{\n schemaname: string; tablename: string; relrowsecurity: boolean; relforcerowsecurity: boolean;\n }>(\n `SELECT n.nspname AS schemaname,\n c.relname AS tablename,\n c.relrowsecurity,\n c.relforcerowsecurity\n FROM pg_class c\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE c.relkind = 'r'\n AND (n.nspname || '.' || c.relname) = ANY($1)`,\n [tables.map((t) => `${t.schema}.${t.table}`)]\n );\n\n return rows\n .filter((r) => !r.relrowsecurity)\n .map((r) => ({\n schema: r.schemaname,\n table: r.tablename,\n forced: !!r.relforcerowsecurity\n }));\n}\n\n/**\n * The permissive tautology `rebase.uid() IS NOT NULL`, without the\n * `<> 'anonymous'` guard that makes it mean \"signed in\".\n *\n * Whitespace varies with Postgres's rewrite, so match on a collapsed form. The\n * guard clause ({@link EXCLUDES_ANONYMOUS}, in any spelling) is what\n * distinguishes the corrected policy from the stale one, so its presence\n * clears the text.\n *\n * Both schema spellings are matched. The helpers moved from `auth` to `rebase`\n * in 1.0, and this check reads policies *as the database stored them*: a\n * database pushed before the move still holds `auth.uid()` until it is\n * recompiled, while everything written since — including raw `securityRules`\n * SQL, which the compiler rewrites on the way in — stores `rebase.uid()`.\n * Matching only the pre-1.0 name made the check blind to every policy a current\n * release could write, which is precisely the set still worth scanning.\n */\nfunction isPermissiveAuthTautology(clause: string | null | undefined): boolean {\n if (!clause) return false;\n const flat = clause.toLowerCase().replace(/\\s+/g, \" \");\n // The call bare, or wrapped as an initPlan, which Postgres stores as\n // `( SELECT rebase.uid() AS uid)`.\n if (!/\\b(?:rebase|auth)\\.uid\\s*\\(\\s*\\)(?:\\s+as\\s+\\w+\\s*\\))?\\s*is not null/.test(flat)) return false;\n return !EXCLUDES_ANONYMOUS.some((guard) => guard.test(flat));\n}\n\n/**\n * The guard that makes the null test mean \"signed in\": the anonymous\n * sentinel excluded, in every spelling a policy reaches the catalogue in.\n *\n * `policy.authenticated()` compiles to `NOT IN ('anonymous', 'anon')`, which\n * Postgres stores as `<> ALL (ARRAY['anonymous'::text, 'anon'::text])`. Only\n * `<> 'anonymous'` was recognised, so `doctor` and every `db push` called each\n * `authenticated()` and `registered()` policy the insecure tautology. A list\n * that leaves out `'anonymous'` excludes nobody a signed-out request arrives\n * as, and does not clear it.\n */\nconst EXCLUDES_ANONYMOUS: RegExp[] = [\n /(?:<>|!=)\\s*'anonymous'/,\n /(?:<>|!=)\\s*all\\s*\\(\\s*array\\s*\\[[^\\]]*'anonymous'/,\n /\\bnot in\\s*\\([^)]*'anonymous'/\n];\n\nconst keyOf = (p: PolicyRef) => `${p.schema}.${p.table}.${p.name}`;\nconst sameRoles = (a: string[], b: string[]) =>\n a.length === b.length && [...a].sort().join(\",\") === [...b].sort().join(\",\");\n\n/**\n * Diff expected against live.\n *\n * Compares names, roles, command, and whether each clause exists — all exact\n * values. Policy expression *text* is deliberately not compared: Postgres\n * rewrites `qual`/`with_check` when storing them (parenthesising, casting,\n * schema-qualifying), so text comparison reports drift that does not exist, and\n * a check that cries wolf gets ignored.\n *\n * Presence is not text, though. A NULL `qual` is not a rewrite of an\n * expression, it is the absence of one, and absence has no false-positive risk:\n * either the generator emitted a clause or it did not. That distinction is worth\n * the extra comparison — a production database was found with a SELECT policy\n * whose `qual` was NULL, matching on every field this checked and denying 100%\n * of reads. The same blindness would hide a policy that fails open.\n */\nexport async function checkPolicyDrift(\n client: Queryable,\n collections: CollectionConfig[]\n): Promise<PolicyDrift> {\n const expected = parseExpectedPolicies(generatePostgresPoliciesDdl(collections));\n const schemas = [...new Set(expected.map((p) => p.schema))];\n // Nothing expected means nothing to reconcile against; scanning every\n // schema would report the whole database as orphaned.\n if (schemas.length === 0) return { missing: [], orphaned: [], diverged: [], insecure: [], rlsDisabled: [] };\n\n const live = await readLivePolicies(client, schemas);\n const liveByKey = new Map(live.map((p) => [keyOf(p), p]));\n const expectedByKey = new Map(expected.map((p) => [keyOf(p), p]));\n\n const drift: PolicyDrift = { missing: [], orphaned: [], diverged: [], insecure: [], rlsDisabled: [] };\n\n // Before comparing policy against policy: is the switch even on? A table\n // with RLS disabled matches every expected policy on every field compared\n // below, so this has to be asked separately or it cannot be asked at all.\n const expectedTables = [...new Map(\n expected.map((p) => [`${p.schema}.${p.table}`, { schema: p.schema, table: p.table }])\n ).values()];\n drift.rlsDisabled = await readTablesWithRlsOff(client, expectedTables);\n\n // Scan every live policy for the permissive tautology. This is deliberately\n // independent of the name-keyed diff below: a database pushed before the\n // `authenticated()` fix matches its expected policy on name, roles, command\n // and clause presence, so nothing else here would flag it.\n for (const p of live) {\n const clause = isPermissiveAuthTautology(p.qual)\n ? \"USING\"\n : isPermissiveAuthTautology(p.withCheck) ? \"WITH CHECK\" : null;\n if (clause) {\n drift.insecure.push({\n policy: p,\n reason: `${clause} is \\`${RLS_UID_SQL} IS NOT NULL\\`, which is true for anonymous ` +\n `visitors too — this grants access to signed-out requests. It predates the ` +\n `\\`policy.authenticated()\\` fix; re-run \\`rebase db push\\` to tighten it.`\n });\n }\n }\n\n for (const [key, want] of expectedByKey) {\n const got = liveByKey.get(key);\n if (!got) {\n drift.missing.push(want);\n continue;\n }\n const differences: string[] = [];\n if (!sameRoles(want.roles, got.roles)) {\n differences.push(`roles: expected [${want.roles.join(\", \")}], database has [${got.roles.join(\", \")}]`);\n }\n if (want.command !== got.command) {\n differences.push(`command: expected ${want.command}, database has ${got.command}`);\n }\n // Skipped when either side is unknown: an unrecognised catalogue shape\n // should not manufacture drift. See `normalizeMode`.\n if (want.mode && got.mode && want.mode !== got.mode) {\n differences.push(\n `mode: expected ${want.mode}, database has ${got.mode}` +\n (got.mode === \"PERMISSIVE\"\n ? \" — a RESTRICTIVE rule is ANDed with the others, a PERMISSIVE one is ORed, so this policy now widens access instead of narrowing it\"\n : \" — a PERMISSIVE rule is ORed with the others, a RESTRICTIVE one is ANDed, so this policy now narrows access instead of widening it\")\n );\n }\n for (const clause of [\"USING\", \"WITH CHECK\"] as const) {\n const key = clause === \"USING\" ? \"hasUsing\" : \"hasWithCheck\";\n if (want[key] === got[key]) continue;\n differences.push(want[key]\n ? `${clause}: expected an expression, database has none — this policy matches no rows`\n : `${clause}: expected none, database has an expression`);\n }\n if (differences.length > 0) drift.diverged.push({ expected: want, actual: got, differences });\n }\n\n for (const [key, got] of liveByKey) {\n if (!expectedByKey.has(key)) drift.orphaned.push(got);\n }\n\n return drift;\n}\n\n/**\n * Does this name look like one the generator produced for this table?\n *\n * Unnamed rules compile to `<table>_<op>_<sha1[0:7]>` (plus `_<idx>` when one\n * rule spans several operations), and the hash covers the rule's semantics — so\n * *editing* a rule renames its policy. The policy under the old name is left\n * behind by `db push`, which only DROPs the names it is about to CREATE, and\n * Postgres ORs PERMISSIVE policies together: a superseded `USING (true)` keeps\n * granting everything no matter how tight its replacement is.\n *\n * Matching the shape is what makes dropping them safe. A hand-written policy\n * would have to collide with a 7-hex digest to be mistaken for generated one;\n * a policy named anything else is left alone and merely reported, because a\n * custom name is indistinguishable from one someone wrote in SQL on purpose.\n */\nexport function isGeneratedPolicyName(name: string, table: string): boolean {\n return new RegExp(`^${table.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}_(select|insert|update|delete|all)_[0-9a-f]{7}(_\\\\d+)?$`)\n .test(name);\n}\n\nexport interface OrphanCleanup {\n /** Superseded generated policies that were dropped. */\n dropped: PolicyRef[];\n /** Orphans left in place because their names are not generator-shaped. */\n kept: PolicyRef[];\n}\n\n/**\n * Drop the policies an earlier push superseded but never removed.\n *\n * Only touches tables the collections describe — a table with no expected\n * policy is not ours to reconcile, and scanning by schema alone would sweep up\n * policies belonging to something else sharing the database.\n */\nexport async function dropOrphanedPolicies(\n client: Queryable,\n drift: PolicyDrift,\n collections: CollectionConfig[]\n): Promise<OrphanCleanup> {\n const { dropped, kept } = partitionOrphans(drift, collections);\n for (const p of dropped) {\n // Identifiers are quoted, and the name came from pg_policies rather than\n // from user input, so it is already a valid identifier.\n await client.query(`DROP POLICY IF EXISTS \"${p.name}\" ON \"${p.schema}\".\"${p.table}\"`);\n }\n return { dropped, kept };\n}\n\n/**\n * Which orphans {@link dropOrphanedPolicies} drops and which it keeps, decided\n * without dropping anything: what `db push --dry-run` lists.\n */\nexport function partitionOrphans(drift: PolicyDrift, collections: CollectionConfig[]): OrphanCleanup {\n const expected = parseExpectedPolicies(generatePostgresPoliciesDdl(collections));\n const managed = new Set(expected.map((p) => `${p.schema}.${p.table}`));\n\n const cleanup: OrphanCleanup = { dropped: [], kept: [] };\n for (const p of drift.orphaned) {\n if (managed.has(`${p.schema}.${p.table}`) && isGeneratedPolicyName(p.name, p.table)) cleanup.dropped.push(p);\n else cleanup.kept.push(p);\n }\n return cleanup;\n}\n\nexport const hasDrift = (d: PolicyDrift): boolean =>\n d.missing.length > 0 || d.orphaned.length > 0 || d.diverged.length > 0 || d.insecure.length > 0\n || d.rlsDisabled.length > 0;\n\n/** Human-readable report; empty string when the database matches the config. */\nexport function formatPolicyDrift(drift: PolicyDrift): string {\n if (!hasDrift(drift)) return \"\";\n const lines: string[] = [];\n\n // First, because it subsumes every other finding on the same table: if RLS\n // is off, the policies listed below are not being applied at all.\n if (drift.rlsDisabled.length > 0) {\n lines.push(\" RLS DISABLED — the table has policies, and Postgres is applying none of them:\");\n for (const t of drift.rlsDisabled) {\n lines.push(` • ${t.schema}.${t.table}${t.forced ? \"\" : \" (and FORCE is off)\"}`);\n }\n lines.push(\" Every row is readable and writable by any request that reaches the table.\");\n lines.push(` Fix: ALTER TABLE \"<schema>\".\"<table>\" ENABLE ROW LEVEL SECURITY; or re-run \\`rebase db push\\`.`);\n }\n if (drift.missing.length > 0) {\n lines.push(\" Missing — described by your collections, absent from the database:\");\n for (const p of drift.missing) lines.push(` • ${p.schema}.${p.table} → \"${p.name}\" (${p.command} TO ${p.roles.join(\", \")})`);\n lines.push(\" Run `rebase db push` to apply them.\");\n }\n if (drift.orphaned.length > 0) {\n // Two different things share this list, and they want opposite\n // advice. A name of the shape an unnamed rule compiles to is a rule\n // edited since: the next push drops it. Any other name was written by\n // hand or by another tool, and may be the table's whole access model —\n // \"left behind by an earlier push\" was false of it, and telling its\n // author to drop it would take that access away.\n const superseded = drift.orphaned.filter((p) => isGeneratedPolicyName(p.name, p.table));\n const unowned = drift.orphaned.filter((p) => !isGeneratedPolicyName(p.name, p.table));\n lines.push(\" Orphaned — in the database, described by no collection:\");\n for (const p of drift.orphaned) lines.push(` • ${p.schema}.${p.table} → \"${p.name}\" (${p.command} TO ${p.roles.join(\", \")})`);\n if (superseded.length > 0) {\n lines.push(` ${superseded.map((p) => `\"${p.name}\"`).join(\", \")}: left behind by an earlier push, when its rule changed. The next \\`rebase db push\\` drops it.`);\n }\n if (unowned.length > 0) {\n lines.push(` ${unowned.map((p) => `\"${p.name}\"`).join(\", \")}: written by hand or by another tool, and still in force.`);\n lines.push(\" To keep one, describe it in its collection's `securityRules` (Studio's RLS view imports it);\");\n lines.push(\" if it is stale, drop it by hand.\");\n }\n }\n if (drift.diverged.length > 0) {\n lines.push(\" Diverged — same policy, different definition:\");\n for (const d of drift.diverged) {\n lines.push(` • ${d.expected.schema}.${d.expected.table} → \"${d.expected.name}\"`);\n for (const diff of d.differences) lines.push(` ${diff}`);\n }\n }\n if (drift.insecure.length > 0) {\n lines.push(\" Insecure — a live policy grants access it should not:\");\n for (const i of drift.insecure) {\n lines.push(` • ${i.policy.schema}.${i.policy.table} → \"${i.policy.name}\"`);\n lines.push(` ${i.reason}`);\n }\n }\n return lines.join(\"\\n\");\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"policy-drift-X4A-xAzv.js","names":[],"sources":["../src/security/policy-drift.ts"],"sourcesContent":["/**\n * Compare the RLS policies a database actually has against the ones the\n * collections describe.\n *\n * Policies live in Postgres; the collection config is only their *source*.\n * Nothing reconciled the two, which is how the demo database served empty\n * collections indefinitely: its policies granted `TO authenticated` (a Supabase\n * role name) while requests run as `rebase_user`, so RLS filtered every row.\n * The config was later corrected and the database never noticed — an empty\n * table is indistinguishable from a table with no data.\n *\n * Expected policies are parsed from `generatePostgresPoliciesDdl`, the same\n * function `db push` uses to write `.rebase/sql/policies.sql`, so this compares\n * against exactly what would be applied rather than a reimplementation.\n */\nimport { RLS_UID_SQL, type CollectionConfig } from \"@rebasepro/types\";\n\nimport { generatePostgresPoliciesDdl } from \"../schema/generate-postgres-ddl-logic\";\n\nexport interface PolicyRef {\n schema: string;\n table: string;\n name: string;\n /** Roles in the TO clause. */\n roles: string[];\n /** SELECT / INSERT / UPDATE / DELETE / ALL. */\n command: string;\n /** Whether a USING clause is present at all (not what it says). */\n hasUsing: boolean;\n /** Whether a WITH CHECK clause is present at all (not what it says). */\n hasWithCheck: boolean;\n /**\n * PERMISSIVE or RESTRICTIVE — the `AS` clause.\n *\n * An exact catalogue value on both sides, so it belongs with roles and\n * command rather than with the expression text. It matters more than either:\n * permissive policies are ORed together and restrictive ones ANDed, so a rule\n * declared `mode: \"restrictive\"` whose live policy is PERMISSIVE has had its\n * gate turned from a requirement into an alternative — the maximally\n * permissive way for this to be wrong.\n *\n * The DDL regex captured this from the start and the destructuring threw it\n * away; `pg_policies.permissive` was never selected.\n */\n mode?: \"PERMISSIVE\" | \"RESTRICTIVE\";\n /**\n * The live clause text, when read from `pg_policies`. Present only for live\n * policies (the expected side is parsed from DDL and does not carry it).\n * Used solely for the insecure-tautology scan, not for divergence — Postgres\n * rewrites this text, so it is not safe to diff against expected.\n */\n qual?: string | null;\n withCheck?: string | null;\n}\n\nexport interface PolicyDrift {\n /** Described by the collections, absent from the database. */\n missing: PolicyRef[];\n /** In the database, described by no collection — stale pushes live here. */\n orphaned: PolicyRef[];\n /** Same policy name, different roles or command. */\n diverged: { expected: PolicyRef; actual: PolicyRef; differences: string[] }[];\n /**\n * A live policy whose expression is the known-permissive tautology\n * `rebase.uid() IS NOT NULL` — true for anonymous visitors too, because the\n * user path coerces a blank id to the `'anonymous'` sentinel. This is what\n * `policy.authenticated()` used to compile to, so a database pushed before\n * that fix carries it, and neither the name, roles, command nor clause\n * *presence* differs from the corrected policy — the only thing that changed\n * is the expression text, which this checker otherwise (correctly) ignores.\n * So it is the one drift that hides from every other check here.\n *\n * @see reason a sentence naming the clause and what to do.\n */\n insecure: { policy: PolicyRef; reason: string }[];\n /**\n * A table the collections describe whose RLS switch is off.\n *\n * `ALTER TABLE posts DISABLE ROW LEVEL SECURITY` leaves every row in\n * `pg_policies` untouched, so before this category every expected policy\n * still matched on name, roles, command and clause presence and the checker\n * reported clean — on a table Postgres was applying no filter to at all.\n * Requests run as `rebase_user`, which holds full DML, so the table is wide\n * open while `doctor` certifies it.\n *\n * `forced` reports `relforcerowsecurity`, which is what also subjects the\n * table's *owner* to its policies. Its absence is not drift on its own —\n * Rebase does not connect as the owner in the request path — so it is\n * reported for context rather than raised as a failure.\n */\n rlsDisabled: { schema: string; table: string; forced: boolean }[];\n}\n\nexport interface Queryable {\n query<R>(text: string, values?: unknown[]): Promise<{ rows: R[] }>;\n}\n\n// The trailing group captures whichever clause follows the TO list, which is\n// what tells us the clause is present.\nconst CREATE_POLICY = /CREATE POLICY \"([^\"]+)\" ON \"([^\"]+)\"\\.\"([^\"]+)\"\\s+AS (\\w+)\\s+FOR (\\w+)\\s+TO ([^\\n]+?)(\\s+USING\\s*\\(|\\s+WITH CHECK\\s*\\(|;)/gi;\n\nconst WITH_CHECK_NEXT = /^\\s+WITH CHECK\\s*\\(/i;\n\n/**\n * Index just past the `)` closing a clause whose `(` ends at `open`.\n *\n * Needed because a policy expression nests parens and can contain a quoted\n * literal holding either character, so \"find the next `)`\" would stop early and\n * miss the `WITH CHECK` that follows.\n */\nfunction clauseEnd(ddl: string, open: number): number {\n let depth = 1;\n let inQuote = false;\n for (let i = open; i < ddl.length; i++) {\n const c = ddl[i];\n if (inQuote) {\n // '' is an escaped quote inside a string, not a close.\n if (c === \"'\") {\n if (ddl[i + 1] === \"'\") i++;\n else inQuote = false;\n }\n continue;\n }\n if (c === \"'\") inQuote = true;\n else if (c === \"(\") depth++;\n else if (c === \")\" && --depth === 0) return i + 1;\n }\n return ddl.length;\n}\n\n/**\n * `AS PERMISSIVE` / `AS RESTRICTIVE` from either side, or undefined.\n *\n * The DDL spells it as a bare word; `pg_policies.permissive` is the string\n * `\"PERMISSIVE\"`/`\"RESTRICTIVE\"` on modern Postgres but a boolean on some\n * drivers and older servers, so both are accepted. Anything unrecognised\n * becomes `undefined` and the comparison below skips it rather than inventing\n * drift from a shape we did not anticipate.\n */\nfunction normalizeMode(raw: unknown): \"PERMISSIVE\" | \"RESTRICTIVE\" | undefined {\n if (typeof raw === \"boolean\") return raw ? \"PERMISSIVE\" : \"RESTRICTIVE\";\n if (typeof raw !== \"string\") return undefined;\n const upper = raw.trim().toUpperCase();\n if (upper === \"PERMISSIVE\" || upper === \"TRUE\" || upper === \"T\") return \"PERMISSIVE\";\n if (upper === \"RESTRICTIVE\" || upper === \"FALSE\" || upper === \"F\") return \"RESTRICTIVE\";\n return undefined;\n}\n\n/** Parse the generated DDL rather than rebuilding the shape by hand. */\nexport function parseExpectedPolicies(ddl: string): PolicyRef[] {\n const found: PolicyRef[] = [];\n for (const m of ddl.matchAll(CREATE_POLICY)) {\n const [, name, schema, table, modeRaw, command, rolesRaw, clause] = m;\n const roles = rolesRaw\n .split(\",\")\n .map((r) => r.trim().replace(/^\"|\"$/g, \"\"))\n .filter(Boolean);\n\n // The generator emits USING before WITH CHECK, so what follows the TO\n // list settles USING; WITH CHECK is then whatever follows that clause.\n const hasUsing = /USING/i.test(clause);\n const hasWithCheck = hasUsing\n ? WITH_CHECK_NEXT.test(ddl.slice(clauseEnd(ddl, m.index + m[0].length)))\n : /WITH CHECK/i.test(clause);\n\n found.push({\n schema, table, name, roles,\n command: command.toUpperCase(),\n hasUsing, hasWithCheck,\n mode: normalizeMode(modeRaw)\n });\n }\n return found;\n}\n\nasync function readLivePolicies(client: Queryable, schemas: string[]): Promise<PolicyRef[]> {\n const { rows } = await client.query<{\n schemaname: string; tablename: string; policyname: string; roles: string[] | string; cmd: string;\n qual: string | null; with_check: string | null; permissive: string | boolean | null;\n }>(\n `SELECT schemaname, tablename, policyname, roles, cmd, qual, with_check, permissive\n FROM pg_policies\n WHERE schemaname = ANY($1)`,\n [schemas]\n );\n\n return rows.map((r) => ({\n schema: r.schemaname,\n table: r.tablename,\n name: r.policyname,\n // node-postgres yields text[] as an array; some drivers hand back \"{a,b}\".\n roles: Array.isArray(r.roles)\n ? r.roles\n : String(r.roles ?? \"\").replace(/^\\{|\\}$/g, \"\").split(\",\").filter(Boolean),\n command: (r.cmd ?? \"ALL\").toUpperCase(),\n // Presence only. Postgres rewrites the text, but it does not invent or\n // drop a clause: NULL here means the policy genuinely has none.\n hasUsing: r.qual != null,\n hasWithCheck: r.with_check != null,\n mode: normalizeMode(r.permissive),\n qual: r.qual,\n withCheck: r.with_check\n }));\n}\n\n/**\n * Tables the collections describe whose RLS switch is off.\n *\n * Read from `pg_class` because `pg_policies` cannot answer it: disabling RLS\n * leaves the policy rows in place, so every other comparison in this file keeps\n * matching while Postgres applies none of them.\n *\n * Only tables that expect at least one policy are considered. A table with no\n * declared rules is not asserting anything about RLS, and reporting it would\n * make the check noisy on exactly the projects least able to judge the noise.\n */\nasync function readTablesWithRlsOff(\n client: Queryable,\n tables: { schema: string; table: string }[]\n): Promise<{ schema: string; table: string; forced: boolean }[]> {\n if (tables.length === 0) return [];\n const { rows } = await client.query<{\n schemaname: string; tablename: string; relrowsecurity: boolean; relforcerowsecurity: boolean;\n }>(\n `SELECT n.nspname AS schemaname,\n c.relname AS tablename,\n c.relrowsecurity,\n c.relforcerowsecurity\n FROM pg_class c\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE c.relkind = 'r'\n AND (n.nspname || '.' || c.relname) = ANY($1)`,\n [tables.map((t) => `${t.schema}.${t.table}`)]\n );\n\n return rows\n .filter((r) => !r.relrowsecurity)\n .map((r) => ({\n schema: r.schemaname,\n table: r.tablename,\n forced: !!r.relforcerowsecurity\n }));\n}\n\n/**\n * The permissive tautology `rebase.uid() IS NOT NULL`, without the\n * `<> 'anonymous'` guard that makes it mean \"signed in\".\n *\n * Whitespace varies with Postgres's rewrite, so match on a collapsed form. The\n * guard clause ({@link EXCLUDES_ANONYMOUS}, in any spelling) is what\n * distinguishes the corrected policy from the stale one, so its presence\n * clears the text.\n *\n * Both schema spellings are matched. The helpers moved from `auth` to `rebase`\n * in 1.0, and this check reads policies *as the database stored them*: a\n * database pushed before the move still holds `auth.uid()` until it is\n * recompiled, while everything written since — including raw `securityRules`\n * SQL, which the compiler rewrites on the way in — stores `rebase.uid()`.\n * Matching only the pre-1.0 name made the check blind to every policy a current\n * release could write, which is precisely the set still worth scanning.\n */\nfunction isPermissiveAuthTautology(clause: string | null | undefined): boolean {\n if (!clause) return false;\n const flat = clause.toLowerCase().replace(/\\s+/g, \" \");\n // The call bare, or wrapped as an initPlan, which Postgres stores as\n // `( SELECT rebase.uid() AS uid)`.\n if (!/\\b(?:rebase|auth)\\.uid\\s*\\(\\s*\\)(?:\\s+as\\s+\\w+\\s*\\))?\\s*is not null/.test(flat)) return false;\n return !EXCLUDES_ANONYMOUS.some((guard) => guard.test(flat));\n}\n\n/**\n * The guard that makes the null test mean \"signed in\": the anonymous\n * sentinel excluded, in every spelling a policy reaches the catalogue in.\n *\n * `policy.authenticated()` compiles to `NOT IN ('anonymous', 'anon')`, which\n * Postgres stores as `<> ALL (ARRAY['anonymous'::text, 'anon'::text])`. Only\n * `<> 'anonymous'` was recognised, so `doctor` and every `db push` called each\n * `authenticated()` and `registered()` policy the insecure tautology. A list\n * that leaves out `'anonymous'` excludes nobody a signed-out request arrives\n * as, and does not clear it.\n */\nconst EXCLUDES_ANONYMOUS: RegExp[] = [\n /(?:<>|!=)\\s*'anonymous'/,\n /(?:<>|!=)\\s*all\\s*\\(\\s*array\\s*\\[[^\\]]*'anonymous'/,\n /\\bnot in\\s*\\([^)]*'anonymous'/\n];\n\nconst keyOf = (p: PolicyRef) => `${p.schema}.${p.table}.${p.name}`;\nconst sameRoles = (a: string[], b: string[]) =>\n a.length === b.length && [...a].sort().join(\",\") === [...b].sort().join(\",\");\n\n/**\n * Diff expected against live.\n *\n * Compares names, roles, command, and whether each clause exists — all exact\n * values. Policy expression *text* is deliberately not compared: Postgres\n * rewrites `qual`/`with_check` when storing them (parenthesising, casting,\n * schema-qualifying), so text comparison reports drift that does not exist, and\n * a check that cries wolf gets ignored.\n *\n * Presence is not text, though. A NULL `qual` is not a rewrite of an\n * expression, it is the absence of one, and absence has no false-positive risk:\n * either the generator emitted a clause or it did not. That distinction is worth\n * the extra comparison — a production database was found with a SELECT policy\n * whose `qual` was NULL, matching on every field this checked and denying 100%\n * of reads. The same blindness would hide a policy that fails open.\n */\nexport async function checkPolicyDrift(\n client: Queryable,\n collections: CollectionConfig[]\n): Promise<PolicyDrift> {\n const expected = parseExpectedPolicies(generatePostgresPoliciesDdl(collections));\n const schemas = [...new Set(expected.map((p) => p.schema))];\n // Nothing expected means nothing to reconcile against; scanning every\n // schema would report the whole database as orphaned.\n if (schemas.length === 0) return { missing: [], orphaned: [], diverged: [], insecure: [], rlsDisabled: [] };\n\n const live = await readLivePolicies(client, schemas);\n const liveByKey = new Map(live.map((p) => [keyOf(p), p]));\n const expectedByKey = new Map(expected.map((p) => [keyOf(p), p]));\n\n const drift: PolicyDrift = { missing: [], orphaned: [], diverged: [], insecure: [], rlsDisabled: [] };\n\n // Before comparing policy against policy: is the switch even on? A table\n // with RLS disabled matches every expected policy on every field compared\n // below, so this has to be asked separately or it cannot be asked at all.\n const expectedTables = [...new Map(\n expected.map((p) => [`${p.schema}.${p.table}`, { schema: p.schema, table: p.table }])\n ).values()];\n drift.rlsDisabled = await readTablesWithRlsOff(client, expectedTables);\n\n // Scan every live policy for the permissive tautology. This is deliberately\n // independent of the name-keyed diff below: a database pushed before the\n // `authenticated()` fix matches its expected policy on name, roles, command\n // and clause presence, so nothing else here would flag it.\n for (const p of live) {\n const clause = isPermissiveAuthTautology(p.qual)\n ? \"USING\"\n : isPermissiveAuthTautology(p.withCheck) ? \"WITH CHECK\" : null;\n if (clause) {\n drift.insecure.push({\n policy: p,\n reason: `${clause} is \\`${RLS_UID_SQL} IS NOT NULL\\`, which is true for anonymous ` +\n `visitors too — this grants access to signed-out requests. It predates the ` +\n `\\`policy.authenticated()\\` fix; re-run \\`rebase db push\\` to tighten it.`\n });\n }\n }\n\n for (const [key, want] of expectedByKey) {\n const got = liveByKey.get(key);\n if (!got) {\n drift.missing.push(want);\n continue;\n }\n const differences: string[] = [];\n if (!sameRoles(want.roles, got.roles)) {\n differences.push(`roles: expected [${want.roles.join(\", \")}], database has [${got.roles.join(\", \")}]`);\n }\n if (want.command !== got.command) {\n differences.push(`command: expected ${want.command}, database has ${got.command}`);\n }\n // Skipped when either side is unknown: an unrecognised catalogue shape\n // should not manufacture drift. See `normalizeMode`.\n if (want.mode && got.mode && want.mode !== got.mode) {\n differences.push(\n `mode: expected ${want.mode}, database has ${got.mode}` +\n (got.mode === \"PERMISSIVE\"\n ? \" — a RESTRICTIVE rule is ANDed with the others, a PERMISSIVE one is ORed, so this policy now widens access instead of narrowing it\"\n : \" — a PERMISSIVE rule is ORed with the others, a RESTRICTIVE one is ANDed, so this policy now narrows access instead of widening it\")\n );\n }\n for (const clause of [\"USING\", \"WITH CHECK\"] as const) {\n const key = clause === \"USING\" ? \"hasUsing\" : \"hasWithCheck\";\n if (want[key] === got[key]) continue;\n differences.push(want[key]\n ? `${clause}: expected an expression, database has none — this policy matches no rows`\n : `${clause}: expected none, database has an expression`);\n }\n if (differences.length > 0) drift.diverged.push({ expected: want, actual: got, differences });\n }\n\n for (const [key, got] of liveByKey) {\n if (!expectedByKey.has(key)) drift.orphaned.push(got);\n }\n\n return drift;\n}\n\n/**\n * Does this name look like one the generator produced for this table?\n *\n * Unnamed rules compile to `<table>_<op>_<sha1[0:7]>` (plus `_<idx>` when one\n * rule spans several operations), and the hash covers the rule's semantics — so\n * *editing* a rule renames its policy. The policy under the old name is left\n * behind by `db push`, which only DROPs the names it is about to CREATE, and\n * Postgres ORs PERMISSIVE policies together: a superseded `USING (true)` keeps\n * granting everything no matter how tight its replacement is.\n *\n * Matching the shape is what makes dropping them safe. A hand-written policy\n * would have to collide with a 7-hex digest to be mistaken for generated one;\n * a policy named anything else is left alone and merely reported, because a\n * custom name is indistinguishable from one someone wrote in SQL on purpose.\n */\nexport function isGeneratedPolicyName(name: string, table: string): boolean {\n return new RegExp(`^${table.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}_(select|insert|update|delete|all)_[0-9a-f]{7}(_\\\\d+)?$`)\n .test(name);\n}\n\nexport interface OrphanCleanup {\n /** Superseded generated policies that were dropped. */\n dropped: PolicyRef[];\n /** Orphans left in place because their names are not generator-shaped. */\n kept: PolicyRef[];\n}\n\n/**\n * Drop the policies an earlier push superseded but never removed.\n *\n * Only touches tables the collections describe — a table with no expected\n * policy is not ours to reconcile, and scanning by schema alone would sweep up\n * policies belonging to something else sharing the database.\n */\nexport async function dropOrphanedPolicies(\n client: Queryable,\n drift: PolicyDrift,\n collections: CollectionConfig[]\n): Promise<OrphanCleanup> {\n const { dropped, kept } = partitionOrphans(drift, collections);\n for (const p of dropped) {\n // Identifiers are quoted, and the name came from pg_policies rather than\n // from user input, so it is already a valid identifier.\n await client.query(`DROP POLICY IF EXISTS \"${p.name}\" ON \"${p.schema}\".\"${p.table}\"`);\n }\n return { dropped, kept };\n}\n\n/**\n * Which orphans {@link dropOrphanedPolicies} drops and which it keeps, decided\n * without dropping anything: what `db push --dry-run` lists.\n */\nexport function partitionOrphans(drift: PolicyDrift, collections: CollectionConfig[]): OrphanCleanup {\n const expected = parseExpectedPolicies(generatePostgresPoliciesDdl(collections));\n const managed = new Set(expected.map((p) => `${p.schema}.${p.table}`));\n\n const cleanup: OrphanCleanup = { dropped: [], kept: [] };\n for (const p of drift.orphaned) {\n if (managed.has(`${p.schema}.${p.table}`) && isGeneratedPolicyName(p.name, p.table)) cleanup.dropped.push(p);\n else cleanup.kept.push(p);\n }\n return cleanup;\n}\n\nexport const hasDrift = (d: PolicyDrift): boolean =>\n d.missing.length > 0 || d.orphaned.length > 0 || d.diverged.length > 0 || d.insecure.length > 0\n || d.rlsDisabled.length > 0;\n\n/** Human-readable report; empty string when the database matches the config. */\nexport function formatPolicyDrift(drift: PolicyDrift): string {\n if (!hasDrift(drift)) return \"\";\n const lines: string[] = [];\n\n // First, because it subsumes every other finding on the same table: if RLS\n // is off, the policies listed below are not being applied at all.\n if (drift.rlsDisabled.length > 0) {\n lines.push(\" RLS DISABLED — the table has policies, and Postgres is applying none of them:\");\n for (const t of drift.rlsDisabled) {\n lines.push(` • ${t.schema}.${t.table}${t.forced ? \"\" : \" (and FORCE is off)\"}`);\n }\n lines.push(\" Every row is readable and writable by any request that reaches the table.\");\n lines.push(` Fix: ALTER TABLE \"<schema>\".\"<table>\" ENABLE ROW LEVEL SECURITY; or re-run \\`rebase db push\\`.`);\n }\n if (drift.missing.length > 0) {\n lines.push(\" Missing — described by your collections, absent from the database:\");\n for (const p of drift.missing) lines.push(` • ${p.schema}.${p.table} → \"${p.name}\" (${p.command} TO ${p.roles.join(\", \")})`);\n lines.push(\" Run `rebase db push` to apply them.\");\n }\n if (drift.orphaned.length > 0) {\n // Two different things share this list, and they want opposite\n // advice. A name of the shape an unnamed rule compiles to is a rule\n // edited since: the next push drops it. Any other name was written by\n // hand or by another tool, and may be the table's whole access model —\n // \"left behind by an earlier push\" was false of it, and telling its\n // author to drop it would take that access away.\n const superseded = drift.orphaned.filter((p) => isGeneratedPolicyName(p.name, p.table));\n const unowned = drift.orphaned.filter((p) => !isGeneratedPolicyName(p.name, p.table));\n lines.push(\" Orphaned — in the database, described by no collection:\");\n for (const p of drift.orphaned) lines.push(` • ${p.schema}.${p.table} → \"${p.name}\" (${p.command} TO ${p.roles.join(\", \")})`);\n if (superseded.length > 0) {\n lines.push(` ${superseded.map((p) => `\"${p.name}\"`).join(\", \")}: left behind by an earlier push, when its rule changed. The next \\`rebase db push\\` drops it.`);\n }\n if (unowned.length > 0) {\n lines.push(` ${unowned.map((p) => `\"${p.name}\"`).join(\", \")}: written by hand or by another tool, and still in force.`);\n lines.push(\" To keep one, describe it in its collection's `securityRules` (Studio's RLS view imports it);\");\n lines.push(\" if it is stale, drop it by hand.\");\n }\n }\n if (drift.diverged.length > 0) {\n lines.push(\" Diverged — same policy, different definition:\");\n for (const d of drift.diverged) {\n lines.push(` • ${d.expected.schema}.${d.expected.table} → \"${d.expected.name}\"`);\n for (const diff of d.differences) lines.push(` ${diff}`);\n }\n }\n if (drift.insecure.length > 0) {\n lines.push(\" Insecure — a live policy grants access it should not:\");\n for (const i of drift.insecure) {\n lines.push(` • ${i.policy.schema}.${i.policy.table} → \"${i.policy.name}\"`);\n lines.push(` ${i.reason}`);\n }\n }\n return lines.join(\"\\n\");\n}\n"],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * What a pool says when the server ends its idle connections.
3
+ *
4
+ * `pg_terminate_backend` ends every connection to a database at once, and an
5
+ * idle pool client hears it as an `error` event: one per connection, from
6
+ * every pool the process holds. `rebase db branch create --force` does exactly
7
+ * that to the database it copies, so fifteen forced creates beside a running
8
+ * `rebase dev` printed 220 `❌ [ERROR] [pg-pool] Unexpected pool error
9
+ * {"detail":"terminating connection due to administrator command"}` lines. It
10
+ * read as an outage, and nothing was wrong: the pool opens new connections on
11
+ * the next query. A database restart looks the same.
12
+ */
13
+ import { logger } from "@rebasepro/server";
14
+ /** Did the server end this connection, rather than something go wrong with it? */
15
+ export declare function serverEndedConnection(err: Error): boolean;
16
+ /** How long one line stands for every server-ended connection after it, across all pools. */
17
+ export declare const DISCONNECT_BURST_MS = 10000;
18
+ /** The logger methods a pool's error listener writes through. */
19
+ export type PoolErrorLog = Pick<typeof logger, "warn" | "error" | "debug">;
20
+ /**
21
+ * Say a pool's `error` event: a connection the server ended is one WARN line
22
+ * per burst for the whole process, and the rest of the burst is at debug;
23
+ * anything else is the ERROR it always was.
24
+ *
25
+ * The window is fixed from the first disconnect, not extended by each, so a
26
+ * second burst ten seconds later is said again.
27
+ */
28
+ export declare function reportPoolError(label: string, err: Error, log?: PoolErrorLog, now?: () => number): void;
@@ -1,8 +1,8 @@
1
1
  import { createRequire as __createRequire } from "module";
2
2
  __createRequire(import.meta.url);
3
- import { n as planSchema } from "./plan-schema-CXOucCw-.js";
4
- import { s as diffPlanAgainstCatalogue } from "./deferred-index-build-Cis_HoGn.js";
5
- import { i as readExistingSchema } from "./ensure-collection-tables-DvvImXDe.js";
3
+ import { n as planSchema } from "./plan-schema-Czf3q4-Q.js";
4
+ import { s as diffPlanAgainstCatalogue } from "./deferred-index-build-CqaOv1Xx.js";
5
+ import { i as readExistingSchema } from "./ensure-collection-tables-CFIRq8ni.js";
6
6
  import { declaredDatabaseExtensions } from "@rebasepro/types";
7
7
  //#region src/schema/push-index-prebuild.ts
8
8
  /**
@@ -42,4 +42,4 @@ async function missingForeignKeyIndexes(client, collections) {
42
42
  //#endregion
43
43
  export { missingForeignKeyIndexes };
44
44
 
45
- //# sourceMappingURL=push-index-prebuild-DIequ4bV.js.map
45
+ //# sourceMappingURL=push-index-prebuild-UTvB5pqH.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"push-index-prebuild-DIequ4bV.js","names":[],"sources":["../src/schema/push-index-prebuild.ts"],"sourcesContent":["/**\n * The foreign-key indexes `rebase db push` builds before Atlas plans.\n *\n * Rebase derives a btree for every `belongsTo` key and every junction's target\n * column (15b00afcd). On a table that already exists — every table of a\n * project upgraded across that release — the index is missing, and a push left\n * it to Atlas, which builds it inside the push's one transaction: a plain\n * `CREATE INDEX`, holding writes to the table for the whole build, about 0.4 s\n * at a million rows and 6 s at ten million. Atlas's own concurrent mode would\n * run the entire apply outside a transaction, which the push relies on.\n *\n * So the push builds those first, `CONCURRENTLY`, with the builder the boot\n * uses for the same indexes (`deferred-index-build.ts`: one at a time, a lock\n * budget for getting the table, an INVALID leftover dropped and rebuilt, a\n * claim shared with booting instances). Atlas then finds them and plans\n * nothing for them. One it could not build is still Atlas's, as before.\n *\n * Only an index whose columns are already there: a column this push adds is\n * created by Atlas, and its index with it, on a column that holds no values.\n */\nimport { declaredDatabaseExtensions, type CollectionConfig } from \"@rebasepro/types\";\nimport { planSchema } from \"./plan/plan-schema\";\nimport { diffPlanAgainstCatalogue, type DeferredIndexBuild } from \"./plan/diff-plan\";\nimport { readExistingSchema, type Queryable } from \"./ensure-collection-tables\";\n\n/** The derived foreign-key indexes a live table lacks (or holds INVALID) whose columns it already has. */\nexport async function missingForeignKeyIndexes(\n client: Queryable,\n collections: CollectionConfig[]\n): Promise<DeferredIndexBuild[]> {\n const schema = planSchema(collections, { databaseExtensions: declaredDatabaseExtensions() });\n const schemas = Array.from(new Set(schema.tables.map(table => table.schema)));\n // No row probes: whether a table holds rows decides only constraints on a\n // column being added, which this does not plan.\n const existing = await readExistingSchema(client, schemas, { probeTables: () => [] });\n const plan = diffPlanAgainstCatalogue(schema, existing);\n\n const specs = new Map(schema.tables.flatMap(table => table.indexes)\n .map(spec => [`${spec.schema}.${spec.indexName}`, spec]));\n return plan.deferredIndexes.filter(build => {\n const spec = specs.get(`${build.schema}.${build.indexName}`);\n if (spec?.derived !== \"foreign-key\") return false;\n const columns = existing.tables.get(`${build.schema}.${build.table}`);\n if (!columns) return false;\n return [...spec.keys.map(key => key.column), ...spec.include].every(column => columns.has(column));\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,eAAsB,yBAClB,QACA,aAC6B;CAC7B,MAAM,SAAS,WAAW,aAAa,EAAE,oBAAoB,2BAA2B,EAAE,CAAC;CAI3F,MAAM,WAAW,MAAM,mBAAmB,QAH1B,MAAM,KAAK,IAAI,IAAI,OAAO,OAAO,KAAI,UAAS,MAAM,MAAM,CAAC,CAGzB,GAAS,EAAE,mBAAmB,CAAC,EAAE,CAAC;CACpF,MAAM,OAAO,yBAAyB,QAAQ,QAAQ;CAEtD,MAAM,QAAQ,IAAI,IAAI,OAAO,OAAO,SAAQ,UAAS,MAAM,OAAO,CAAC,CAC9D,KAAI,SAAQ,CAAC,GAAG,KAAK,OAAO,GAAG,KAAK,aAAa,IAAI,CAAC,CAAC;CAC5D,OAAO,KAAK,gBAAgB,QAAO,UAAS;EACxC,MAAM,OAAO,MAAM,IAAI,GAAG,MAAM,OAAO,GAAG,MAAM,WAAW;EAC3D,IAAI,MAAM,YAAY,eAAe,OAAO;EAC5C,MAAM,UAAU,SAAS,OAAO,IAAI,GAAG,MAAM,OAAO,GAAG,MAAM,OAAO;EACpE,IAAI,CAAC,SAAS,OAAO;EACrB,OAAO,CAAC,GAAG,KAAK,KAAK,KAAI,QAAO,IAAI,MAAM,GAAG,GAAG,KAAK,OAAO,CAAC,CAAC,OAAM,WAAU,QAAQ,IAAI,MAAM,CAAC;CACrG,CAAC;AACL"}
1
+ {"version":3,"file":"push-index-prebuild-UTvB5pqH.js","names":[],"sources":["../src/schema/push-index-prebuild.ts"],"sourcesContent":["/**\n * The foreign-key indexes `rebase db push` builds before Atlas plans.\n *\n * Rebase derives a btree for every `belongsTo` key and every junction's target\n * column (15b00afcd). On a table that already exists — every table of a\n * project upgraded across that release — the index is missing, and a push left\n * it to Atlas, which builds it inside the push's one transaction: a plain\n * `CREATE INDEX`, holding writes to the table for the whole build, about 0.4 s\n * at a million rows and 6 s at ten million. Atlas's own concurrent mode would\n * run the entire apply outside a transaction, which the push relies on.\n *\n * So the push builds those first, `CONCURRENTLY`, with the builder the boot\n * uses for the same indexes (`deferred-index-build.ts`: one at a time, a lock\n * budget for getting the table, an INVALID leftover dropped and rebuilt, a\n * claim shared with booting instances). Atlas then finds them and plans\n * nothing for them. One it could not build is still Atlas's, as before.\n *\n * Only an index whose columns are already there: a column this push adds is\n * created by Atlas, and its index with it, on a column that holds no values.\n */\nimport { declaredDatabaseExtensions, type CollectionConfig } from \"@rebasepro/types\";\nimport { planSchema } from \"./plan/plan-schema\";\nimport { diffPlanAgainstCatalogue, type DeferredIndexBuild } from \"./plan/diff-plan\";\nimport { readExistingSchema, type Queryable } from \"./ensure-collection-tables\";\n\n/** The derived foreign-key indexes a live table lacks (or holds INVALID) whose columns it already has. */\nexport async function missingForeignKeyIndexes(\n client: Queryable,\n collections: CollectionConfig[]\n): Promise<DeferredIndexBuild[]> {\n const schema = planSchema(collections, { databaseExtensions: declaredDatabaseExtensions() });\n const schemas = Array.from(new Set(schema.tables.map(table => table.schema)));\n // No row probes: whether a table holds rows decides only constraints on a\n // column being added, which this does not plan.\n const existing = await readExistingSchema(client, schemas, { probeTables: () => [] });\n const plan = diffPlanAgainstCatalogue(schema, existing);\n\n const specs = new Map(schema.tables.flatMap(table => table.indexes)\n .map(spec => [`${spec.schema}.${spec.indexName}`, spec]));\n return plan.deferredIndexes.filter(build => {\n const spec = specs.get(`${build.schema}.${build.indexName}`);\n if (spec?.derived !== \"foreign-key\") return false;\n const columns = existing.tables.get(`${build.schema}.${build.table}`);\n if (!columns) return false;\n return [...spec.keys.map(key => key.column), ...spec.include].every(column => columns.has(column));\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,eAAsB,yBAClB,QACA,aAC6B;CAC7B,MAAM,SAAS,WAAW,aAAa,EAAE,oBAAoB,2BAA2B,EAAE,CAAC;CAI3F,MAAM,WAAW,MAAM,mBAAmB,QAH1B,MAAM,KAAK,IAAI,IAAI,OAAO,OAAO,KAAI,UAAS,MAAM,MAAM,CAAC,CAGzB,GAAS,EAAE,mBAAmB,CAAC,EAAE,CAAC;CACpF,MAAM,OAAO,yBAAyB,QAAQ,QAAQ;CAEtD,MAAM,QAAQ,IAAI,IAAI,OAAO,OAAO,SAAQ,UAAS,MAAM,OAAO,CAAC,CAC9D,KAAI,SAAQ,CAAC,GAAG,KAAK,OAAO,GAAG,KAAK,aAAa,IAAI,CAAC,CAAC;CAC5D,OAAO,KAAK,gBAAgB,QAAO,UAAS;EACxC,MAAM,OAAO,MAAM,IAAI,GAAG,MAAM,OAAO,GAAG,MAAM,WAAW;EAC3D,IAAI,MAAM,YAAY,eAAe,OAAO;EAC5C,MAAM,UAAU,SAAS,OAAO,IAAI,GAAG,MAAM,OAAO,GAAG,MAAM,OAAO;EACpE,IAAI,CAAC,SAAS,OAAO;EACrB,OAAO,CAAC,GAAG,KAAK,KAAK,KAAI,QAAO,IAAI,MAAM,GAAG,GAAG,KAAK,OAAO,CAAC,CAAC,OAAM,WAAU,QAAQ,IAAI,MAAM,CAAC;CACrG,CAAC;AACL"}
@@ -0,0 +1,137 @@
1
+ import { createRequire as __createRequire } from "module";
2
+ __createRequire(import.meta.url);
3
+ import { u as resolveColumnName } from "./column-plan-helpers-IG6uFb1s.js";
4
+ import { n as planSchema } from "./plan-schema-Czf3q4-Q.js";
5
+ import { declaredDatabaseExtensions } from "@rebasepro/types";
6
+ //#region src/schema/push-kept-defaults.ts
7
+ /**
8
+ * The DEFAULTs `db push` keeps because a property says the database owns them.
9
+ *
10
+ * `databaseDefault: true` declares a DEFAULT somebody else wrote: a DBA's
11
+ * `now()`, a migration's `'X-' || gen_random_uuid()`. The planner writes no
12
+ * DEFAULT for it, since it has no expression to write, so `schema.sql` holds
13
+ * the column without one. Atlas reads that file as the whole desired state and
14
+ * planned `ALTER COLUMN … DROP DEFAULT`. The API kept letting creates through
15
+ * without the field, and the database then refused them on NOT NULL.
16
+ *
17
+ * Atlas has no way to leave one column's DEFAULT alone: `--exclude` names
18
+ * objects, and a DEFAULT is not one. So the desired state is made to agree with
19
+ * the database instead. Before Atlas plans, the live DEFAULT of each such
20
+ * column is read and appended to `schema.sql` as `ALTER TABLE … SET DEFAULT`,
21
+ * and Atlas finds nothing to change.
22
+ *
23
+ * Two kinds are left out, because Atlas already does something else with them:
24
+ *
25
+ * - a DEFAULT reading a sequence (`nextval(…)`). Atlas does not manage
26
+ * sequences and leaves those DEFAULTs alone, and the sequence does not exist
27
+ * in the scratch database Atlas loads `schema.sql` into.
28
+ * - a DEFAULT calling a function or operator the database defines (an
29
+ * extension's `uuid_generate_v4()`, a project's own function). The scratch
30
+ * database has none of them, and Atlas refuses such a column whether or not
31
+ * it is in `schema.sql`; adding it would only change the error.
32
+ */
33
+ /**
34
+ * Every column declared `databaseDefault: true` that the planner writes no
35
+ * DEFAULT for. An id strategy or `autoValue` writes one of its own, and that
36
+ * one is the planner's to keep.
37
+ */
38
+ function databaseOwnedDefaultColumns(collections) {
39
+ const declared = collections.filter((collection) => Object.values(collection.properties ?? {}).some((prop) => isDatabaseOwned(prop)));
40
+ if (declared.length === 0) return [];
41
+ const plan = planSchema(collections, { databaseExtensions: declaredDatabaseExtensions() });
42
+ const found = [];
43
+ for (const table of plan.tables) {
44
+ const collection = declared.find((each) => each.slug === table.slug);
45
+ if (!collection) continue;
46
+ for (const [key, prop] of Object.entries(collection.properties ?? {})) {
47
+ if (!isDatabaseOwned(prop)) continue;
48
+ const name = resolveColumnName(key, prop);
49
+ const column = table.columns.find((each) => each.column === name);
50
+ if (!column || column.default || column.generated) continue;
51
+ found.push({
52
+ schema: table.schema,
53
+ table: table.table,
54
+ column: name
55
+ });
56
+ }
57
+ }
58
+ return found;
59
+ }
60
+ var isDatabaseOwned = (prop) => typeof prop === "object" && prop !== null && prop.databaseDefault === true;
61
+ /**
62
+ * Every column DEFAULT outside the system schemas, and whether Atlas can carry
63
+ * it (see the module comment for the two it cannot).
64
+ *
65
+ * Rendered with only `pg_catalog` on the search path, so anything outside it
66
+ * comes back schema-qualified and reads the same in whichever database
67
+ * `schema.sql` is loaded into.
68
+ */
69
+ var LIVE_DEFAULTS_SQL = `
70
+ SELECT n.nspname AS "schema",
71
+ c.relname AS "table",
72
+ a.attname AS "column",
73
+ pg_get_expr(d.adbin, d.adrelid) AS "expression",
74
+ EXISTS (
75
+ SELECT 1
76
+ FROM pg_depend dep
77
+ WHERE dep.classid = 'pg_attrdef'::regclass
78
+ AND dep.objid = d.oid
79
+ AND (
80
+ (dep.refclassid = 'pg_class'::regclass
81
+ AND EXISTS (SELECT 1 FROM pg_class s WHERE s.oid = dep.refobjid AND s.relkind = 'S'))
82
+ OR (dep.refclassid IN ('pg_proc'::regclass, 'pg_operator'::regclass)
83
+ AND dep.refobjid >= 16384)
84
+ )
85
+ ) AS "uncarried"
86
+ FROM pg_attrdef d
87
+ JOIN pg_attribute a ON a.attrelid = d.adrelid AND a.attnum = d.adnum
88
+ JOIN pg_class c ON c.oid = d.adrelid
89
+ JOIN pg_namespace n ON n.oid = c.relnamespace
90
+ WHERE NOT a.attisdropped
91
+ AND a.attgenerated = ''
92
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema')
93
+ AND n.nspname NOT LIKE 'pg\\_%'
94
+ `;
95
+ /**
96
+ * The live DEFAULTs of `columns` that the desired state must carry so Atlas
97
+ * plans no `DROP DEFAULT`. A column with no DEFAULT, or one Atlas cannot carry,
98
+ * is not listed.
99
+ *
100
+ * Read in a transaction of its own, so the search path it narrows goes back
101
+ * when it ends.
102
+ */
103
+ async function readKeptDefaults(client, columns) {
104
+ if (columns.length === 0) return [];
105
+ const wanted = new Set(columns.map(keyOf));
106
+ let rows;
107
+ await client.query("BEGIN READ ONLY");
108
+ try {
109
+ await client.query("SET LOCAL search_path TO pg_catalog");
110
+ rows = (await client.query(LIVE_DEFAULTS_SQL)).rows;
111
+ } finally {
112
+ await client.query("ROLLBACK");
113
+ }
114
+ return rows.filter((row) => wanted.has(keyOf(row)) && !row.uncarried && row.expression).map(({ schema, table, column, expression }) => ({
115
+ schema,
116
+ table,
117
+ column,
118
+ expression
119
+ })).sort((a, b) => keyOf(a).localeCompare(keyOf(b)));
120
+ }
121
+ var keyOf = (column) => `${column.schema}\u0000${column.table}\u0000${column.column}`;
122
+ var ident = (name) => `"${name.replace(/"/g, "\"\"")}"`;
123
+ /** What is appended to `schema.sql`: one `SET DEFAULT` per kept DEFAULT. */
124
+ function keptDefaultStatements(kept) {
125
+ return kept.map(({ schema, table, column, expression }) => `ALTER TABLE ${ident(schema)}.${ident(table)} ALTER COLUMN ${ident(column)} SET DEFAULT ${expression};`);
126
+ }
127
+ /** `schema.sql` with the kept DEFAULTs appended, under a comment saying why. */
128
+ function withKeptDefaults(desiredSql, kept) {
129
+ if (kept.length === 0) return desiredSql;
130
+ return `${desiredSql.replace(/\s*$/, "\n")}\n-- DEFAULTs the database holds for columns declared \`databaseDefault: true\`,
131
+ -- read before planning so the push leaves them as they are.
132
+ ${keptDefaultStatements(kept).join("\n")}\n`;
133
+ }
134
+ //#endregion
135
+ export { databaseOwnedDefaultColumns, readKeptDefaults, withKeptDefaults };
136
+
137
+ //# sourceMappingURL=push-kept-defaults-BGOlOqRQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push-kept-defaults-BGOlOqRQ.js","names":[],"sources":["../src/schema/push-kept-defaults.ts"],"sourcesContent":["/**\n * The DEFAULTs `db push` keeps because a property says the database owns them.\n *\n * `databaseDefault: true` declares a DEFAULT somebody else wrote: a DBA's\n * `now()`, a migration's `'X-' || gen_random_uuid()`. The planner writes no\n * DEFAULT for it, since it has no expression to write, so `schema.sql` holds\n * the column without one. Atlas reads that file as the whole desired state and\n * planned `ALTER COLUMN … DROP DEFAULT`. The API kept letting creates through\n * without the field, and the database then refused them on NOT NULL.\n *\n * Atlas has no way to leave one column's DEFAULT alone: `--exclude` names\n * objects, and a DEFAULT is not one. So the desired state is made to agree with\n * the database instead. Before Atlas plans, the live DEFAULT of each such\n * column is read and appended to `schema.sql` as `ALTER TABLE … SET DEFAULT`,\n * and Atlas finds nothing to change.\n *\n * Two kinds are left out, because Atlas already does something else with them:\n *\n * - a DEFAULT reading a sequence (`nextval(…)`). Atlas does not manage\n * sequences and leaves those DEFAULTs alone, and the sequence does not exist\n * in the scratch database Atlas loads `schema.sql` into.\n * - a DEFAULT calling a function or operator the database defines (an\n * extension's `uuid_generate_v4()`, a project's own function). The scratch\n * database has none of them, and Atlas refuses such a column whether or not\n * it is in `schema.sql`; adding it would only change the error.\n */\nimport { declaredDatabaseExtensions, type CollectionConfig, type Property } from \"@rebasepro/types\";\nimport { planSchema } from \"./plan/plan-schema\";\nimport { resolveColumnName } from \"./column-plan-helpers\";\nimport type { Queryable } from \"./ensure-collection-tables\";\n\n/** A column whose DEFAULT the collections leave to the database. */\nexport interface DatabaseOwnedDefaultColumn {\n schema: string;\n table: string;\n column: string;\n}\n\n/** A DEFAULT the database holds for one of those columns, as SQL. */\nexport interface KeptDefault extends DatabaseOwnedDefaultColumn {\n expression: string;\n}\n\n/**\n * Every column declared `databaseDefault: true` that the planner writes no\n * DEFAULT for. An id strategy or `autoValue` writes one of its own, and that\n * one is the planner's to keep.\n */\nexport function databaseOwnedDefaultColumns(collections: CollectionConfig[]): DatabaseOwnedDefaultColumn[] {\n const declared = collections.filter(collection =>\n Object.values(collection.properties ?? {}).some(prop => isDatabaseOwned(prop)));\n if (declared.length === 0) return [];\n\n const plan = planSchema(collections, { databaseExtensions: declaredDatabaseExtensions() });\n const found: DatabaseOwnedDefaultColumn[] = [];\n for (const table of plan.tables) {\n const collection = declared.find(each => each.slug === table.slug);\n if (!collection) continue;\n for (const [key, prop] of Object.entries(collection.properties ?? {})) {\n if (!isDatabaseOwned(prop)) continue;\n const name = resolveColumnName(key, prop);\n const column = table.columns.find(each => each.column === name);\n if (!column || column.default || column.generated) continue;\n found.push({ schema: table.schema, table: table.table, column: name });\n }\n }\n return found;\n}\n\nconst isDatabaseOwned = (prop: unknown): prop is Property =>\n typeof prop === \"object\" && prop !== null &&\n (prop as { databaseDefault?: unknown }).databaseDefault === true;\n\n/**\n * Every column DEFAULT outside the system schemas, and whether Atlas can carry\n * it (see the module comment for the two it cannot).\n *\n * Rendered with only `pg_catalog` on the search path, so anything outside it\n * comes back schema-qualified and reads the same in whichever database\n * `schema.sql` is loaded into.\n */\nconst LIVE_DEFAULTS_SQL = `\n SELECT n.nspname AS \"schema\",\n c.relname AS \"table\",\n a.attname AS \"column\",\n pg_get_expr(d.adbin, d.adrelid) AS \"expression\",\n EXISTS (\n SELECT 1\n FROM pg_depend dep\n WHERE dep.classid = 'pg_attrdef'::regclass\n AND dep.objid = d.oid\n AND (\n (dep.refclassid = 'pg_class'::regclass\n AND EXISTS (SELECT 1 FROM pg_class s WHERE s.oid = dep.refobjid AND s.relkind = 'S'))\n OR (dep.refclassid IN ('pg_proc'::regclass, 'pg_operator'::regclass)\n AND dep.refobjid >= 16384)\n )\n ) AS \"uncarried\"\n FROM pg_attrdef d\n JOIN pg_attribute a ON a.attrelid = d.adrelid AND a.attnum = d.adnum\n JOIN pg_class c ON c.oid = d.adrelid\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE NOT a.attisdropped\n AND a.attgenerated = ''\n AND n.nspname NOT IN ('pg_catalog', 'information_schema')\n AND n.nspname NOT LIKE 'pg\\\\_%'\n`;\n\ninterface LiveDefaultRow extends KeptDefault {\n uncarried: boolean;\n}\n\n/**\n * The live DEFAULTs of `columns` that the desired state must carry so Atlas\n * plans no `DROP DEFAULT`. A column with no DEFAULT, or one Atlas cannot carry,\n * is not listed.\n *\n * Read in a transaction of its own, so the search path it narrows goes back\n * when it ends.\n */\nexport async function readKeptDefaults(client: Queryable, columns: DatabaseOwnedDefaultColumn[]): Promise<KeptDefault[]> {\n if (columns.length === 0) return [];\n const wanted = new Set(columns.map(keyOf));\n let rows: LiveDefaultRow[];\n await client.query(\"BEGIN READ ONLY\");\n try {\n await client.query(\"SET LOCAL search_path TO pg_catalog\");\n rows = (await client.query<LiveDefaultRow>(LIVE_DEFAULTS_SQL)).rows;\n } finally {\n await client.query(\"ROLLBACK\");\n }\n return rows\n .filter(row => wanted.has(keyOf(row)) && !row.uncarried && row.expression)\n .map(({ schema, table, column, expression }) => ({ schema, table, column, expression }))\n .sort((a, b) => keyOf(a).localeCompare(keyOf(b)));\n}\n\nconst keyOf = (column: DatabaseOwnedDefaultColumn): string => `${column.schema}\\u0000${column.table}\\u0000${column.column}`;\n\nconst ident = (name: string): string => `\"${name.replace(/\"/g, \"\\\"\\\"\")}\"`;\n\n/** What is appended to `schema.sql`: one `SET DEFAULT` per kept DEFAULT. */\nexport function keptDefaultStatements(kept: KeptDefault[]): string[] {\n return kept.map(({ schema, table, column, expression }) =>\n `ALTER TABLE ${ident(schema)}.${ident(table)} ALTER COLUMN ${ident(column)} SET DEFAULT ${expression};`);\n}\n\n/** `schema.sql` with the kept DEFAULTs appended, under a comment saying why. */\nexport function withKeptDefaults(desiredSql: string, kept: KeptDefault[]): string {\n if (kept.length === 0) return desiredSql;\n return `${desiredSql.replace(/\\s*$/, \"\\n\")}\\n` +\n \"-- DEFAULTs the database holds for columns declared `databaseDefault: true`,\\n\" +\n \"-- read before planning so the push leaves them as they are.\\n\" +\n `${keptDefaultStatements(kept).join(\"\\n\")}\\n`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,SAAgB,4BAA4B,aAA+D;CACvG,MAAM,WAAW,YAAY,QAAO,eAChC,OAAO,OAAO,WAAW,cAAc,CAAC,CAAC,CAAC,CAAC,MAAK,SAAQ,gBAAgB,IAAI,CAAC,CAAC;CAClF,IAAI,SAAS,WAAW,GAAG,OAAO,CAAC;CAEnC,MAAM,OAAO,WAAW,aAAa,EAAE,oBAAoB,2BAA2B,EAAE,CAAC;CACzF,MAAM,QAAsC,CAAC;CAC7C,KAAK,MAAM,SAAS,KAAK,QAAQ;EAC7B,MAAM,aAAa,SAAS,MAAK,SAAQ,KAAK,SAAS,MAAM,IAAI;EACjE,IAAI,CAAC,YAAY;EACjB,KAAK,MAAM,CAAC,KAAK,SAAS,OAAO,QAAQ,WAAW,cAAc,CAAC,CAAC,GAAG;GACnE,IAAI,CAAC,gBAAgB,IAAI,GAAG;GAC5B,MAAM,OAAO,kBAAkB,KAAK,IAAI;GACxC,MAAM,SAAS,MAAM,QAAQ,MAAK,SAAQ,KAAK,WAAW,IAAI;GAC9D,IAAI,CAAC,UAAU,OAAO,WAAW,OAAO,WAAW;GACnD,MAAM,KAAK;IAAE,QAAQ,MAAM;IAAQ,OAAO,MAAM;IAAO,QAAQ;GAAK,CAAC;EACzE;CACJ;CACA,OAAO;AACX;AAEA,IAAM,mBAAmB,SACrB,OAAO,SAAS,YAAY,SAAS,QACpC,KAAuC,oBAAoB;;;;;;;;;AAUhE,IAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuC1B,eAAsB,iBAAiB,QAAmB,SAA+D;CACrH,IAAI,QAAQ,WAAW,GAAG,OAAO,CAAC;CAClC,MAAM,SAAS,IAAI,IAAI,QAAQ,IAAI,KAAK,CAAC;CACzC,IAAI;CACJ,MAAM,OAAO,MAAM,iBAAiB;CACpC,IAAI;EACA,MAAM,OAAO,MAAM,qCAAqC;EACxD,QAAQ,MAAM,OAAO,MAAsB,iBAAiB,EAAA,CAAG;CACnE,UAAU;EACN,MAAM,OAAO,MAAM,UAAU;CACjC;CACA,OAAO,KACF,QAAO,QAAO,OAAO,IAAI,MAAM,GAAG,CAAC,KAAK,CAAC,IAAI,aAAa,IAAI,UAAU,CAAC,CACzE,KAAK,EAAE,QAAQ,OAAO,QAAQ,kBAAkB;EAAE;EAAQ;EAAO;EAAQ;CAAW,EAAE,CAAC,CACvF,MAAM,GAAG,MAAM,MAAM,CAAC,CAAC,CAAC,cAAc,MAAM,CAAC,CAAC,CAAC;AACxD;AAEA,IAAM,SAAS,WAA+C,GAAG,OAAO,OAAO,QAAQ,OAAO,MAAM,QAAQ,OAAO;AAEnH,IAAM,SAAS,SAAyB,IAAI,KAAK,QAAQ,MAAM,MAAM,EAAE;;AAGvE,SAAgB,sBAAsB,MAA+B;CACjE,OAAO,KAAK,KAAK,EAAE,QAAQ,OAAO,QAAQ,iBACtC,eAAe,MAAM,MAAM,EAAE,GAAG,MAAM,KAAK,EAAE,gBAAgB,MAAM,MAAM,EAAE,eAAe,WAAW,EAAE;AAC/G;;AAGA,SAAgB,iBAAiB,YAAoB,MAA6B;CAC9E,IAAI,KAAK,WAAW,GAAG,OAAO;CAC9B,OAAO,GAAG,WAAW,QAAQ,QAAQ,IAAI,EAAE;;EAGpC,sBAAsB,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE;AAClD"}
@@ -1,8 +1,8 @@
1
1
  import { createRequire as __createRequire } from "module";
2
2
  __createRequire(import.meta.url);
3
- import { n as planSchema } from "./plan-schema-CXOucCw-.js";
4
- import { s as diffPlanAgainstCatalogue } from "./deferred-index-build-Cis_HoGn.js";
5
- import { i as readExistingSchema } from "./ensure-collection-tables-DvvImXDe.js";
3
+ import { n as planSchema } from "./plan-schema-Czf3q4-Q.js";
4
+ import { s as diffPlanAgainstCatalogue } from "./deferred-index-build-CqaOv1Xx.js";
5
+ import { i as readExistingSchema } from "./ensure-collection-tables-CFIRq8ni.js";
6
6
  import { declaredDatabaseExtensions } from "@rebasepro/types";
7
7
  //#region src/schema/push-legacy-renames.ts
8
8
  /**
@@ -31,4 +31,4 @@ async function legacyForeignKeyRenames(client, collections) {
31
31
  //#endregion
32
32
  export { legacyForeignKeyRenames };
33
33
 
34
- //# sourceMappingURL=push-legacy-renames-BqJut4F3.js.map
34
+ //# sourceMappingURL=push-legacy-renames-BwaKi8xF.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"push-legacy-renames-BqJut4F3.js","names":[],"sources":["../src/schema/push-legacy-renames.ts"],"sourcesContent":["/**\n * The relation columns an aged database still holds under the name Rebase\n * derived before 0.13 (`categorie_id`, `people_id`), for `db push` to rename\n * before Atlas plans.\n *\n * Boot renames them losslessly (`diff-plan.ts`, `renameLegacyColumn`). Push\n * runs Atlas, which sees only \"drop `categorie_id`, add `category_id` NOT\n * NULL\": it listed the links as destructive changes to approve, and with\n * `--allow-destructive` dropped them. A pipeline that runs only `db push`\n * could not upgrade an aged database at all. Renaming first — the same\n * metadata-only statement boot runs — leaves Atlas nothing to plan.\n */\nimport { declaredDatabaseExtensions, type CollectionConfig } from \"@rebasepro/types\";\nimport { planSchema } from \"./plan/plan-schema\";\nimport { diffPlanAgainstCatalogue, type LegacyForeignKey } from \"./plan/diff-plan\";\nimport { readExistingSchema, type Queryable } from \"./ensure-collection-tables\";\n\nexport interface LegacyRename extends LegacyForeignKey {\n /** `ALTER TABLE … RENAME COLUMN …;` */\n sql: string;\n}\n\nexport async function legacyForeignKeyRenames(client: Queryable, collections: CollectionConfig[]): Promise<LegacyRename[]> {\n const schema = planSchema(collections, { databaseExtensions: declaredDatabaseExtensions() });\n const schemas = Array.from(new Set(schema.tables.map(table => table.schema)));\n const existing = await readExistingSchema(client, schemas, { probeTables: () => [] });\n const plan = diffPlanAgainstCatalogue(schema, existing);\n return plan.legacyForeignKeys.map(legacy => {\n const action = plan.actions.find(a => a.kind === \"rename-column\" && a.target === `${legacy.table}.${legacy.expected}`);\n return { ...legacy, sql: action?.sql ?? \"\" };\n }).filter(rename => rename.sql !== \"\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAsBA,eAAsB,wBAAwB,QAAmB,aAA0D;CACvH,MAAM,SAAS,WAAW,aAAa,EAAE,oBAAoB,2BAA2B,EAAE,CAAC;CAG3F,MAAM,OAAO,yBAAyB,QAAQ,MADvB,mBAAmB,QAD1B,MAAM,KAAK,IAAI,IAAI,OAAO,OAAO,KAAI,UAAS,MAAM,MAAM,CAAC,CACzB,GAAS,EAAE,mBAAmB,CAAC,EAAE,CAAC,CAC9B;CACtD,OAAO,KAAK,kBAAkB,KAAI,WAAU;EACxC,MAAM,SAAS,KAAK,QAAQ,MAAK,MAAK,EAAE,SAAS,mBAAmB,EAAE,WAAW,GAAG,OAAO,MAAM,GAAG,OAAO,UAAU;EACrH,OAAO;GAAE,GAAG;GAAQ,KAAK,QAAQ,OAAO;EAAG;CAC/C,CAAC,CAAC,CAAC,QAAO,WAAU,OAAO,QAAQ,EAAE;AACzC"}
1
+ {"version":3,"file":"push-legacy-renames-BwaKi8xF.js","names":[],"sources":["../src/schema/push-legacy-renames.ts"],"sourcesContent":["/**\n * The relation columns an aged database still holds under the name Rebase\n * derived before 0.13 (`categorie_id`, `people_id`), for `db push` to rename\n * before Atlas plans.\n *\n * Boot renames them losslessly (`diff-plan.ts`, `renameLegacyColumn`). Push\n * runs Atlas, which sees only \"drop `categorie_id`, add `category_id` NOT\n * NULL\": it listed the links as destructive changes to approve, and with\n * `--allow-destructive` dropped them. A pipeline that runs only `db push`\n * could not upgrade an aged database at all. Renaming first — the same\n * metadata-only statement boot runs — leaves Atlas nothing to plan.\n */\nimport { declaredDatabaseExtensions, type CollectionConfig } from \"@rebasepro/types\";\nimport { planSchema } from \"./plan/plan-schema\";\nimport { diffPlanAgainstCatalogue, type LegacyForeignKey } from \"./plan/diff-plan\";\nimport { readExistingSchema, type Queryable } from \"./ensure-collection-tables\";\n\nexport interface LegacyRename extends LegacyForeignKey {\n /** `ALTER TABLE … RENAME COLUMN …;` */\n sql: string;\n}\n\nexport async function legacyForeignKeyRenames(client: Queryable, collections: CollectionConfig[]): Promise<LegacyRename[]> {\n const schema = planSchema(collections, { databaseExtensions: declaredDatabaseExtensions() });\n const schemas = Array.from(new Set(schema.tables.map(table => table.schema)));\n const existing = await readExistingSchema(client, schemas, { probeTables: () => [] });\n const plan = diffPlanAgainstCatalogue(schema, existing);\n return plan.legacyForeignKeys.map(legacy => {\n const action = plan.actions.find(a => a.kind === \"rename-column\" && a.target === `${legacy.table}.${legacy.expected}`);\n return { ...legacy, sql: action?.sql ?? \"\" };\n }).filter(rename => rename.sql !== \"\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAsBA,eAAsB,wBAAwB,QAAmB,aAA0D;CACvH,MAAM,SAAS,WAAW,aAAa,EAAE,oBAAoB,2BAA2B,EAAE,CAAC;CAG3F,MAAM,OAAO,yBAAyB,QAAQ,MADvB,mBAAmB,QAD1B,MAAM,KAAK,IAAI,IAAI,OAAO,OAAO,KAAI,UAAS,MAAM,MAAM,CAAC,CACzB,GAAS,EAAE,mBAAmB,CAAC,EAAE,CAAC,CAC9B;CACtD,OAAO,KAAK,kBAAkB,KAAI,WAAU;EACxC,MAAM,SAAS,KAAK,QAAQ,MAAK,MAAK,EAAE,SAAS,mBAAmB,EAAE,WAAW,GAAG,OAAO,MAAM,GAAG,OAAO,UAAU;EACrH,OAAO;GAAE,GAAG;GAAQ,KAAK,QAAQ,OAAO;EAAG;CAC/C,CAAC,CAAC,CAAC,QAAO,WAAU,OAAO,QAAQ,EAAE;AACzC"}
@@ -1,8 +1,8 @@
1
1
  import { createRequire as __createRequire } from "module";
2
2
  __createRequire(import.meta.url);
3
- import { n as generatePostgresPoliciesDdl } from "./generate-postgres-ddl-logic-4eUP4t8w.js";
4
- import { a as parseExpectedPolicies, o as partitionOrphans, t as checkPolicyDrift } from "./policy-drift-vm0BcvcE.js";
5
- import { probeRenderedPolicies, readPolicyStamps, tablesRlsWouldEnable } from "./ensure-collection-policies-CCqayoUz.js";
3
+ import { n as generatePostgresPoliciesDdl } from "./generate-postgres-ddl-logic-DPl1oH_z.js";
4
+ import { a as parseExpectedPolicies, o as partitionOrphans, t as checkPolicyDrift } from "./policy-drift-X4A-xAzv.js";
5
+ import { probeRenderedPolicies, readPolicyStamps, tablesRlsWouldEnable } from "./ensure-collection-policies-DLWgabQ2.js";
6
6
  //#region src/schema/push-policy-plan.ts
7
7
  var keyOf = (p) => `${p.schema}.${p.table}.${p.name}`;
8
8
  /** What a stamp that no longer matches says. */
@@ -122,6 +122,6 @@ async function underSearchPath(client, searchPath, read) {
122
122
  }
123
123
  }
124
124
  //#endregion
125
- export { formatPolicyPushPlan, planPolicyPush, summarizePolicyPushPlan };
125
+ export { formatPolicyPushPlan, planPolicyPush, summarizePolicyPushPlan, underSearchPath };
126
126
 
127
- //# sourceMappingURL=push-policy-plan-CvX8hiPb.js.map
127
+ //# sourceMappingURL=push-policy-plan-DQ2tWceD.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"push-policy-plan-CvX8hiPb.js","names":[],"sources":["../src/schema/push-policy-plan.ts"],"sourcesContent":["/**\n * What `rebase db push` will do to row-level security, read before it does it.\n *\n * Atlas has no model for a policy: its free tier neither plans nor reads one.\n * So its plan, which was everything `db push --dry-run` showed, never named\n * one. After `schema introspect` on a database whose tables had policies of\n * their own and no admin baseline, the dry run said \"No changes: the database\n * already matches these collections\", and the push then created eight\n * `<table>_default_admin_*` policies that let an admin read and write every\n * row.\n *\n * The plan is the push's own steps, read instead of run:\n *\n * - **RLS switched on:** the tables `lockCollectionTables` switches on\n * ({@link tablesRlsWouldEnable}).\n * - **Policies created and replaced:** `policies.sql`, which DROPs and CREATEs\n * every declared policy, against the database as `checkPolicyDrift` reads\n * it: by name, `TO` list, command, mode and which clauses there are. Text\n * is compared through the stamp the push and every boot write\n * ({@link readPolicyStamps}), which is what catches a rule whose SQL changed\n * under the same name. Where the stamp does not match, or there is none (a\n * policy `db migrate` or a hand made), the policy is made again under a probe\n * name and rolled back ({@link probeRenderedPolicies}): one Postgres would\n * store exactly as it is now is no change. Only one that could not be probed\n * is listed as unstamped.\n * - **Policies dropped and kept:** {@link partitionOrphans}, which is how the\n * push's reconcile step decides.\n */\nimport type { CollectionConfig } from \"@rebasepro/types\";\nimport {\n checkPolicyDrift,\n parseExpectedPolicies,\n partitionOrphans,\n type PolicyRef,\n type Queryable\n} from \"../security/policy-drift\";\nimport { generatePostgresPoliciesDdl } from \"./generate-postgres-ddl-logic\";\nimport { probeRenderedPolicies, readPolicyStamps, tablesRlsWouldEnable } from \"./ensure-collection-policies\";\n\nexport interface PolicyPushPlan {\n /** Declared tables in the database with row-level security off, as `schema.table`: the push switches it on. */\n enableRls: string[];\n /** Declared policies the database does not have: the push creates them. */\n create: PolicyRef[];\n /** Declared policies the database has under the same name, defined otherwise: the push replaces them. */\n replace: { policy: PolicyRef; why: string[] }[];\n /** Policies an earlier push generated from a rule edited since: the push drops them. */\n drop: PolicyRef[];\n /** Policies no collection describes, which the push leaves in force. */\n keep: PolicyRef[];\n /** Declared policies the database has with no stamp of Rebase's: re-created from their rules, changed or not. */\n unstamped: PolicyRef[];\n}\n\nconst keyOf = (p: PolicyRef): string => `${p.schema}.${p.table}.${p.name}`;\n\n/** What a stamp that no longer matches says. */\nconst STALE = \"its rule, or the policy itself, has changed since Rebase wrote it\";\n\n/** What a policy with no stamp, whose rule stores something else, says. */\nconst UNSTAMPED_DIFFERS = \"it carries no stamp of Rebase's, and its rule defines it otherwise\";\n\n/**\n * Read the plan.\n *\n * @param options.searchPath the `search_path` the stamps were written under\n * when the connection's own differs — the one `db push` stamps under.\n * See {@link readPolicyStamps}.\n */\nexport async function planPolicyPush(\n client: Queryable,\n collections: CollectionConfig[],\n options: { searchPath?: string } = {}\n): Promise<PolicyPushPlan> {\n const drift = await checkPolicyDrift(client, collections);\n const { dropped, kept } = partitionOrphans(drift, collections);\n const enableRls = await tablesRlsWouldEnable(client, collections);\n const stamps = await underSearchPath(client, options.searchPath, () => readPolicyStamps(client, collections));\n\n const replace = drift.diverged.map(d => ({ policy: d.expected, why: d.differences }));\n const settled = new Set([...drift.missing, ...drift.diverged.map(d => d.expected)].map(keyOf));\n const unsure: { policy: PolicyRef; state: \"stale\" | \"unstamped\" }[] = [];\n for (const policy of parseExpectedPolicies(generatePostgresPoliciesDdl(collections))) {\n if (settled.has(keyOf(policy))) continue;\n const state = stamps.get(`${policy.schema}.${policy.table}`)?.get(policy.name);\n if (state === \"stale\" || state === \"unstamped\") unsure.push({ policy, state });\n }\n // What the stamp cannot say, asked of Postgres: would the push store the\n // definition that is there? A policy it would re-create exactly is no change.\n const rendersAsStored = unsure.length === 0\n ? new Map<string, boolean>()\n : await probeRenderedPolicies(client, collections, unsure.map(entry => entry.policy), { searchPath: options.searchPath });\n const unstamped: PolicyRef[] = [];\n for (const { policy, state } of unsure) {\n const same = rendersAsStored.get(keyOf(policy));\n if (same === true) continue;\n if (state === \"stale\") replace.push({ policy, why: [STALE] });\n else if (same === false) replace.push({ policy, why: [UNSTAMPED_DIFFERS] });\n else unstamped.push(policy);\n }\n\n return { enableRls, create: drift.missing, replace, drop: dropped, keep: kept, unstamped };\n}\n\n/** Whether the push would change any row-level security. Kept and unstamped policies are not changes. */\nexport function policyPlanChanges(plan: PolicyPushPlan): boolean {\n return plan.enableRls.length + plan.create.length + plan.replace.length + plan.drop.length > 0;\n}\n\nconst describe = (p: PolicyRef): string => `${p.schema}.${p.table} → \"${p.name}\" (${p.command} TO ${p.roles.join(\", \")})`;\n\n/**\n * The plan as `db push --dry-run` prints it: `changes`, what the push does,\n * and `notes`, what it leaves or cannot tell. Both empty when there is\n * nothing to say.\n */\nexport function formatPolicyPushPlan(plan: PolicyPushPlan): { changes: string[]; notes: string[] } {\n const changes: string[] = [];\n if (policyPlanChanges(plan)) {\n changes.push(\" Planned row-level security changes:\");\n for (const table of plan.enableRls) changes.push(` switch RLS on ${table}`);\n for (const p of plan.create) changes.push(` create policy ${describe(p)}`);\n for (const { policy, why } of plan.replace) changes.push(` replace policy ${describe(policy)}: ${why.join(\"; \")}`);\n for (const p of plan.drop) changes.push(` drop policy ${describe(p)}, left by a rule edited since`);\n }\n\n const notes: string[] = [];\n if (plan.unstamped.length > 0) {\n notes.push(` ${plan.unstamped.length} declared ${plan.unstamped.length === 1 ? \"policy carries\" : \"policies carry\"} ` +\n \"no stamp of Rebase's, so the push re-creates each from its rule\");\n notes.push(\" and cannot tell beforehand whether that changes it:\");\n for (const p of plan.unstamped) notes.push(` ${describe(p)}`);\n }\n if (plan.keep.length > 0) {\n notes.push(\" Policies in the database that no collection describes, which the push leaves in force:\");\n for (const p of plan.keep) notes.push(` ${describe(p)}`);\n }\n return { changes, notes };\n}\n\n/** One line for the push itself: the counts, or `null` when it changes no row-level security. */\nexport function summarizePolicyPushPlan(plan: PolicyPushPlan): string | null {\n if (!policyPlanChanges(plan)) return null;\n const count = (n: number, one: string, many: string): string => `${n} ${n === 1 ? one : many}`;\n const parts = [\n plan.create.length > 0 ? count(plan.create.length, \"policy to create\", \"policies to create\") : \"\",\n plan.replace.length > 0 ? `${plan.replace.length} to replace` : \"\",\n plan.drop.length > 0 ? `${plan.drop.length} to drop` : \"\",\n plan.enableRls.length > 0 ? `RLS to switch on for ${count(plan.enableRls.length, \"table\", \"tables\")}` : \"\"\n ].filter(Boolean);\n return ` Row-level security: ${parts.join(\", \")}. \\`rebase db push --dry-run\\` lists them.`;\n}\n\n/**\n * Run `read` under `searchPath`, set for one transaction and gone after it, or\n * as the connection is when there is none to set.\n */\nasync function underSearchPath<T>(client: Queryable, searchPath: string | undefined, read: () => Promise<T>): Promise<T> {\n if (!searchPath) return read();\n await client.query(\"BEGIN\");\n try {\n await client.query(`SET LOCAL search_path TO ${searchPath}`);\n const result = await read();\n await client.query(\"COMMIT\");\n return result;\n } catch (err) {\n await client.query(\"ROLLBACK\").catch(() => undefined);\n throw err;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"push-policy-plan-DQ2tWceD.js","names":[],"sources":["../src/schema/push-policy-plan.ts"],"sourcesContent":["/**\n * What `rebase db push` will do to row-level security, read before it does it.\n *\n * Atlas has no model for a policy: its free tier neither plans nor reads one.\n * So its plan, which was everything `db push --dry-run` showed, never named\n * one. After `schema introspect` on a database whose tables had policies of\n * their own and no admin baseline, the dry run said \"No changes: the database\n * already matches these collections\", and the push then created eight\n * `<table>_default_admin_*` policies that let an admin read and write every\n * row.\n *\n * The plan is the push's own steps, read instead of run:\n *\n * - **RLS switched on:** the tables `lockCollectionTables` switches on\n * ({@link tablesRlsWouldEnable}).\n * - **Policies created and replaced:** `policies.sql`, which DROPs and CREATEs\n * every declared policy, against the database as `checkPolicyDrift` reads\n * it: by name, `TO` list, command, mode and which clauses there are. Text\n * is compared through the stamp the push and every boot write\n * ({@link readPolicyStamps}), which is what catches a rule whose SQL changed\n * under the same name. Where the stamp does not match, or there is none (a\n * policy `db migrate` or a hand made), the policy is made again under a probe\n * name and rolled back ({@link probeRenderedPolicies}): one Postgres would\n * store exactly as it is now is no change. Only one that could not be probed\n * is listed as unstamped.\n * - **Policies dropped and kept:** {@link partitionOrphans}, which is how the\n * push's reconcile step decides.\n */\nimport type { CollectionConfig } from \"@rebasepro/types\";\nimport {\n checkPolicyDrift,\n parseExpectedPolicies,\n partitionOrphans,\n type PolicyRef,\n type Queryable\n} from \"../security/policy-drift\";\nimport { generatePostgresPoliciesDdl } from \"./generate-postgres-ddl-logic\";\nimport { probeRenderedPolicies, readPolicyStamps, tablesRlsWouldEnable } from \"./ensure-collection-policies\";\n\nexport interface PolicyPushPlan {\n /** Declared tables in the database with row-level security off, as `schema.table`: the push switches it on. */\n enableRls: string[];\n /** Declared policies the database does not have: the push creates them. */\n create: PolicyRef[];\n /** Declared policies the database has under the same name, defined otherwise: the push replaces them. */\n replace: { policy: PolicyRef; why: string[] }[];\n /** Policies an earlier push generated from a rule edited since: the push drops them. */\n drop: PolicyRef[];\n /** Policies no collection describes, which the push leaves in force. */\n keep: PolicyRef[];\n /** Declared policies the database has with no stamp of Rebase's: re-created from their rules, changed or not. */\n unstamped: PolicyRef[];\n}\n\nconst keyOf = (p: PolicyRef): string => `${p.schema}.${p.table}.${p.name}`;\n\n/** What a stamp that no longer matches says. */\nconst STALE = \"its rule, or the policy itself, has changed since Rebase wrote it\";\n\n/** What a policy with no stamp, whose rule stores something else, says. */\nconst UNSTAMPED_DIFFERS = \"it carries no stamp of Rebase's, and its rule defines it otherwise\";\n\n/**\n * Read the plan.\n *\n * @param options.searchPath the `search_path` the stamps were written under\n * when the connection's own differs — the one `db push` stamps under.\n * See {@link readPolicyStamps}.\n */\nexport async function planPolicyPush(\n client: Queryable,\n collections: CollectionConfig[],\n options: { searchPath?: string } = {}\n): Promise<PolicyPushPlan> {\n const drift = await checkPolicyDrift(client, collections);\n const { dropped, kept } = partitionOrphans(drift, collections);\n const enableRls = await tablesRlsWouldEnable(client, collections);\n const stamps = await underSearchPath(client, options.searchPath, () => readPolicyStamps(client, collections));\n\n const replace = drift.diverged.map(d => ({ policy: d.expected, why: d.differences }));\n const settled = new Set([...drift.missing, ...drift.diverged.map(d => d.expected)].map(keyOf));\n const unsure: { policy: PolicyRef; state: \"stale\" | \"unstamped\" }[] = [];\n for (const policy of parseExpectedPolicies(generatePostgresPoliciesDdl(collections))) {\n if (settled.has(keyOf(policy))) continue;\n const state = stamps.get(`${policy.schema}.${policy.table}`)?.get(policy.name);\n if (state === \"stale\" || state === \"unstamped\") unsure.push({ policy, state });\n }\n // What the stamp cannot say, asked of Postgres: would the push store the\n // definition that is there? A policy it would re-create exactly is no change.\n const rendersAsStored = unsure.length === 0\n ? new Map<string, boolean>()\n : await probeRenderedPolicies(client, collections, unsure.map(entry => entry.policy), { searchPath: options.searchPath });\n const unstamped: PolicyRef[] = [];\n for (const { policy, state } of unsure) {\n const same = rendersAsStored.get(keyOf(policy));\n if (same === true) continue;\n if (state === \"stale\") replace.push({ policy, why: [STALE] });\n else if (same === false) replace.push({ policy, why: [UNSTAMPED_DIFFERS] });\n else unstamped.push(policy);\n }\n\n return { enableRls, create: drift.missing, replace, drop: dropped, keep: kept, unstamped };\n}\n\n/** Whether the push would change any row-level security. Kept and unstamped policies are not changes. */\nexport function policyPlanChanges(plan: PolicyPushPlan): boolean {\n return plan.enableRls.length + plan.create.length + plan.replace.length + plan.drop.length > 0;\n}\n\nconst describe = (p: PolicyRef): string => `${p.schema}.${p.table} → \"${p.name}\" (${p.command} TO ${p.roles.join(\", \")})`;\n\n/**\n * The plan as `db push --dry-run` prints it: `changes`, what the push does,\n * and `notes`, what it leaves or cannot tell. Both empty when there is\n * nothing to say.\n */\nexport function formatPolicyPushPlan(plan: PolicyPushPlan): { changes: string[]; notes: string[] } {\n const changes: string[] = [];\n if (policyPlanChanges(plan)) {\n changes.push(\" Planned row-level security changes:\");\n for (const table of plan.enableRls) changes.push(` switch RLS on ${table}`);\n for (const p of plan.create) changes.push(` create policy ${describe(p)}`);\n for (const { policy, why } of plan.replace) changes.push(` replace policy ${describe(policy)}: ${why.join(\"; \")}`);\n for (const p of plan.drop) changes.push(` drop policy ${describe(p)}, left by a rule edited since`);\n }\n\n const notes: string[] = [];\n if (plan.unstamped.length > 0) {\n notes.push(` ${plan.unstamped.length} declared ${plan.unstamped.length === 1 ? \"policy carries\" : \"policies carry\"} ` +\n \"no stamp of Rebase's, so the push re-creates each from its rule\");\n notes.push(\" and cannot tell beforehand whether that changes it:\");\n for (const p of plan.unstamped) notes.push(` ${describe(p)}`);\n }\n if (plan.keep.length > 0) {\n notes.push(\" Policies in the database that no collection describes, which the push leaves in force:\");\n for (const p of plan.keep) notes.push(` ${describe(p)}`);\n }\n return { changes, notes };\n}\n\n/** One line for the push itself: the counts, or `null` when it changes no row-level security. */\nexport function summarizePolicyPushPlan(plan: PolicyPushPlan): string | null {\n if (!policyPlanChanges(plan)) return null;\n const count = (n: number, one: string, many: string): string => `${n} ${n === 1 ? one : many}`;\n const parts = [\n plan.create.length > 0 ? count(plan.create.length, \"policy to create\", \"policies to create\") : \"\",\n plan.replace.length > 0 ? `${plan.replace.length} to replace` : \"\",\n plan.drop.length > 0 ? `${plan.drop.length} to drop` : \"\",\n plan.enableRls.length > 0 ? `RLS to switch on for ${count(plan.enableRls.length, \"table\", \"tables\")}` : \"\"\n ].filter(Boolean);\n return ` Row-level security: ${parts.join(\", \")}. \\`rebase db push --dry-run\\` lists them.`;\n}\n\n/**\n * Run `read` under `searchPath`, set for one transaction and gone after it, or\n * as the connection is when there is none to set.\n */\nexport async function underSearchPath<T>(client: Queryable, searchPath: string | undefined, read: () => Promise<T>): Promise<T> {\n if (!searchPath) return read();\n await client.query(\"BEGIN\");\n try {\n await client.query(`SET LOCAL search_path TO ${searchPath}`);\n const result = await read();\n await client.query(\"COMMIT\");\n return result;\n } catch (err) {\n await client.query(\"ROLLBACK\").catch(() => undefined);\n throw err;\n }\n}\n"],"mappings":"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"}
@@ -1,9 +1,9 @@
1
1
  import { createRequire as __createRequire } from "module";
2
2
  __createRequire(import.meta.url);
3
3
  import { c as searchColumnDefinition, f as searchExtensionStatements, g as searchStampGuards, h as searchIndexStatements, m as searchIndexNames, o as fuzzyColumnDefinition, p as searchHelperFunctions, u as searchColumnStamps } from "./search-column-BM-GV6vH.js";
4
- import { a as vectorDimensionGuard, c as vectorExtensionStatement, d as vectorIndexStatements, i as vectorColumnDefinition, l as vectorIndexNames, o as vectorExtensionDeclared, t as VECTOR_EXTENSION_OPT_IN } from "./vector-index-BFhyEaiz.js";
5
- import { o as atlasExcludePattern, u as collectionIndexStatements } from "./source-Bv5KPeIx.js";
6
- import { a as triggerStatements, r as quoteSqlLiteral } from "./plan-schema-CXOucCw-.js";
4
+ import { a as vectorDimensionGuard, c as vectorExtensionStatement, d as vectorIndexStatements, i as vectorColumnDefinition, l as vectorIndexNames, o as vectorExtensionDeclared, t as VECTOR_EXTENSION_OPT_IN } from "./vector-index-DsQ3V2d2.js";
5
+ import { o as atlasExcludePattern, u as collectionIndexStatements } from "./source-fZb92pN8.js";
6
+ import { a as triggerStatements, r as quoteSqlLiteral } from "./plan-schema-Czf3q4-Q.js";
7
7
  //#region src/schema/plan/render-ddl.ts
8
8
  /**
9
9
  * The SQL files, rendered from a {@link SchemaPlan}.
@@ -339,4 +339,4 @@ var triggerExcludePatternsOf = (plan) => plan.tables.flatMap((table) => table.tr
339
339
  //#endregion
340
340
  export { renderPolicyStatements as a, renderSearchDdl as c, searchExcludePatternsOf as d, triggerExcludePatternsOf as f, renderPoliciesDdl as i, renderTriggersDdl as l, renderEnableRls as n, renderPostgresDdl as o, vectorExcludePatternsOf as p, renderPgType as r, renderPrimaryKeyConstraint as s, renderColumnDefinition as t, renderVectorDdl as u };
341
341
 
342
- //# sourceMappingURL=render-ddl-CQNwZdse.js.map
342
+ //# sourceMappingURL=render-ddl-4v88y-Gv.js.map