@rebasepro/server-postgres 0.25.1 → 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.
- package/dist/{BranchService-ClklxyAQ.js → BranchService-CWBnDhcv.js} +25 -9
- package/dist/BranchService-CWBnDhcv.js.map +1 -0
- package/dist/PostgresBackendDriver.d.ts +13 -0
- package/dist/PostgresBootstrapper.d.ts +7 -0
- package/dist/auth/services.d.ts +8 -1
- package/dist/auth/verify-auth-tables.d.ts +21 -0
- package/dist/{backup-cli-gijeXe0u.js → backup-cli-PoRG-zEq.js} +3 -3
- package/dist/{backup-cli-gijeXe0u.js.map → backup-cli-PoRG-zEq.js.map} +1 -1
- package/dist/{boot-ddl-DYH95Qeg.js → boot-ddl-EsgqDkx0.js} +2 -2
- package/dist/{boot-ddl-DYH95Qeg.js.map → boot-ddl-EsgqDkx0.js.map} +1 -1
- package/dist/branch-diff-CxE4VlxT.js +1396 -0
- package/dist/branch-diff-CxE4VlxT.js.map +1 -0
- package/dist/branch-diff-report-DpvVf7hR.js +48 -0
- package/dist/branch-diff-report-DpvVf7hR.js.map +1 -0
- package/dist/branching/branch-diff-report.d.ts +7 -0
- package/dist/branching/branch-diff.d.ts +115 -0
- package/dist/{cli-console-CQrQ0lAY.js → cli-console-f0k6_TsJ.js} +38 -5
- package/dist/cli-console-f0k6_TsJ.js.map +1 -0
- package/dist/cli-console.d.ts +17 -1
- package/dist/{cli-errors-DiA0XFIS.js → cli-errors-DsYyNYzd.js} +53 -2
- package/dist/cli-errors-DsYyNYzd.js.map +1 -0
- package/dist/cli.js +102 -122
- package/dist/cli.js.map +1 -1
- package/dist/{collections-schema-version-T6cGvrDi.js → collections-schema-version-D7bDI0PJ.js} +13 -9
- package/dist/collections-schema-version-D7bDI0PJ.js.map +1 -0
- package/dist/{deferred-index-build-BE0I6sYU.js → deferred-index-build-CqaOv1Xx.js} +3 -3
- package/dist/{deferred-index-build-BE0I6sYU.js.map → deferred-index-build-CqaOv1Xx.js.map} +1 -1
- package/dist/{doctor-BT--1kq3.js → doctor-DW_mlTzz.js} +81 -33
- package/dist/doctor-DW_mlTzz.js.map +1 -0
- package/dist/{ensure-collection-policies-BnGOURpG.js → ensure-collection-policies-DLWgabQ2.js} +5 -5
- package/dist/{ensure-collection-policies-BnGOURpG.js.map → ensure-collection-policies-DLWgabQ2.js.map} +1 -1
- package/dist/{ensure-collection-tables-Cet4650m.js → ensure-collection-tables-CFIRq8ni.js} +5 -5
- package/dist/{ensure-collection-tables-Cet4650m.js.map → ensure-collection-tables-CFIRq8ni.js.map} +1 -1
- package/dist/{ensure-tables-l_OgFQjN.js → ensure-tables-4nA-tfwi.js} +4 -4
- package/dist/{ensure-tables-l_OgFQjN.js.map → ensure-tables-4nA-tfwi.js.map} +1 -1
- package/dist/{enum-labels-kept-nheku1pB.js → enum-labels-kept-PcPW65xy.js} +2 -2
- package/dist/{enum-labels-kept-nheku1pB.js.map → enum-labels-kept-PcPW65xy.js.map} +1 -1
- package/dist/history/HistoryService.d.ts +7 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.es.js +226 -8
- package/dist/index.es.js.map +1 -1
- package/dist/{db-subcommands-BPoHCxJR.js → migrate-baseline-B-2Ef4Mv.js} +266 -41
- package/dist/migrate-baseline-B-2Ef4Mv.js.map +1 -0
- package/dist/{migrate-policy-stamps-3eqfhes6.js → migrate-policy-stamps-BQtFHApx.js} +5 -5
- package/dist/{migrate-policy-stamps-3eqfhes6.js.map → migrate-policy-stamps-BQtFHApx.js.map} +1 -1
- package/dist/{open-rebase-EmqVmS14.js → open-rebase-C8jQ6s5a.js} +292 -46
- package/dist/open-rebase-C8jQ6s5a.js.map +1 -0
- package/dist/open-rebase.d.ts +9 -1
- package/dist/{pg-error-utils-CFEnm2m9.js → pg-error-utils-DhiUTlVx.js} +2 -2
- package/dist/{pg-error-utils-CFEnm2m9.js.map → pg-error-utils-DhiUTlVx.js.map} +1 -1
- package/dist/{push-index-prebuild-COd7i6wm.js → push-index-prebuild-UTvB5pqH.js} +3 -3
- package/dist/{push-index-prebuild-COd7i6wm.js.map → push-index-prebuild-UTvB5pqH.js.map} +1 -1
- package/dist/push-kept-defaults-BGOlOqRQ.js +137 -0
- package/dist/push-kept-defaults-BGOlOqRQ.js.map +1 -0
- package/dist/{push-legacy-renames-CLgYYwPE.js → push-legacy-renames-BwaKi8xF.js} +3 -3
- package/dist/{push-legacy-renames-CLgYYwPE.js.map → push-legacy-renames-BwaKi8xF.js.map} +1 -1
- package/dist/{push-policy-plan-D2gpQsdv.js → push-policy-plan-DQ2tWceD.js} +2 -2
- package/dist/{push-policy-plan-D2gpQsdv.js.map → push-policy-plan-DQ2tWceD.js.map} +1 -1
- package/dist/{rls-enforcement-DDyNoOSy.js → rls-enforcement-BOHuHVlc.js} +42 -3
- package/dist/rls-enforcement-BOHuHVlc.js.map +1 -0
- package/dist/schema/destructive-sql.d.ts +16 -0
- package/dist/schema/doctor-cli.js +3 -3
- package/dist/schema/introspect-db.js +1 -1
- package/dist/schema/migration-freshness.d.ts +69 -0
- package/dist/schema/push-kept-defaults.d.ts +57 -0
- package/dist/schema/rls-bootstrap-sql.d.ts +47 -10
- package/dist/security/rls-enforcement.d.ts +18 -0
- package/dist/services/FetchService.d.ts +12 -0
- package/dist/services/channel-presence.d.ts +3 -1
- package/dist/services/nested-path.d.ts +16 -0
- package/dist/services/realtimeService.d.ts +18 -1
- package/dist/{websocket-BKZwsjmf.js → websocket-CKYc3V4N.js} +41 -12
- package/dist/websocket-CKYc3V4N.js.map +1 -0
- package/package.json +7 -7
- package/dist/BranchService-ClklxyAQ.js.map +0 -1
- package/dist/cli-console-CQrQ0lAY.js.map +0 -1
- package/dist/cli-errors-DiA0XFIS.js.map +0 -1
- package/dist/collections-schema-version-T6cGvrDi.js.map +0 -1
- package/dist/db-subcommands-BPoHCxJR.js.map +0 -1
- package/dist/doctor-BT--1kq3.js.map +0 -1
- package/dist/open-rebase-EmqVmS14.js.map +0 -1
- package/dist/rls-enforcement-DDyNoOSy.js.map +0 -1
- package/dist/websocket-BKZwsjmf.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ensure-collection-policies-BnGOURpG.js","names":[],"sources":["../src/schema/ensure-collection-policies.ts"],"sourcesContent":["/**\n * Applying a bundle's RLS policies to a database at boot, idempotently.\n *\n * ## Why this exists\n *\n * {@link ensureCollectionTables} creates the collection *tables* a managed\n * runtime boots against, but a table with row-level security disabled and no\n * policies is not servable: authenticated requests run as the restricted\n * `rebase_user` role, so a read with no `SELECT` policy returns nothing (a\n * public collection answered 401) and a write with no `INSERT`/`UPDATE` policy\n * is denied. The policies live in the collections' `securityRules`; nothing at\n * boot applied them. `rebase db push` does — but it drives Atlas against a\n * local `DATABASE_URL`, and a managed tenant's database is reachable only from\n * inside the cluster, by the runtime that is already connected to it. So the\n * runtime is the only thing that *can* apply them, and this is where it does.\n *\n * ## Why this is safe to run on every boot\n *\n * Every statement is idempotent: `ENABLE ROW LEVEL SECURITY` is only issued for\n * a table the catalogue reports RLS-off (on one that has it, it changes nothing\n * and still queues for an ACCESS EXCLUSIVE lock), and each policy is a `DROP\n * POLICY IF EXISTS` immediately followed by a `CREATE POLICY`, so re-applying\n * asserts exactly the declared state. It adds\n * and replaces; it never drops data. (It does not *reconcile* — a policy a\n * previous push left behind under an old name is not removed here; that stays a\n * `db push` / `db migrate` concern, alongside destructive schema changes.)\n *\n * ## A table whose policies are already the declared ones is left alone\n *\n * Every policy this module writes carries a stamp, `COMMENT ON POLICY … IS\n * 'rebase:<hash>'` (see {@link policyStampSql}). The hash covers the\n * `CREATE POLICY` statement that was rendered *and* the definition Postgres\n * stored for it — command, mode, roles, and the deparsed `USING` / `WITH\n * CHECK` — as read back in the same transaction. A boot recomputes it from the\n * statement it would run now and the definition the catalogue holds now, and\n * re-applies a table only when that differs for any of its policies, a declared\n * policy is missing, a superseded one is still there, or RLS is off.\n *\n * So both halves of \"the declared state\" still win: a rule whose compiled SQL\n * changed (a new rule under the same name, a compiler change) misses on the\n * first half, and a policy someone altered by hand — `ALTER POLICY`, a\n * different role list — misses on the second, and either is re-applied exactly\n * as every boot used to re-apply everything. A boot that changes nothing runs\n * two catalogue reads here, no DDL and takes no table lock. It used to drop and\n * re-create every policy, one ACCESS EXCLUSIVE lock per table: about 1 s at 200\n * collections, and under a `pg_dump` up to the whole retry budget.\n *\n * A policy without a stamp — written by `db push`, by hand, or by a release\n * that predates stamps — counts as different, so the first boot after either\n * re-applies its table once and stamps it. Nothing else reads the comment:\n * drift checks, `doctor`, rls-check and Studio compare names, roles, commands\n * and clauses, never descriptions.\n *\n * Unlike table creation, a failure here is not fatal: RLS stays enabled, so a\n * table whose policies could not be applied fails **closed** (denies) rather\n * than leaking rows. One collection's policy failing (e.g. a rule that\n * references a table a real migration has not created yet) must not crash-loop\n * the whole deployment and take the other collections' working routes down with\n * it. Failures are reported loudly and per-table so the operator can see\n * exactly which collection is not yet servable and why.\n */\nimport { createHash } from \"node:crypto\";\nimport { type CollectionConfig } from \"@rebasepro/types\";\nimport { planCollectionPolicies, type CollectionPolicyPlan } from \"./generate-postgres-ddl-logic\";\nimport { isGeneratedPolicyName } from \"../security/policy-drift\";\nimport { describeDriverError, type Queryable } from \"./ensure-collection-tables\";\nimport { assertSafeIdentifier } from \"./plan/diff-plan\";\nimport { quoteSqlLiteral } from \"./generate-postgres-ddl-logic\";\nimport { revokeUserRoleSql } from \"../security/rls-enforcement\";\nimport {\n atomicBatch,\n isLockTimeout,\n isMultiStatementRefusal,\n resolveBootLockOptions,\n sleep,\n type BootLockOptions,\n type ResolvedBootLockOptions\n} from \"./boot-ddl\";\n\nexport interface PolicyEnsureResult {\n /** `CREATE POLICY` statements that ran successfully. */\n policiesApplied: number;\n /** Tables that had RLS enabled — including the {@link unchanged} ones, which already had it. */\n tablesSecured: number;\n /**\n * Tables whose policies were already exactly the declared ones, RLS on, by\n * the stamp each policy carries — left alone: no statement, no lock.\n */\n unchanged: number;\n /** Declared tables absent from the database — left to a real migration. */\n skipped: { table: string; reason: string }[];\n /**\n * Tables that have RLS on but did not get every policy. They deny — RLS\n * with no matching policy is deny-all — so they are safe but not servable.\n */\n failures: { table: string; error: string }[];\n /**\n * Tables RLS could not be enabled on, whose DML grant was withdrawn instead.\n *\n * This state had no name, and that was the bug: `ENABLE ROW LEVEL SECURITY`\n * failing was recorded as a `failure` and reported with the same \"it stays\n * locked (denies)\" wording as a failed policy — but the two are opposites.\n * A policy statement failing leaves RLS on and the table denying. `enableRls`\n * failing leaves RLS *off*, and the schema-wide grant to the user role has\n * already been made by `ensureRlsEnforcement`, so the table is readable and\n * writable by every authenticated request with no row filtering at all.\n */\n unsecured: { table: string; error: string; grantWithdrawn: boolean }[];\n /**\n * Generated policies removed because no current rule produces them.\n *\n * A policy's name embeds a hash of the rule's semantics, so editing a rule\n * does not update a policy — it creates a new one and abandons the old.\n * Postgres ORs permissive policies, so the abandoned one keeps granting:\n * a `USING (true)` tightened to an owner check went on admitting everyone,\n * forever, while the deploy logged success.\n *\n * `db push` reconciles this, and cannot reach a managed tenant's in-cluster\n * database — which is the reason this module exists. So boot has to do it.\n */\n orphansDropped: number;\n /**\n * Tables whose policies were not re-applied because another session held\n * the table's lock for every attempt — a long transaction, a report, a\n * dump. Each table's policy statements run in one transaction, so a table\n * here was left exactly as it was: RLS on, its existing policies in force.\n * `rulesChanged` says whether those differ from the declared ones by name\n * (a rule whose policy is missing, or an orphan still granting), in which\n * case the new rules are not in force on this table until a later boot.\n */\n deferred: { table: string; error: string; rulesChanged: boolean }[];\n /**\n * Tables whose collection declares no rule of its own and which still held\n * generated rule policies, all of them dropped: the generated policies, by\n * name, the boot is to warn about.\n *\n * An edited rule supersedes its old policy, and that drop is the point of\n * {@link PolicyEnsureResult.orphansDropped}. A collection with no rule at\n * all is something else: what a collection looks like when the\n * `defaultSecurityRules` its directory's index declares never reached it —\n * an array handed to a backend without them. Dropping is still right (the\n * declared state wins, and the table is left admin-only rather than open),\n * but it changes who can read the table on every server sharing the\n * database, so it is not done in silence.\n */\n rulesDropped: { table: string; policies: string[] }[];\n}\n\nconst isCreatePolicy = (statement: string): boolean => /^\\s*CREATE POLICY/i.test(statement);\n\n/** The policy names a plan creates. */\nconst plannedPolicyNames = (plan: CollectionPolicyPlan): Set<string> => {\n const expected = new Set<string>();\n for (const statement of plan.policyStatements) {\n const match = /^\\s*CREATE POLICY\\s+\"([^\"]+)\"/i.exec(statement);\n if (match) expected.add(match[1]);\n }\n return expected;\n};\n\n/**\n * Generated policies on this table that the current plan does not produce.\n *\n * Scoped hard, because dropping a policy is destructive: only this one table,\n * only names matching the generated `<table>_<op>_<sha1>` shape, and only names\n * absent from the plan. A hand-written policy, or one belonging to another\n * table, is never touched — `isGeneratedPolicyName` is the same predicate `db\n * push` uses to draw that line.\n *\n * Computed from the names read before anything is applied. The plan only ever\n * drops and re-creates its own names, so \"present before, not planned\" is the\n * same set the old read-after-apply found.\n */\nconst orphansOf = (plan: CollectionPolicyPlan, present: readonly string[]): string[] => {\n const expected = plannedPolicyNames(plan);\n return present.filter(name => !expected.has(name) && isGeneratedPolicyName(name, plan.table));\n};\n\nconst dropPolicySql = (plan: CollectionPolicyPlan, name: string): string =>\n `DROP POLICY IF EXISTS \"${name}\" ON \"${plan.schema}\".\"${plan.table}\"`;\n\n/** What every policy stamp starts with. See {@link policyStampSql}. */\nexport const POLICY_STAMP_PREFIX = \"rebase:\";\n\nconst sha256Hex = (text: string): string => createHash(\"sha256\").update(text, \"utf8\").digest(\"hex\");\n\n/** The same digest, computed by Postgres: hex SHA-256 of the text's UTF-8 bytes. */\nconst sha256Sql = (expression: string): string => `encode(sha256(convert_to(${expression}, 'UTF8')), 'hex')`;\n\n/**\n * The definition Postgres holds for the `pg_policy` row aliased `p`, as one\n * text: command, permissive/restrictive, the role OIDs, and the deparsed\n * `USING` and `WITH CHECK` (`-` for an absent clause, so absent and empty\n * differ). Everything `ALTER POLICY` can change, and everything a `CREATE\n * POLICY` sets.\n *\n * Read back with `pg_get_expr`, whose spelling depends on `search_path` (a\n * name on the path prints unqualified). The stamp is written and checked by\n * the same boot connections, so the two agree; a process with a different\n * path would only disagree, and re-apply — what every boot did before stamps.\n */\nconst liveDefinitionSql = (p: string): string =>\n `concat_ws(E'\\\\n', ${p}.polcmd::text, ${p}.polpermissive::text, ${p}.polroles::text, ` +\n `coalesce(pg_get_expr(${p}.polqual, ${p}.polrelid), '-'), ` +\n `coalesce(pg_get_expr(${p}.polwithcheck, ${p}.polrelid), '-'))`;\n\n/** The stamp a policy rendered as `renderedHash` carries when its stored definition hashes to `liveHash`. */\nconst stampOf = (renderedHash: string, liveHash: string): string =>\n `${POLICY_STAMP_PREFIX}${sha256Hex(`${renderedHash}:${liveHash}`)}`;\n\n/** The policies a plan creates, by name, each with the hash of its `CREATE POLICY` statement. */\nconst renderedHashes = (plan: CollectionPolicyPlan): Map<string, string> => {\n const hashes = new Map<string, string>();\n for (const statement of plan.policyStatements) {\n const match = /^\\s*CREATE POLICY\\s+\"([^\"]+)\"/i.exec(statement);\n if (match) hashes.set(match[1], sha256Hex(statement.trim()));\n }\n return hashes;\n};\n\n/**\n * One statement that stamps every policy the plan just created on its table:\n * `COMMENT ON POLICY … IS 'rebase:<hash>'`, the hash taken over the rendered\n * statement's hash and the definition Postgres stored — read back here, after\n * the `CREATE`s, in the same transaction. A `DO` block because the stored\n * definition is only known once the policy exists, and a `COMMENT` takes a\n * literal. One line, and it names only this table.\n *\n * `undefined` for a table the plan gives no policies.\n *\n * @param only the names to stamp, when not every policy on the table is known\n * to be what its statement makes — see `stampPoliciesStoredAsRendered`.\n */\nexport function policyStampSql(plan: CollectionPolicyPlan, only?: ReadonlySet<string>): string | undefined {\n const hashes = new Map([...renderedHashes(plan)].filter(([name]) => !only || only.has(name)));\n if (hashes.size === 0) return undefined;\n const target = `\"${plan.schema}\".\"${plan.table}\"`;\n const values = [...hashes].map(([name, hash]) => `(${quoteSqlLiteral(name)}, '${hash}')`).join(\", \");\n const stamp = `${quoteSqlLiteral(POLICY_STAMP_PREFIX)} || ${sha256Sql(`s.rendered || ':' || ${sha256Sql(liveDefinitionSql(\"p\"))}`)}`;\n return \"DO $rebase_policy_stamp$ DECLARE r record; BEGIN \" +\n `FOR r IN SELECT p.polname AS name, ${stamp} AS stamp FROM pg_policy p ` +\n `JOIN (VALUES ${values}) AS s(name, rendered) ON s.name = p.polname ` +\n `WHERE p.polrelid = ${quoteSqlLiteral(target)}::regclass LOOP ` +\n `EXECUTE 'COMMENT ON POLICY ' || quote_ident(r.name) || ${quoteSqlLiteral(` ON ${target} IS `)} || quote_literal(r.stamp); ` +\n \"END LOOP; END $rebase_policy_stamp$;\";\n}\n\n/** One policy as the catalogue has it. */\ninterface LivePolicy {\n /** Its comment — a stamp, when this module wrote it. */\n stamp: string | null;\n /** Hex SHA-256 of {@link liveDefinitionSql}. */\n live: string;\n}\n\n/**\n * Every policy on every table in the declared schemas — name, stamp and a hash\n * of its stored definition — in one read. It replaced a `pg_policies` query\n * per table, and later the name-only read that preceded stamps.\n */\nasync function readLivePolicies(client: Queryable, schemas: string[]): Promise<Map<string, Map<string, LivePolicy>>> {\n const policies = new Map<string, Map<string, LivePolicy>>();\n if (schemas.length === 0) return policies;\n const inList = schemas.map(schema => `'${assertSafeIdentifier(schema, \"schema name\")}'`).join(\", \");\n const { rows } = await client.query<{\n schemaname: string; tablename: string; policyname: string; stamp?: string | null; live?: string | null;\n }>(\n `SELECT n.nspname AS schemaname, c.relname AS tablename, p.polname AS policyname,\n obj_description(p.oid, 'pg_policy') AS stamp, ${sha256Sql(liveDefinitionSql(\"p\"))} AS live\n FROM pg_policy p\n JOIN pg_class c ON c.oid = p.polrelid\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE n.nspname IN (${inList})`\n );\n for (const row of rows) {\n const key = `${row.schemaname}.${row.tablename}`;\n if (!policies.has(key)) policies.set(key, new Map());\n policies.get(key)!.set(row.policyname, { stamp: row.stamp ?? null, live: row.live ?? \"\" });\n }\n return policies;\n}\n\n/**\n * Whether every policy the plan declares is on the table, stamped for exactly\n * the statement the plan would run and exactly the definition it holds now.\n */\nconst policiesAreCurrent = (plan: CollectionPolicyPlan, present: Map<string, LivePolicy>): boolean => {\n for (const [name, renderedHash] of renderedHashes(plan)) {\n const policy = present.get(name);\n if (!policy || policy.stamp !== stampOf(renderedHash, policy.live)) return false;\n }\n return true;\n};\n\n/**\n * What its stamp says of a declared policy the database has: written by Rebase\n * from the statement the plan renders now and still holding that definition\n * (`current`), stamped once but changed since — the rule's SQL, or the policy\n * by hand (`stale`), or carrying no stamp at all, so nothing tells whether a\n * re-apply changes it (`unstamped`).\n */\nexport type PolicyStampState = \"current\" | \"stale\" | \"unstamped\";\n\n/**\n * Every declared policy the database holds, by `schema.table`, then name, with\n * its {@link PolicyStampState}. The comparison a boot makes to leave a table\n * alone, read for each policy instead of each table.\n *\n * The stored definition is read with `pg_get_expr`, whose spelling depends on\n * `search_path`: call it under the path the stamps were written under (a\n * boot's, `public` unless the URL pins one), or a current stamp reads stale.\n */\nexport async function readPolicyStamps(\n client: Queryable,\n collections: CollectionConfig[]\n): Promise<Map<string, Map<string, PolicyStampState>>> {\n const plans = planCollectionPolicies(collections);\n const live = await readLivePolicies(client, Array.from(new Set(plans.map(p => p.schema))));\n const states = new Map<string, Map<string, PolicyStampState>>();\n for (const plan of plans) {\n const present = live.get(plan.qualified);\n if (!present) continue;\n const table = new Map<string, PolicyStampState>();\n for (const [name, renderedHash] of renderedHashes(plan)) {\n const policy = present.get(name);\n if (!policy) continue;\n table.set(name, policy.stamp === null || !policy.stamp.startsWith(POLICY_STAMP_PREFIX)\n ? \"unstamped\"\n : policy.stamp === stampOf(renderedHash, policy.live) ? \"current\" : \"stale\");\n }\n states.set(plan.qualified, table);\n }\n return states;\n}\n\n/** The name a probed policy is made under, in a transaction nothing commits. */\nconst PROBE_POLICY = \"rebase_policy_probe\";\n\n/**\n * For each of `policies`, whether the `CREATE POLICY` the plan renders for it\n * stores the definition the database holds now: made under a probe name on\n * the same table, read back, compared with the one there, and rolled back.\n *\n * What a stamp cannot answer. A stamp covers the statement as rendered, so a\n * rule whose text differs from the one that made the policy reads as changed\n * though Postgres would store the same thing: `schema introspect` writes a role\n * rule back as its deparsed SQL, and every such policy was planned as a\n * replacement of itself. A policy `db migrate` created has no stamp at all.\n *\n * One short transaction per policy, which takes the table's lock for as long\n * as it lasts and gives up on it after two seconds. A policy that could not be\n * probed — no statement for it, a lock not granted, a rule that does not\n * parse — is left out of the answer, and the caller keeps what the stamp said.\n * Keyed `schema.table.name`.\n */\nexport async function probeRenderedPolicies(\n client: Queryable,\n collections: CollectionConfig[],\n policies: readonly { schema: string; table: string; name: string }[],\n options: { searchPath?: string } = {}\n): Promise<Map<string, boolean>> {\n const statements = new Map<string, string>();\n for (const plan of planCollectionPolicies(collections)) {\n for (const statement of plan.policyStatements) {\n const match = /^\\s*CREATE POLICY\\s+\"([^\"]+)\"/i.exec(statement);\n if (match) statements.set(`${plan.schema}.${plan.table}.${match[1]}`, statement.trim());\n }\n }\n const answers = new Map<string, boolean>();\n for (const policy of policies) {\n const key = `${policy.schema}.${policy.table}.${policy.name}`;\n const statement = statements.get(key);\n if (!statement) continue;\n const probe = statement.replace(/^(\\s*CREATE POLICY\\s+)\"[^\"]+\"/i, `$1\"${PROBE_POLICY}\"`);\n const target = `\"${policy.schema.replace(/\"/g, '\"\"')}\".\"${policy.table.replace(/\"/g, '\"\"')}\"`;\n // Literals rather than parameters: a boot's `Queryable` takes none.\n const definition = (name: string): string =>\n `(SELECT ${liveDefinitionSql(\"p\")} FROM pg_policy p ` +\n `WHERE p.polrelid = ${quoteSqlLiteral(target)}::regclass AND p.polname = ${quoteSqlLiteral(name)})`;\n try {\n await client.query(\"BEGIN\");\n await client.query(\"SET LOCAL lock_timeout = '2s'\");\n if (options.searchPath) await client.query(`SET LOCAL search_path TO ${options.searchPath}`);\n await client.query(probe);\n const { rows } = await client.query<{ same: boolean | null }>(\n `SELECT ${definition(PROBE_POLICY)} IS NOT DISTINCT FROM ${definition(policy.name)} AS same`\n );\n if (typeof rows[0]?.same === \"boolean\") answers.set(key, rows[0].same);\n } catch {\n // Not probed; see above.\n } finally {\n await client.query(\"ROLLBACK\").catch(() => undefined);\n }\n }\n return answers;\n}\n\n/**\n * Every declared schema's tables, and whether each has row-level security on.\n *\n * One catalogue read for the whole step. It replaced a full `readExistingSchema`\n * — eight catalogue queries and a row probe of every table, of which only the\n * table names were used — and it carries the one fact that lets a boot leave an\n * already-locked table alone: `relrowsecurity`.\n *\n * Presence is still decided by `information_schema.columns`, as it was: the\n * same relations (views included, which have no RLS and are closed by revoking,\n * exactly as before) and the same privilege filter.\n */\nasync function readTableSecurity(client: Queryable, schemas: string[]): Promise<Map<string, { rls: boolean }>> {\n const tables = new Map<string, { rls: boolean }>();\n if (schemas.length === 0) return tables;\n const inList = schemas.map(schema => `'${assertSafeIdentifier(schema, \"schema name\")}'`).join(\", \");\n const { rows } = await client.query<{ table_schema: string; table_name: string; rls: boolean | null }>(\n `SELECT t.table_schema, t.table_name, c.relrowsecurity AS rls\n FROM (SELECT DISTINCT table_schema, table_name FROM information_schema.columns\n WHERE table_schema IN (${inList})) t\n JOIN pg_namespace n ON n.nspname = t.table_schema\n JOIN pg_class c ON c.relnamespace = n.oid AND c.relname = t.table_name`\n );\n for (const row of rows) tables.set(`${row.table_schema}.${row.table_name}`, { rls: row.rls === true });\n return tables;\n}\n\n/**\n * Whether the catalogue says RLS is on for one table, read fresh. `false` when\n * it cannot be read, which sends the caller down the fail-closed path.\n */\nasync function rlsIsOn(client: Queryable, plan: CollectionPolicyPlan): Promise<boolean> {\n try {\n const { rows } = await client.query<{ rls: boolean }>(\n `SELECT c.relrowsecurity AS rls FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE n.nspname = ${quoteSqlLiteral(plan.schema)} AND c.relname = ${quoteSqlLiteral(plan.table)}`\n );\n return rows[0]?.rls === true;\n } catch {\n return false;\n }\n}\n\n/** What one table's `ENABLE ROW LEVEL SECURITY` came to. */\ntype TableLock =\n | { locked: true }\n | { locked: false; error: string; grantWithdrawn: boolean };\n\n/**\n * Switch RLS on for one table, or — when Postgres refuses — take the user\n * role's privileges on it back, so the table is unreachable rather than\n * unprotected.\n *\n * A table the catalogue already reports RLS-on is left alone. `ENABLE` on it\n * changes nothing, and it is not free: it queues for an `ACCESS EXCLUSIVE` lock,\n * so a boot that met a long reader held every live query on that table behind\n * it — and when the wait ended in a timeout, the catch below revoked the user\n * role's privileges on a table that had been protected all along. Every\n * replica shares the grants, so one boot during a `pg_dump` closed the\n * collection for the whole deployment until the next clean boot.\n */\nasync function lockTable(\n client: Queryable,\n plan: CollectionPolicyPlan,\n rlsOn: boolean,\n run: (sql: string) => Promise<unknown> = sql => client.query(sql)\n): Promise<TableLock> {\n if (rlsOn) return { locked: true };\n try {\n await run(plan.enableRls);\n return { locked: true };\n } catch (err) {\n // The database's own answer, not the ORM's wrapper — see\n // `describeDriverError`. This is the most consequential message in\n // the file: it is what the operator reads about a table that is now\n // denying every request, and `Failed query: <the statement>` told\n // them nothing they did not already have.\n const error = describeDriverError(err);\n // A failed statement says nothing about the state it meant to reach: a\n // lock or statement timeout, a cancelled query, a peer that enabled it\n // first. Ask the catalogue. Only a table that really is RLS-off is\n // revoked — that safety is unchanged; a read that fails counts as off.\n if (await rlsIsOn(client, plan)) return { locked: true };\n // Take the privilege back rather than serve an unprotected table.\n // This is the fail-closed step the old code assumed it already had:\n // per-table, so one collection cannot take the rest of the\n // deployment down, but leaving nothing readable without RLS.\n //\n // Quoted, like every other statement here: unquoted, Postgres folds\n // the name to lower case, so a mixed-case adopted table (`\"User\"`)\n // was revoked as `public.user` — which fails, and a table that could\n // have been closed refused the boot instead.\n return withdrawGrant(client, plan, error);\n }\n}\n\n/**\n * Take the user role's privileges on a table back, so an RLS-off table is\n * unreachable rather than unprotected. `REVOKE` waits for no table lock a\n * reader holds, so it runs as a plain statement.\n */\nasync function withdrawGrant(client: Queryable, plan: CollectionPolicyPlan, error: string): Promise<TableLock> {\n try {\n await client.query(revokeUserRoleSql(plan.schema, plan.table));\n return { locked: false, error, grantWithdrawn: true };\n } catch {\n // Reported with `grantWithdrawn: false`, which the caller escalates.\n // There is nothing else this can do to make the table safe.\n return { locked: false, error, grantWithdrawn: false };\n }\n}\n\nexport interface TableLockResult {\n /** Tables RLS is now on for. */\n locked: number;\n /** Tables it could not be switched on for. See {@link PolicyEnsureResult.unsecured}. */\n unsecured: PolicyEnsureResult[\"unsecured\"];\n}\n\n/**\n * Switch RLS on for every table the collections declare — each collection's\n * and each junction's — and do nothing else.\n *\n * The first thing `rebase db push` does once Atlas has committed. Atlas cannot\n * carry `ENABLE ROW LEVEL SECURITY` (its free tier neither plans nor reads it),\n * so a table it creates arrives with RLS off — while the user role already\n * holds DML on it through the schema's default privileges. Everything push runs\n * after Atlas can refuse: the vector, search and trigger DDL, the policies\n * themselves. Locking first means none of those failures can leave a table\n * open: RLS with no policy denies, which is a state a failed push may leave.\n *\n * A declared table that is not in the database is skipped — nothing can reach\n * a table that does not exist, and the step that creates it locks it.\n */\nexport async function lockCollectionTables(\n client: Queryable,\n collections: CollectionConfig[]\n): Promise<TableLockResult> {\n const result: TableLockResult = { locked: 0, unsecured: [] };\n for (const { plan, rls } of await declaredTablesPresent(client, collections)) {\n const lock = await lockTable(client, plan, rls);\n if (lock.locked) {\n result.locked++;\n } else {\n result.unsecured.push({ table: plan.qualified, error: lock.error, grantWithdrawn: lock.grantWithdrawn });\n }\n }\n return result;\n}\n\n/**\n * The declared tables {@link lockCollectionTables} would switch row-level\n * security on for: those in the database with it off. What `db push\n * --dry-run` lists, read the way the push's lock step reads it.\n */\nexport async function tablesRlsWouldEnable(client: Queryable, collections: CollectionConfig[]): Promise<string[]> {\n return (await declaredTablesPresent(client, collections)).filter(table => !table.rls).map(table => table.plan.qualified);\n}\n\n/** Each declared table the database has, with whether its RLS is on. */\nasync function declaredTablesPresent(\n client: Queryable,\n collections: CollectionConfig[]\n): Promise<{ plan: CollectionPolicyPlan; rls: boolean }[]> {\n const plans = planCollectionPolicies(collections);\n if (plans.length === 0) return [];\n const tables = await readTableSecurity(client, Array.from(new Set(plans.map(p => p.schema))));\n return plans.flatMap(plan => {\n const table = tables.get(plan.qualified);\n return table ? [{ plan, rls: table.rls }] : [];\n });\n}\n\n/** One declared table that exists, and what the catalogue said about it. */\ninterface TableWork {\n plan: CollectionPolicyPlan;\n rlsOn: boolean;\n orphans: string[];\n /** Whether the policy names on the table differ from the plan's. */\n rulesChanged: boolean;\n /** Whether the plan holds no generated rule of its own, so the orphans are every rule the table had. */\n declaresNoRule: boolean;\n}\n\ntype AtomicOutcome =\n | { kind: \"applied\" }\n | { kind: \"lock-timeout\"; error: string }\n | { kind: \"unsupported\" }\n | { kind: \"failed\"; error: string };\n\n/**\n * Bring the declared collections' RLS policies up to date. Returns what it did.\n *\n * Only tables that already exist are touched: the boot-time table creator runs\n * first, so anything still missing is a table this additive path did not\n * create (it creates junctions too) — one a failed boot DDL left out, or a\n * project whose schema a migration owns. Enabling RLS on a\n * non-existent table would error, so those are recorded as skipped, not failed.\n *\n * ## One transaction per table\n *\n * Each table's statements — `ENABLE` when RLS is off, every `DROP POLICY IF\n * EXISTS` / `CREATE POLICY` pair, the orphan drops — go to Postgres as one\n * string, which it runs as one transaction (see `boot-ddl`). Before, each was\n * its own autocommit: at 240 tables, 4,080 commits and 4,080 ACCESS EXCLUSIVE\n * lock acquisitions per boot, 6–22 s on a disk that really flushes. And between\n * a policy's DROP and its CREATE, every other replica evaluated the table with\n * that policy missing — a user's continuous reads of their own row came back\n * empty once per boot (measured, Postgres 18). Now a concurrent request sees the\n * old policies or the new ones, never neither.\n *\n * If the transaction fails for any reason but a lock timeout, the table is\n * applied again one statement at a time, exactly as before — so a rule that\n * cannot be created (a function a migration has not added yet) leaves the\n * table in the same deny-leaning partial state it always did, rather than\n * rolling back to the old, possibly looser, policies.\n *\n * ## Lock budget\n *\n * The transaction runs under `lock_timeout` (`boot-ddl`), so a table that a\n * long transaction, a report or a dump is holding costs live traffic one\n * second, not the statement timeout. A contended table is retried after the\n * others, with backoff, within a retry budget for the whole step; one still\n * held after that is reported in `deferred` — its transaction rolled back, so\n * it is exactly as it was before this boot — or, if RLS was off on it, closed\n * by revoking the user role's grant, as any table RLS cannot be enabled on is.\n */\nexport async function ensureCollectionPolicies(\n client: Queryable,\n collections: CollectionConfig[],\n log?: (message: string) => void,\n options: BootLockOptions = {}\n): Promise<PolicyEnsureResult> {\n const result: PolicyEnsureResult = {\n policiesApplied: 0, tablesSecured: 0, unchanged: 0, skipped: [], failures: [], unsecured: [],\n orphansDropped: 0, deferred: [], rulesDropped: []\n };\n\n const plans = planCollectionPolicies(collections);\n if (plans.length === 0) return result;\n const lock = resolveBootLockOptions(options);\n\n const schemas = Array.from(new Set(plans.map(p => p.schema)));\n const tables = await readTableSecurity(client, schemas);\n const livePolicies = await readLivePolicies(client, schemas);\n\n const work: TableWork[] = [];\n for (const plan of plans) {\n const table = tables.get(plan.qualified);\n if (!table) {\n result.skipped.push({\n table: plan.qualified,\n reason: \"table is not present in the database; create it with `rebase db push` / `rebase db migrate`\"\n });\n continue;\n }\n const live = livePolicies.get(plan.qualified) ?? new Map<string, LivePolicy>();\n const present = [...live.keys()];\n const orphans = orphansOf(plan, present);\n // Already exactly what the plan would leave: nothing to run, no lock to take.\n if (table.rls && orphans.length === 0 && policiesAreCurrent(plan, live)) {\n result.tablesSecured++;\n result.unchanged++;\n continue;\n }\n const presentSet = new Set(present);\n const rulesChanged = orphans.length > 0 || [...plannedPolicyNames(plan)].some(name => !presentSet.has(name));\n const declaresNoRule = ![...plannedPolicyNames(plan)].some(name => isGeneratedPolicyName(name, plan.table));\n work.push({ plan, rlsOn: table.rls, orphans, rulesChanged, declaresNoRule });\n }\n if (result.unchanged > 0) {\n log?.(`${result.unchanged} table(s) already have exactly the declared policies; left as they are`);\n }\n\n // Until the handle refuses a multi-statement string (an extended-protocol\n // driver), after which every table goes one statement at a time.\n let atomic = true;\n let retryBudget = lock.retryBudgetMs;\n let queue = work;\n let lastError = new Map<TableWork, string>();\n\n for (let attempt = 1; queue.length > 0; attempt++) {\n if (attempt > 1) {\n if (attempt > lock.attempts || retryBudget <= 0) break;\n await sleep(lock.backoffMs(attempt - 1));\n }\n const contended: TableWork[] = [];\n const errors = new Map<TableWork, string>();\n for (const table of queue) {\n if (attempt > 1 && retryBudget <= 0) {\n contended.push(table);\n errors.set(table, lastError.get(table) ?? \"\");\n continue;\n }\n if (atomic) {\n const outcome = await applyAtomically(client, table, lock);\n if (outcome.kind === \"applied\") {\n record(result, table, log);\n continue;\n }\n if (outcome.kind === \"lock-timeout\") {\n if (attempt > 1) retryBudget -= lock.lockTimeoutMs;\n contended.push(table);\n errors.set(table, outcome.error);\n continue;\n }\n if (outcome.kind === \"unsupported\") {\n atomic = false;\n } else {\n log?.(`${table.plan.qualified}: applying its policies in one transaction failed ` +\n `(${outcome.error}); applying them one statement at a time`);\n }\n }\n await applyEachStatement(client, table, result, log, atomic ? lock : undefined);\n }\n queue = contended;\n lastError = errors;\n }\n\n // Still contended after every attempt the budget allowed.\n for (const table of queue) {\n await giveUp(client, table, lastError.get(table) ?? \"\", lock, result);\n }\n\n return result;\n}\n\n/**\n * The table's statements, in the order they must run — the stamp last, so it\n * reads back the policies this same transaction created.\n */\nconst statementsOf = (table: TableWork): string[] => {\n const stamp = policyStampSql(table.plan);\n return [\n ...(table.rlsOn ? [] : [table.plan.enableRls]),\n ...table.plan.policyStatements,\n ...table.orphans.map(name => dropPolicySql(table.plan, name)),\n ...(stamp ? [stamp] : [])\n ];\n};\n\nasync function applyAtomically(\n client: Queryable,\n table: TableWork,\n lock: ResolvedBootLockOptions\n): Promise<AtomicOutcome> {\n const statements = statementsOf(table);\n if (statements.length === 0) return { kind: \"applied\" };\n try {\n await client.query(atomicBatch(statements, lock.lockTimeoutMs));\n return { kind: \"applied\" };\n } catch (err) {\n if (isLockTimeout(err)) return { kind: \"lock-timeout\", error: describeDriverError(err) };\n if (isMultiStatementRefusal(err)) return { kind: \"unsupported\" };\n return { kind: \"failed\", error: describeDriverError(err) };\n }\n}\n\n/**\n * The warning a boot gives for one entry of {@link PolicyEnsureResult.rulesDropped}:\n * each policy, what it means for the table, and the likely cause.\n */\nexport function describeRulesDropped(dropped: { table: string; policies: string[] }): string {\n return `🔐 [rls] \"${dropped.table}\" declares no securityRules, so the policies its rules had made were dropped: ` +\n `${dropped.policies.join(\", \")}. Only administrators reach its rows now, on every server sharing this database. ` +\n \"If its collection came in an array handed to createRebaseBackend, that array lacked the defaultSecurityRules \" +\n \"its config/collections/index.ts declares: pass collectionsDir, or the array that index exports, which carries \" +\n \"them. If the rules were removed on purpose, this is that change.\";\n}\n\n/** The orphans just dropped, when they were every rule the table had. See {@link PolicyEnsureResult.rulesDropped}. */\nfunction recordRulesDropped(result: PolicyEnsureResult, table: TableWork): void {\n if (table.declaresNoRule && table.orphans.length > 0) {\n result.rulesDropped.push({ table: table.plan.qualified, policies: [...table.orphans].sort() });\n }\n}\n\nfunction record(result: PolicyEnsureResult, table: TableWork, log?: (message: string) => void): void {\n const created = table.plan.policyStatements.filter(isCreatePolicy).length;\n result.tablesSecured++;\n result.policiesApplied += created;\n result.orphansDropped += table.orphans.length;\n recordRulesDropped(result, table);\n log?.(`${table.plan.qualified}: RLS enabled, ${created} policy(ies) applied` +\n (table.orphans.length > 0 ? `, ${table.orphans.length} orphan(s) dropped` : \"\"));\n}\n\n/**\n * The table applied one statement at a time — the path every table took before\n * the per-table transaction, kept as its fallback so a failure ends exactly\n * where it always did.\n *\n * Each statement still carries the lock budget when the handle can take it.\n */\nasync function applyEachStatement(\n client: Queryable,\n table: TableWork,\n result: PolicyEnsureResult,\n log: ((message: string) => void) | undefined,\n lock: ResolvedBootLockOptions | undefined\n): Promise<void> {\n const { plan } = table;\n const run = (sql: string) => client.query(lock ? atomicBatch([sql], lock.lockTimeoutMs) : sql);\n\n // Enabling RLS is its own step, because its failure is the opposite of\n // every other failure here. Once RLS is on, anything that goes wrong\n // afterwards leaves the table denying; while it is off, the grant made\n // earlier in boot leaves the table wide open. Sharing one `try` meant\n // the dangerous case was reported in the safe case's words.\n const locked = await lockTable(client, plan, table.rlsOn, run);\n if (!locked.locked) {\n result.unsecured.push({ table: plan.qualified, error: locked.error, grantWithdrawn: locked.grantWithdrawn });\n return;\n }\n result.tablesSecured++;\n\n try {\n let created = 0;\n for (const statement of plan.policyStatements) {\n await run(statement);\n if (isCreatePolicy(statement)) {\n result.policiesApplied++;\n created++;\n }\n }\n for (const name of table.orphans) {\n await run(dropPolicySql(plan, name));\n result.orphansDropped++;\n }\n recordRulesDropped(result, table);\n log?.(`${plan.qualified}: RLS enabled, ${created} policy(ies) applied` +\n (table.orphans.length > 0 ? `, ${table.orphans.length} orphan(s) dropped` : \"\"));\n } catch (err) {\n result.failures.push({\n table: plan.qualified,\n error: describeDriverError(err)\n });\n return;\n }\n\n // Only once every policy is in. A stamp that cannot be written costs the\n // next boot a re-apply of this table — what every boot did before stamps —\n // so it is reported, not failed.\n const stamp = policyStampSql(plan);\n if (!stamp) return;\n try {\n await run(stamp);\n } catch (err) {\n log?.(`${plan.qualified}: policies applied, but not stamped (${describeDriverError(err)}); ` +\n \"the next boot applies them again\");\n }\n}\n\n/**\n * A table whose lock was held for every attempt. Its transaction rolled back\n * each time, so it is exactly as it was before this boot.\n *\n * With RLS on, that is a safe state to leave it in — reported, not failed. With\n * RLS off it is an open table over the schema-wide grant, so it is closed the\n * way any table RLS cannot be enabled on is: the catalogue is asked once more,\n * and only a table that really is RLS-off has the user role's grant withdrawn.\n */\nasync function giveUp(\n client: Queryable,\n table: TableWork,\n lastError: string,\n lock: ResolvedBootLockOptions,\n result: PolicyEnsureResult\n): Promise<void> {\n const { plan } = table;\n const error =\n `another session held the table's lock for every attempt (lock_timeout ${lock.lockTimeoutMs} ms` +\n `${lastError ? `: ${lastError.split(\"\\n\")[0]}` : \"\"}) — a long transaction, a report or a dump`;\n if (table.rlsOn || await rlsIsOn(client, plan)) {\n result.tablesSecured++;\n result.deferred.push({ table: plan.qualified, error, rulesChanged: table.rulesChanged });\n return;\n }\n const closed = await withdrawGrant(client, plan, error);\n if (!closed.locked) {\n result.unsecured.push({ table: plan.qualified, error: closed.error, grantWithdrawn: closed.grantWithdrawn });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ensure-collection-policies-DLWgabQ2.js","names":[],"sources":["../src/schema/ensure-collection-policies.ts"],"sourcesContent":["/**\n * Applying a bundle's RLS policies to a database at boot, idempotently.\n *\n * ## Why this exists\n *\n * {@link ensureCollectionTables} creates the collection *tables* a managed\n * runtime boots against, but a table with row-level security disabled and no\n * policies is not servable: authenticated requests run as the restricted\n * `rebase_user` role, so a read with no `SELECT` policy returns nothing (a\n * public collection answered 401) and a write with no `INSERT`/`UPDATE` policy\n * is denied. The policies live in the collections' `securityRules`; nothing at\n * boot applied them. `rebase db push` does — but it drives Atlas against a\n * local `DATABASE_URL`, and a managed tenant's database is reachable only from\n * inside the cluster, by the runtime that is already connected to it. So the\n * runtime is the only thing that *can* apply them, and this is where it does.\n *\n * ## Why this is safe to run on every boot\n *\n * Every statement is idempotent: `ENABLE ROW LEVEL SECURITY` is only issued for\n * a table the catalogue reports RLS-off (on one that has it, it changes nothing\n * and still queues for an ACCESS EXCLUSIVE lock), and each policy is a `DROP\n * POLICY IF EXISTS` immediately followed by a `CREATE POLICY`, so re-applying\n * asserts exactly the declared state. It adds\n * and replaces; it never drops data. (It does not *reconcile* — a policy a\n * previous push left behind under an old name is not removed here; that stays a\n * `db push` / `db migrate` concern, alongside destructive schema changes.)\n *\n * ## A table whose policies are already the declared ones is left alone\n *\n * Every policy this module writes carries a stamp, `COMMENT ON POLICY … IS\n * 'rebase:<hash>'` (see {@link policyStampSql}). The hash covers the\n * `CREATE POLICY` statement that was rendered *and* the definition Postgres\n * stored for it — command, mode, roles, and the deparsed `USING` / `WITH\n * CHECK` — as read back in the same transaction. A boot recomputes it from the\n * statement it would run now and the definition the catalogue holds now, and\n * re-applies a table only when that differs for any of its policies, a declared\n * policy is missing, a superseded one is still there, or RLS is off.\n *\n * So both halves of \"the declared state\" still win: a rule whose compiled SQL\n * changed (a new rule under the same name, a compiler change) misses on the\n * first half, and a policy someone altered by hand — `ALTER POLICY`, a\n * different role list — misses on the second, and either is re-applied exactly\n * as every boot used to re-apply everything. A boot that changes nothing runs\n * two catalogue reads here, no DDL and takes no table lock. It used to drop and\n * re-create every policy, one ACCESS EXCLUSIVE lock per table: about 1 s at 200\n * collections, and under a `pg_dump` up to the whole retry budget.\n *\n * A policy without a stamp — written by `db push`, by hand, or by a release\n * that predates stamps — counts as different, so the first boot after either\n * re-applies its table once and stamps it. Nothing else reads the comment:\n * drift checks, `doctor`, rls-check and Studio compare names, roles, commands\n * and clauses, never descriptions.\n *\n * Unlike table creation, a failure here is not fatal: RLS stays enabled, so a\n * table whose policies could not be applied fails **closed** (denies) rather\n * than leaking rows. One collection's policy failing (e.g. a rule that\n * references a table a real migration has not created yet) must not crash-loop\n * the whole deployment and take the other collections' working routes down with\n * it. Failures are reported loudly and per-table so the operator can see\n * exactly which collection is not yet servable and why.\n */\nimport { createHash } from \"node:crypto\";\nimport { type CollectionConfig } from \"@rebasepro/types\";\nimport { planCollectionPolicies, type CollectionPolicyPlan } from \"./generate-postgres-ddl-logic\";\nimport { isGeneratedPolicyName } from \"../security/policy-drift\";\nimport { describeDriverError, type Queryable } from \"./ensure-collection-tables\";\nimport { assertSafeIdentifier } from \"./plan/diff-plan\";\nimport { quoteSqlLiteral } from \"./generate-postgres-ddl-logic\";\nimport { revokeUserRoleSql } from \"../security/rls-enforcement\";\nimport {\n atomicBatch,\n isLockTimeout,\n isMultiStatementRefusal,\n resolveBootLockOptions,\n sleep,\n type BootLockOptions,\n type ResolvedBootLockOptions\n} from \"./boot-ddl\";\n\nexport interface PolicyEnsureResult {\n /** `CREATE POLICY` statements that ran successfully. */\n policiesApplied: number;\n /** Tables that had RLS enabled — including the {@link unchanged} ones, which already had it. */\n tablesSecured: number;\n /**\n * Tables whose policies were already exactly the declared ones, RLS on, by\n * the stamp each policy carries — left alone: no statement, no lock.\n */\n unchanged: number;\n /** Declared tables absent from the database — left to a real migration. */\n skipped: { table: string; reason: string }[];\n /**\n * Tables that have RLS on but did not get every policy. They deny — RLS\n * with no matching policy is deny-all — so they are safe but not servable.\n */\n failures: { table: string; error: string }[];\n /**\n * Tables RLS could not be enabled on, whose DML grant was withdrawn instead.\n *\n * This state had no name, and that was the bug: `ENABLE ROW LEVEL SECURITY`\n * failing was recorded as a `failure` and reported with the same \"it stays\n * locked (denies)\" wording as a failed policy — but the two are opposites.\n * A policy statement failing leaves RLS on and the table denying. `enableRls`\n * failing leaves RLS *off*, and the schema-wide grant to the user role has\n * already been made by `ensureRlsEnforcement`, so the table is readable and\n * writable by every authenticated request with no row filtering at all.\n */\n unsecured: { table: string; error: string; grantWithdrawn: boolean }[];\n /**\n * Generated policies removed because no current rule produces them.\n *\n * A policy's name embeds a hash of the rule's semantics, so editing a rule\n * does not update a policy — it creates a new one and abandons the old.\n * Postgres ORs permissive policies, so the abandoned one keeps granting:\n * a `USING (true)` tightened to an owner check went on admitting everyone,\n * forever, while the deploy logged success.\n *\n * `db push` reconciles this, and cannot reach a managed tenant's in-cluster\n * database — which is the reason this module exists. So boot has to do it.\n */\n orphansDropped: number;\n /**\n * Tables whose policies were not re-applied because another session held\n * the table's lock for every attempt — a long transaction, a report, a\n * dump. Each table's policy statements run in one transaction, so a table\n * here was left exactly as it was: RLS on, its existing policies in force.\n * `rulesChanged` says whether those differ from the declared ones by name\n * (a rule whose policy is missing, or an orphan still granting), in which\n * case the new rules are not in force on this table until a later boot.\n */\n deferred: { table: string; error: string; rulesChanged: boolean }[];\n /**\n * Tables whose collection declares no rule of its own and which still held\n * generated rule policies, all of them dropped: the generated policies, by\n * name, the boot is to warn about.\n *\n * An edited rule supersedes its old policy, and that drop is the point of\n * {@link PolicyEnsureResult.orphansDropped}. A collection with no rule at\n * all is something else: what a collection looks like when the\n * `defaultSecurityRules` its directory's index declares never reached it —\n * an array handed to a backend without them. Dropping is still right (the\n * declared state wins, and the table is left admin-only rather than open),\n * but it changes who can read the table on every server sharing the\n * database, so it is not done in silence.\n */\n rulesDropped: { table: string; policies: string[] }[];\n}\n\nconst isCreatePolicy = (statement: string): boolean => /^\\s*CREATE POLICY/i.test(statement);\n\n/** The policy names a plan creates. */\nconst plannedPolicyNames = (plan: CollectionPolicyPlan): Set<string> => {\n const expected = new Set<string>();\n for (const statement of plan.policyStatements) {\n const match = /^\\s*CREATE POLICY\\s+\"([^\"]+)\"/i.exec(statement);\n if (match) expected.add(match[1]);\n }\n return expected;\n};\n\n/**\n * Generated policies on this table that the current plan does not produce.\n *\n * Scoped hard, because dropping a policy is destructive: only this one table,\n * only names matching the generated `<table>_<op>_<sha1>` shape, and only names\n * absent from the plan. A hand-written policy, or one belonging to another\n * table, is never touched — `isGeneratedPolicyName` is the same predicate `db\n * push` uses to draw that line.\n *\n * Computed from the names read before anything is applied. The plan only ever\n * drops and re-creates its own names, so \"present before, not planned\" is the\n * same set the old read-after-apply found.\n */\nconst orphansOf = (plan: CollectionPolicyPlan, present: readonly string[]): string[] => {\n const expected = plannedPolicyNames(plan);\n return present.filter(name => !expected.has(name) && isGeneratedPolicyName(name, plan.table));\n};\n\nconst dropPolicySql = (plan: CollectionPolicyPlan, name: string): string =>\n `DROP POLICY IF EXISTS \"${name}\" ON \"${plan.schema}\".\"${plan.table}\"`;\n\n/** What every policy stamp starts with. See {@link policyStampSql}. */\nexport const POLICY_STAMP_PREFIX = \"rebase:\";\n\nconst sha256Hex = (text: string): string => createHash(\"sha256\").update(text, \"utf8\").digest(\"hex\");\n\n/** The same digest, computed by Postgres: hex SHA-256 of the text's UTF-8 bytes. */\nconst sha256Sql = (expression: string): string => `encode(sha256(convert_to(${expression}, 'UTF8')), 'hex')`;\n\n/**\n * The definition Postgres holds for the `pg_policy` row aliased `p`, as one\n * text: command, permissive/restrictive, the role OIDs, and the deparsed\n * `USING` and `WITH CHECK` (`-` for an absent clause, so absent and empty\n * differ). Everything `ALTER POLICY` can change, and everything a `CREATE\n * POLICY` sets.\n *\n * Read back with `pg_get_expr`, whose spelling depends on `search_path` (a\n * name on the path prints unqualified). The stamp is written and checked by\n * the same boot connections, so the two agree; a process with a different\n * path would only disagree, and re-apply — what every boot did before stamps.\n */\nconst liveDefinitionSql = (p: string): string =>\n `concat_ws(E'\\\\n', ${p}.polcmd::text, ${p}.polpermissive::text, ${p}.polroles::text, ` +\n `coalesce(pg_get_expr(${p}.polqual, ${p}.polrelid), '-'), ` +\n `coalesce(pg_get_expr(${p}.polwithcheck, ${p}.polrelid), '-'))`;\n\n/** The stamp a policy rendered as `renderedHash` carries when its stored definition hashes to `liveHash`. */\nconst stampOf = (renderedHash: string, liveHash: string): string =>\n `${POLICY_STAMP_PREFIX}${sha256Hex(`${renderedHash}:${liveHash}`)}`;\n\n/** The policies a plan creates, by name, each with the hash of its `CREATE POLICY` statement. */\nconst renderedHashes = (plan: CollectionPolicyPlan): Map<string, string> => {\n const hashes = new Map<string, string>();\n for (const statement of plan.policyStatements) {\n const match = /^\\s*CREATE POLICY\\s+\"([^\"]+)\"/i.exec(statement);\n if (match) hashes.set(match[1], sha256Hex(statement.trim()));\n }\n return hashes;\n};\n\n/**\n * One statement that stamps every policy the plan just created on its table:\n * `COMMENT ON POLICY … IS 'rebase:<hash>'`, the hash taken over the rendered\n * statement's hash and the definition Postgres stored — read back here, after\n * the `CREATE`s, in the same transaction. A `DO` block because the stored\n * definition is only known once the policy exists, and a `COMMENT` takes a\n * literal. One line, and it names only this table.\n *\n * `undefined` for a table the plan gives no policies.\n *\n * @param only the names to stamp, when not every policy on the table is known\n * to be what its statement makes — see `stampPoliciesStoredAsRendered`.\n */\nexport function policyStampSql(plan: CollectionPolicyPlan, only?: ReadonlySet<string>): string | undefined {\n const hashes = new Map([...renderedHashes(plan)].filter(([name]) => !only || only.has(name)));\n if (hashes.size === 0) return undefined;\n const target = `\"${plan.schema}\".\"${plan.table}\"`;\n const values = [...hashes].map(([name, hash]) => `(${quoteSqlLiteral(name)}, '${hash}')`).join(\", \");\n const stamp = `${quoteSqlLiteral(POLICY_STAMP_PREFIX)} || ${sha256Sql(`s.rendered || ':' || ${sha256Sql(liveDefinitionSql(\"p\"))}`)}`;\n return \"DO $rebase_policy_stamp$ DECLARE r record; BEGIN \" +\n `FOR r IN SELECT p.polname AS name, ${stamp} AS stamp FROM pg_policy p ` +\n `JOIN (VALUES ${values}) AS s(name, rendered) ON s.name = p.polname ` +\n `WHERE p.polrelid = ${quoteSqlLiteral(target)}::regclass LOOP ` +\n `EXECUTE 'COMMENT ON POLICY ' || quote_ident(r.name) || ${quoteSqlLiteral(` ON ${target} IS `)} || quote_literal(r.stamp); ` +\n \"END LOOP; END $rebase_policy_stamp$;\";\n}\n\n/** One policy as the catalogue has it. */\ninterface LivePolicy {\n /** Its comment — a stamp, when this module wrote it. */\n stamp: string | null;\n /** Hex SHA-256 of {@link liveDefinitionSql}. */\n live: string;\n}\n\n/**\n * Every policy on every table in the declared schemas — name, stamp and a hash\n * of its stored definition — in one read. It replaced a `pg_policies` query\n * per table, and later the name-only read that preceded stamps.\n */\nasync function readLivePolicies(client: Queryable, schemas: string[]): Promise<Map<string, Map<string, LivePolicy>>> {\n const policies = new Map<string, Map<string, LivePolicy>>();\n if (schemas.length === 0) return policies;\n const inList = schemas.map(schema => `'${assertSafeIdentifier(schema, \"schema name\")}'`).join(\", \");\n const { rows } = await client.query<{\n schemaname: string; tablename: string; policyname: string; stamp?: string | null; live?: string | null;\n }>(\n `SELECT n.nspname AS schemaname, c.relname AS tablename, p.polname AS policyname,\n obj_description(p.oid, 'pg_policy') AS stamp, ${sha256Sql(liveDefinitionSql(\"p\"))} AS live\n FROM pg_policy p\n JOIN pg_class c ON c.oid = p.polrelid\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE n.nspname IN (${inList})`\n );\n for (const row of rows) {\n const key = `${row.schemaname}.${row.tablename}`;\n if (!policies.has(key)) policies.set(key, new Map());\n policies.get(key)!.set(row.policyname, { stamp: row.stamp ?? null, live: row.live ?? \"\" });\n }\n return policies;\n}\n\n/**\n * Whether every policy the plan declares is on the table, stamped for exactly\n * the statement the plan would run and exactly the definition it holds now.\n */\nconst policiesAreCurrent = (plan: CollectionPolicyPlan, present: Map<string, LivePolicy>): boolean => {\n for (const [name, renderedHash] of renderedHashes(plan)) {\n const policy = present.get(name);\n if (!policy || policy.stamp !== stampOf(renderedHash, policy.live)) return false;\n }\n return true;\n};\n\n/**\n * What its stamp says of a declared policy the database has: written by Rebase\n * from the statement the plan renders now and still holding that definition\n * (`current`), stamped once but changed since — the rule's SQL, or the policy\n * by hand (`stale`), or carrying no stamp at all, so nothing tells whether a\n * re-apply changes it (`unstamped`).\n */\nexport type PolicyStampState = \"current\" | \"stale\" | \"unstamped\";\n\n/**\n * Every declared policy the database holds, by `schema.table`, then name, with\n * its {@link PolicyStampState}. The comparison a boot makes to leave a table\n * alone, read for each policy instead of each table.\n *\n * The stored definition is read with `pg_get_expr`, whose spelling depends on\n * `search_path`: call it under the path the stamps were written under (a\n * boot's, `public` unless the URL pins one), or a current stamp reads stale.\n */\nexport async function readPolicyStamps(\n client: Queryable,\n collections: CollectionConfig[]\n): Promise<Map<string, Map<string, PolicyStampState>>> {\n const plans = planCollectionPolicies(collections);\n const live = await readLivePolicies(client, Array.from(new Set(plans.map(p => p.schema))));\n const states = new Map<string, Map<string, PolicyStampState>>();\n for (const plan of plans) {\n const present = live.get(plan.qualified);\n if (!present) continue;\n const table = new Map<string, PolicyStampState>();\n for (const [name, renderedHash] of renderedHashes(plan)) {\n const policy = present.get(name);\n if (!policy) continue;\n table.set(name, policy.stamp === null || !policy.stamp.startsWith(POLICY_STAMP_PREFIX)\n ? \"unstamped\"\n : policy.stamp === stampOf(renderedHash, policy.live) ? \"current\" : \"stale\");\n }\n states.set(plan.qualified, table);\n }\n return states;\n}\n\n/** The name a probed policy is made under, in a transaction nothing commits. */\nconst PROBE_POLICY = \"rebase_policy_probe\";\n\n/**\n * For each of `policies`, whether the `CREATE POLICY` the plan renders for it\n * stores the definition the database holds now: made under a probe name on\n * the same table, read back, compared with the one there, and rolled back.\n *\n * What a stamp cannot answer. A stamp covers the statement as rendered, so a\n * rule whose text differs from the one that made the policy reads as changed\n * though Postgres would store the same thing: `schema introspect` writes a role\n * rule back as its deparsed SQL, and every such policy was planned as a\n * replacement of itself. A policy `db migrate` created has no stamp at all.\n *\n * One short transaction per policy, which takes the table's lock for as long\n * as it lasts and gives up on it after two seconds. A policy that could not be\n * probed — no statement for it, a lock not granted, a rule that does not\n * parse — is left out of the answer, and the caller keeps what the stamp said.\n * Keyed `schema.table.name`.\n */\nexport async function probeRenderedPolicies(\n client: Queryable,\n collections: CollectionConfig[],\n policies: readonly { schema: string; table: string; name: string }[],\n options: { searchPath?: string } = {}\n): Promise<Map<string, boolean>> {\n const statements = new Map<string, string>();\n for (const plan of planCollectionPolicies(collections)) {\n for (const statement of plan.policyStatements) {\n const match = /^\\s*CREATE POLICY\\s+\"([^\"]+)\"/i.exec(statement);\n if (match) statements.set(`${plan.schema}.${plan.table}.${match[1]}`, statement.trim());\n }\n }\n const answers = new Map<string, boolean>();\n for (const policy of policies) {\n const key = `${policy.schema}.${policy.table}.${policy.name}`;\n const statement = statements.get(key);\n if (!statement) continue;\n const probe = statement.replace(/^(\\s*CREATE POLICY\\s+)\"[^\"]+\"/i, `$1\"${PROBE_POLICY}\"`);\n const target = `\"${policy.schema.replace(/\"/g, '\"\"')}\".\"${policy.table.replace(/\"/g, '\"\"')}\"`;\n // Literals rather than parameters: a boot's `Queryable` takes none.\n const definition = (name: string): string =>\n `(SELECT ${liveDefinitionSql(\"p\")} FROM pg_policy p ` +\n `WHERE p.polrelid = ${quoteSqlLiteral(target)}::regclass AND p.polname = ${quoteSqlLiteral(name)})`;\n try {\n await client.query(\"BEGIN\");\n await client.query(\"SET LOCAL lock_timeout = '2s'\");\n if (options.searchPath) await client.query(`SET LOCAL search_path TO ${options.searchPath}`);\n await client.query(probe);\n const { rows } = await client.query<{ same: boolean | null }>(\n `SELECT ${definition(PROBE_POLICY)} IS NOT DISTINCT FROM ${definition(policy.name)} AS same`\n );\n if (typeof rows[0]?.same === \"boolean\") answers.set(key, rows[0].same);\n } catch {\n // Not probed; see above.\n } finally {\n await client.query(\"ROLLBACK\").catch(() => undefined);\n }\n }\n return answers;\n}\n\n/**\n * Every declared schema's tables, and whether each has row-level security on.\n *\n * One catalogue read for the whole step. It replaced a full `readExistingSchema`\n * — eight catalogue queries and a row probe of every table, of which only the\n * table names were used — and it carries the one fact that lets a boot leave an\n * already-locked table alone: `relrowsecurity`.\n *\n * Presence is still decided by `information_schema.columns`, as it was: the\n * same relations (views included, which have no RLS and are closed by revoking,\n * exactly as before) and the same privilege filter.\n */\nasync function readTableSecurity(client: Queryable, schemas: string[]): Promise<Map<string, { rls: boolean }>> {\n const tables = new Map<string, { rls: boolean }>();\n if (schemas.length === 0) return tables;\n const inList = schemas.map(schema => `'${assertSafeIdentifier(schema, \"schema name\")}'`).join(\", \");\n const { rows } = await client.query<{ table_schema: string; table_name: string; rls: boolean | null }>(\n `SELECT t.table_schema, t.table_name, c.relrowsecurity AS rls\n FROM (SELECT DISTINCT table_schema, table_name FROM information_schema.columns\n WHERE table_schema IN (${inList})) t\n JOIN pg_namespace n ON n.nspname = t.table_schema\n JOIN pg_class c ON c.relnamespace = n.oid AND c.relname = t.table_name`\n );\n for (const row of rows) tables.set(`${row.table_schema}.${row.table_name}`, { rls: row.rls === true });\n return tables;\n}\n\n/**\n * Whether the catalogue says RLS is on for one table, read fresh. `false` when\n * it cannot be read, which sends the caller down the fail-closed path.\n */\nasync function rlsIsOn(client: Queryable, plan: CollectionPolicyPlan): Promise<boolean> {\n try {\n const { rows } = await client.query<{ rls: boolean }>(\n `SELECT c.relrowsecurity AS rls FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE n.nspname = ${quoteSqlLiteral(plan.schema)} AND c.relname = ${quoteSqlLiteral(plan.table)}`\n );\n return rows[0]?.rls === true;\n } catch {\n return false;\n }\n}\n\n/** What one table's `ENABLE ROW LEVEL SECURITY` came to. */\ntype TableLock =\n | { locked: true }\n | { locked: false; error: string; grantWithdrawn: boolean };\n\n/**\n * Switch RLS on for one table, or — when Postgres refuses — take the user\n * role's privileges on it back, so the table is unreachable rather than\n * unprotected.\n *\n * A table the catalogue already reports RLS-on is left alone. `ENABLE` on it\n * changes nothing, and it is not free: it queues for an `ACCESS EXCLUSIVE` lock,\n * so a boot that met a long reader held every live query on that table behind\n * it — and when the wait ended in a timeout, the catch below revoked the user\n * role's privileges on a table that had been protected all along. Every\n * replica shares the grants, so one boot during a `pg_dump` closed the\n * collection for the whole deployment until the next clean boot.\n */\nasync function lockTable(\n client: Queryable,\n plan: CollectionPolicyPlan,\n rlsOn: boolean,\n run: (sql: string) => Promise<unknown> = sql => client.query(sql)\n): Promise<TableLock> {\n if (rlsOn) return { locked: true };\n try {\n await run(plan.enableRls);\n return { locked: true };\n } catch (err) {\n // The database's own answer, not the ORM's wrapper — see\n // `describeDriverError`. This is the most consequential message in\n // the file: it is what the operator reads about a table that is now\n // denying every request, and `Failed query: <the statement>` told\n // them nothing they did not already have.\n const error = describeDriverError(err);\n // A failed statement says nothing about the state it meant to reach: a\n // lock or statement timeout, a cancelled query, a peer that enabled it\n // first. Ask the catalogue. Only a table that really is RLS-off is\n // revoked — that safety is unchanged; a read that fails counts as off.\n if (await rlsIsOn(client, plan)) return { locked: true };\n // Take the privilege back rather than serve an unprotected table.\n // This is the fail-closed step the old code assumed it already had:\n // per-table, so one collection cannot take the rest of the\n // deployment down, but leaving nothing readable without RLS.\n //\n // Quoted, like every other statement here: unquoted, Postgres folds\n // the name to lower case, so a mixed-case adopted table (`\"User\"`)\n // was revoked as `public.user` — which fails, and a table that could\n // have been closed refused the boot instead.\n return withdrawGrant(client, plan, error);\n }\n}\n\n/**\n * Take the user role's privileges on a table back, so an RLS-off table is\n * unreachable rather than unprotected. `REVOKE` waits for no table lock a\n * reader holds, so it runs as a plain statement.\n */\nasync function withdrawGrant(client: Queryable, plan: CollectionPolicyPlan, error: string): Promise<TableLock> {\n try {\n await client.query(revokeUserRoleSql(plan.schema, plan.table));\n return { locked: false, error, grantWithdrawn: true };\n } catch {\n // Reported with `grantWithdrawn: false`, which the caller escalates.\n // There is nothing else this can do to make the table safe.\n return { locked: false, error, grantWithdrawn: false };\n }\n}\n\nexport interface TableLockResult {\n /** Tables RLS is now on for. */\n locked: number;\n /** Tables it could not be switched on for. See {@link PolicyEnsureResult.unsecured}. */\n unsecured: PolicyEnsureResult[\"unsecured\"];\n}\n\n/**\n * Switch RLS on for every table the collections declare — each collection's\n * and each junction's — and do nothing else.\n *\n * The first thing `rebase db push` does once Atlas has committed. Atlas cannot\n * carry `ENABLE ROW LEVEL SECURITY` (its free tier neither plans nor reads it),\n * so a table it creates arrives with RLS off — while the user role already\n * holds DML on it through the schema's default privileges. Everything push runs\n * after Atlas can refuse: the vector, search and trigger DDL, the policies\n * themselves. Locking first means none of those failures can leave a table\n * open: RLS with no policy denies, which is a state a failed push may leave.\n *\n * A declared table that is not in the database is skipped — nothing can reach\n * a table that does not exist, and the step that creates it locks it.\n */\nexport async function lockCollectionTables(\n client: Queryable,\n collections: CollectionConfig[]\n): Promise<TableLockResult> {\n const result: TableLockResult = { locked: 0, unsecured: [] };\n for (const { plan, rls } of await declaredTablesPresent(client, collections)) {\n const lock = await lockTable(client, plan, rls);\n if (lock.locked) {\n result.locked++;\n } else {\n result.unsecured.push({ table: plan.qualified, error: lock.error, grantWithdrawn: lock.grantWithdrawn });\n }\n }\n return result;\n}\n\n/**\n * The declared tables {@link lockCollectionTables} would switch row-level\n * security on for: those in the database with it off. What `db push\n * --dry-run` lists, read the way the push's lock step reads it.\n */\nexport async function tablesRlsWouldEnable(client: Queryable, collections: CollectionConfig[]): Promise<string[]> {\n return (await declaredTablesPresent(client, collections)).filter(table => !table.rls).map(table => table.plan.qualified);\n}\n\n/** Each declared table the database has, with whether its RLS is on. */\nasync function declaredTablesPresent(\n client: Queryable,\n collections: CollectionConfig[]\n): Promise<{ plan: CollectionPolicyPlan; rls: boolean }[]> {\n const plans = planCollectionPolicies(collections);\n if (plans.length === 0) return [];\n const tables = await readTableSecurity(client, Array.from(new Set(plans.map(p => p.schema))));\n return plans.flatMap(plan => {\n const table = tables.get(plan.qualified);\n return table ? [{ plan, rls: table.rls }] : [];\n });\n}\n\n/** One declared table that exists, and what the catalogue said about it. */\ninterface TableWork {\n plan: CollectionPolicyPlan;\n rlsOn: boolean;\n orphans: string[];\n /** Whether the policy names on the table differ from the plan's. */\n rulesChanged: boolean;\n /** Whether the plan holds no generated rule of its own, so the orphans are every rule the table had. */\n declaresNoRule: boolean;\n}\n\ntype AtomicOutcome =\n | { kind: \"applied\" }\n | { kind: \"lock-timeout\"; error: string }\n | { kind: \"unsupported\" }\n | { kind: \"failed\"; error: string };\n\n/**\n * Bring the declared collections' RLS policies up to date. Returns what it did.\n *\n * Only tables that already exist are touched: the boot-time table creator runs\n * first, so anything still missing is a table this additive path did not\n * create (it creates junctions too) — one a failed boot DDL left out, or a\n * project whose schema a migration owns. Enabling RLS on a\n * non-existent table would error, so those are recorded as skipped, not failed.\n *\n * ## One transaction per table\n *\n * Each table's statements — `ENABLE` when RLS is off, every `DROP POLICY IF\n * EXISTS` / `CREATE POLICY` pair, the orphan drops — go to Postgres as one\n * string, which it runs as one transaction (see `boot-ddl`). Before, each was\n * its own autocommit: at 240 tables, 4,080 commits and 4,080 ACCESS EXCLUSIVE\n * lock acquisitions per boot, 6–22 s on a disk that really flushes. And between\n * a policy's DROP and its CREATE, every other replica evaluated the table with\n * that policy missing — a user's continuous reads of their own row came back\n * empty once per boot (measured, Postgres 18). Now a concurrent request sees the\n * old policies or the new ones, never neither.\n *\n * If the transaction fails for any reason but a lock timeout, the table is\n * applied again one statement at a time, exactly as before — so a rule that\n * cannot be created (a function a migration has not added yet) leaves the\n * table in the same deny-leaning partial state it always did, rather than\n * rolling back to the old, possibly looser, policies.\n *\n * ## Lock budget\n *\n * The transaction runs under `lock_timeout` (`boot-ddl`), so a table that a\n * long transaction, a report or a dump is holding costs live traffic one\n * second, not the statement timeout. A contended table is retried after the\n * others, with backoff, within a retry budget for the whole step; one still\n * held after that is reported in `deferred` — its transaction rolled back, so\n * it is exactly as it was before this boot — or, if RLS was off on it, closed\n * by revoking the user role's grant, as any table RLS cannot be enabled on is.\n */\nexport async function ensureCollectionPolicies(\n client: Queryable,\n collections: CollectionConfig[],\n log?: (message: string) => void,\n options: BootLockOptions = {}\n): Promise<PolicyEnsureResult> {\n const result: PolicyEnsureResult = {\n policiesApplied: 0, tablesSecured: 0, unchanged: 0, skipped: [], failures: [], unsecured: [],\n orphansDropped: 0, deferred: [], rulesDropped: []\n };\n\n const plans = planCollectionPolicies(collections);\n if (plans.length === 0) return result;\n const lock = resolveBootLockOptions(options);\n\n const schemas = Array.from(new Set(plans.map(p => p.schema)));\n const tables = await readTableSecurity(client, schemas);\n const livePolicies = await readLivePolicies(client, schemas);\n\n const work: TableWork[] = [];\n for (const plan of plans) {\n const table = tables.get(plan.qualified);\n if (!table) {\n result.skipped.push({\n table: plan.qualified,\n reason: \"table is not present in the database; create it with `rebase db push` / `rebase db migrate`\"\n });\n continue;\n }\n const live = livePolicies.get(plan.qualified) ?? new Map<string, LivePolicy>();\n const present = [...live.keys()];\n const orphans = orphansOf(plan, present);\n // Already exactly what the plan would leave: nothing to run, no lock to take.\n if (table.rls && orphans.length === 0 && policiesAreCurrent(plan, live)) {\n result.tablesSecured++;\n result.unchanged++;\n continue;\n }\n const presentSet = new Set(present);\n const rulesChanged = orphans.length > 0 || [...plannedPolicyNames(plan)].some(name => !presentSet.has(name));\n const declaresNoRule = ![...plannedPolicyNames(plan)].some(name => isGeneratedPolicyName(name, plan.table));\n work.push({ plan, rlsOn: table.rls, orphans, rulesChanged, declaresNoRule });\n }\n if (result.unchanged > 0) {\n log?.(`${result.unchanged} table(s) already have exactly the declared policies; left as they are`);\n }\n\n // Until the handle refuses a multi-statement string (an extended-protocol\n // driver), after which every table goes one statement at a time.\n let atomic = true;\n let retryBudget = lock.retryBudgetMs;\n let queue = work;\n let lastError = new Map<TableWork, string>();\n\n for (let attempt = 1; queue.length > 0; attempt++) {\n if (attempt > 1) {\n if (attempt > lock.attempts || retryBudget <= 0) break;\n await sleep(lock.backoffMs(attempt - 1));\n }\n const contended: TableWork[] = [];\n const errors = new Map<TableWork, string>();\n for (const table of queue) {\n if (attempt > 1 && retryBudget <= 0) {\n contended.push(table);\n errors.set(table, lastError.get(table) ?? \"\");\n continue;\n }\n if (atomic) {\n const outcome = await applyAtomically(client, table, lock);\n if (outcome.kind === \"applied\") {\n record(result, table, log);\n continue;\n }\n if (outcome.kind === \"lock-timeout\") {\n if (attempt > 1) retryBudget -= lock.lockTimeoutMs;\n contended.push(table);\n errors.set(table, outcome.error);\n continue;\n }\n if (outcome.kind === \"unsupported\") {\n atomic = false;\n } else {\n log?.(`${table.plan.qualified}: applying its policies in one transaction failed ` +\n `(${outcome.error}); applying them one statement at a time`);\n }\n }\n await applyEachStatement(client, table, result, log, atomic ? lock : undefined);\n }\n queue = contended;\n lastError = errors;\n }\n\n // Still contended after every attempt the budget allowed.\n for (const table of queue) {\n await giveUp(client, table, lastError.get(table) ?? \"\", lock, result);\n }\n\n return result;\n}\n\n/**\n * The table's statements, in the order they must run — the stamp last, so it\n * reads back the policies this same transaction created.\n */\nconst statementsOf = (table: TableWork): string[] => {\n const stamp = policyStampSql(table.plan);\n return [\n ...(table.rlsOn ? [] : [table.plan.enableRls]),\n ...table.plan.policyStatements,\n ...table.orphans.map(name => dropPolicySql(table.plan, name)),\n ...(stamp ? [stamp] : [])\n ];\n};\n\nasync function applyAtomically(\n client: Queryable,\n table: TableWork,\n lock: ResolvedBootLockOptions\n): Promise<AtomicOutcome> {\n const statements = statementsOf(table);\n if (statements.length === 0) return { kind: \"applied\" };\n try {\n await client.query(atomicBatch(statements, lock.lockTimeoutMs));\n return { kind: \"applied\" };\n } catch (err) {\n if (isLockTimeout(err)) return { kind: \"lock-timeout\", error: describeDriverError(err) };\n if (isMultiStatementRefusal(err)) return { kind: \"unsupported\" };\n return { kind: \"failed\", error: describeDriverError(err) };\n }\n}\n\n/**\n * The warning a boot gives for one entry of {@link PolicyEnsureResult.rulesDropped}:\n * each policy, what it means for the table, and the likely cause.\n */\nexport function describeRulesDropped(dropped: { table: string; policies: string[] }): string {\n return `🔐 [rls] \"${dropped.table}\" declares no securityRules, so the policies its rules had made were dropped: ` +\n `${dropped.policies.join(\", \")}. Only administrators reach its rows now, on every server sharing this database. ` +\n \"If its collection came in an array handed to createRebaseBackend, that array lacked the defaultSecurityRules \" +\n \"its config/collections/index.ts declares: pass collectionsDir, or the array that index exports, which carries \" +\n \"them. If the rules were removed on purpose, this is that change.\";\n}\n\n/** The orphans just dropped, when they were every rule the table had. See {@link PolicyEnsureResult.rulesDropped}. */\nfunction recordRulesDropped(result: PolicyEnsureResult, table: TableWork): void {\n if (table.declaresNoRule && table.orphans.length > 0) {\n result.rulesDropped.push({ table: table.plan.qualified, policies: [...table.orphans].sort() });\n }\n}\n\nfunction record(result: PolicyEnsureResult, table: TableWork, log?: (message: string) => void): void {\n const created = table.plan.policyStatements.filter(isCreatePolicy).length;\n result.tablesSecured++;\n result.policiesApplied += created;\n result.orphansDropped += table.orphans.length;\n recordRulesDropped(result, table);\n log?.(`${table.plan.qualified}: RLS enabled, ${created} policy(ies) applied` +\n (table.orphans.length > 0 ? `, ${table.orphans.length} orphan(s) dropped` : \"\"));\n}\n\n/**\n * The table applied one statement at a time — the path every table took before\n * the per-table transaction, kept as its fallback so a failure ends exactly\n * where it always did.\n *\n * Each statement still carries the lock budget when the handle can take it.\n */\nasync function applyEachStatement(\n client: Queryable,\n table: TableWork,\n result: PolicyEnsureResult,\n log: ((message: string) => void) | undefined,\n lock: ResolvedBootLockOptions | undefined\n): Promise<void> {\n const { plan } = table;\n const run = (sql: string) => client.query(lock ? atomicBatch([sql], lock.lockTimeoutMs) : sql);\n\n // Enabling RLS is its own step, because its failure is the opposite of\n // every other failure here. Once RLS is on, anything that goes wrong\n // afterwards leaves the table denying; while it is off, the grant made\n // earlier in boot leaves the table wide open. Sharing one `try` meant\n // the dangerous case was reported in the safe case's words.\n const locked = await lockTable(client, plan, table.rlsOn, run);\n if (!locked.locked) {\n result.unsecured.push({ table: plan.qualified, error: locked.error, grantWithdrawn: locked.grantWithdrawn });\n return;\n }\n result.tablesSecured++;\n\n try {\n let created = 0;\n for (const statement of plan.policyStatements) {\n await run(statement);\n if (isCreatePolicy(statement)) {\n result.policiesApplied++;\n created++;\n }\n }\n for (const name of table.orphans) {\n await run(dropPolicySql(plan, name));\n result.orphansDropped++;\n }\n recordRulesDropped(result, table);\n log?.(`${plan.qualified}: RLS enabled, ${created} policy(ies) applied` +\n (table.orphans.length > 0 ? `, ${table.orphans.length} orphan(s) dropped` : \"\"));\n } catch (err) {\n result.failures.push({\n table: plan.qualified,\n error: describeDriverError(err)\n });\n return;\n }\n\n // Only once every policy is in. A stamp that cannot be written costs the\n // next boot a re-apply of this table — what every boot did before stamps —\n // so it is reported, not failed.\n const stamp = policyStampSql(plan);\n if (!stamp) return;\n try {\n await run(stamp);\n } catch (err) {\n log?.(`${plan.qualified}: policies applied, but not stamped (${describeDriverError(err)}); ` +\n \"the next boot applies them again\");\n }\n}\n\n/**\n * A table whose lock was held for every attempt. Its transaction rolled back\n * each time, so it is exactly as it was before this boot.\n *\n * With RLS on, that is a safe state to leave it in — reported, not failed. With\n * RLS off it is an open table over the schema-wide grant, so it is closed the\n * way any table RLS cannot be enabled on is: the catalogue is asked once more,\n * and only a table that really is RLS-off has the user role's grant withdrawn.\n */\nasync function giveUp(\n client: Queryable,\n table: TableWork,\n lastError: string,\n lock: ResolvedBootLockOptions,\n result: PolicyEnsureResult\n): Promise<void> {\n const { plan } = table;\n const error =\n `another session held the table's lock for every attempt (lock_timeout ${lock.lockTimeoutMs} ms` +\n `${lastError ? `: ${lastError.split(\"\\n\")[0]}` : \"\"}) — a long transaction, a report or a dump`;\n if (table.rlsOn || await rlsIsOn(client, plan)) {\n result.tablesSecured++;\n result.deferred.push({ table: plan.qualified, error, rulesChanged: table.rulesChanged });\n return;\n }\n const closed = await withdrawGrant(client, plan, error);\n if (!closed.locked) {\n result.unsecured.push({ table: plan.qualified, error: closed.error, grantWithdrawn: closed.grantWithdrawn });\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
__createRequire(import.meta.url);
|
|
3
3
|
import { i as __exportAll } from "./cli-prompt-DXG0yk-h.js";
|
|
4
|
-
import {
|
|
5
|
-
import { n as extractPgError } from "./pg-error-utils-
|
|
4
|
+
import { rt as extractCauseMessage } from "./cli-errors-DsYyNYzd.js";
|
|
5
|
+
import { n as extractPgError } from "./pg-error-utils-DhiUTlVx.js";
|
|
6
6
|
import { s as vectorExtensionHint } from "./vector-index-DsQ3V2d2.js";
|
|
7
7
|
import { n as planSchema, r as quoteSqlLiteral } from "./plan-schema-Czf3q4-Q.js";
|
|
8
8
|
import { c as planJunctionTables } from "./generate-postgres-ddl-logic-DPl1oH_z.js";
|
|
9
9
|
import { r as quoteRelation } from "./introspect-db-queries-DevUrzll.js";
|
|
10
|
-
import { a as assertSafeIdentifier, c as helperFunctionKey, i as HELPER_FUNCTIONS, l as columnTypeDriftMessage, n as buildDeferredIndexes, o as describeHelperFunction, s as diffPlanAgainstCatalogue } from "./deferred-index-build-
|
|
11
|
-
import { a as isLockTimeout, l as sleep, o as isMultiStatementRefusal, r as atomicBatch, s as joinStatements } from "./boot-ddl-
|
|
10
|
+
import { a as assertSafeIdentifier, c as helperFunctionKey, i as HELPER_FUNCTIONS, l as columnTypeDriftMessage, n as buildDeferredIndexes, o as describeHelperFunction, s as diffPlanAgainstCatalogue } from "./deferred-index-build-CqaOv1Xx.js";
|
|
11
|
+
import { a as isLockTimeout, l as sleep, o as isMultiStatementRefusal, r as atomicBatch, s as joinStatements } from "./boot-ddl-EsgqDkx0.js";
|
|
12
12
|
import { isConcurrentDdlRace, isDuplicateObjectRace, logger } from "@rebasepro/server";
|
|
13
13
|
import { REBASE_SCHEMA, declaredDatabaseExtensions } from "@rebasepro/types";
|
|
14
14
|
import { getTableName, relationalCollections } from "@rebasepro/common";
|
|
@@ -529,4 +529,4 @@ async function applyAction(client, action, lockBudget) {
|
|
|
529
529
|
//#endregion
|
|
530
530
|
export { readSchemaFactsFor as a, readExistingSchema as i, ensure_collection_tables_exports as n, planCollectionSchemaEnsure as r, describeDriverError as t };
|
|
531
531
|
|
|
532
|
-
//# sourceMappingURL=ensure-collection-tables-
|
|
532
|
+
//# sourceMappingURL=ensure-collection-tables-CFIRq8ni.js.map
|
package/dist/{ensure-collection-tables-Cet4650m.js.map → ensure-collection-tables-CFIRq8ni.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ensure-collection-tables-Cet4650m.js","names":[],"sources":["../src/schema/ensure-collection-tables.ts"],"sourcesContent":["/**\n * Applying a schema plan to a database that is already running.\n *\n * ## What this module is\n *\n * A managed runtime boots someone else's compiled project against a database it\n * has never seen. Auth tables are ensured at boot already, but collection tables\n * were not created by anything: the platform ran the app and every `/api/data/*`\n * request answered 500 on a missing relation. `rebase db push` cannot help — it\n * is an Atlas-driven CLI command, and the runtime image ships no CLI.\n *\n * Two halves, and only one of them is here. *What the collections ask for* is\n * `schema/plan/plan-schema.ts`, shared with `db push` and the generated Drizzle\n * file; *what the database already has, and which of those statements are safe\n * to run against it* is `schema/plan/diff-plan.ts`. This module reads the\n * catalogue, runs the statements, and reports what happened.\n *\n * ## Why additive-only, forever\n *\n * This runs unattended, against a database with customers' data in it, with no\n * human reading a diff. So it may only ever do things that cannot lose data.\n * See `diff-plan.ts` for the whole of that argument.\n */\nimport {\n declaredDatabaseExtensions,\n REBASE_SCHEMA,\n type CollectionConfig\n} from \"@rebasepro/types\";\nimport { getTableName, relationalCollections } from \"@rebasepro/common\";\nimport { logger, isConcurrentDdlRace, isDuplicateObjectRace } from \"@rebasepro/server\";\nimport { SEARCH_STAMP_PREFIX } from \"./search-column\";\nimport { vectorExtensionHint } from \"./vector-index\";\nimport { planJunctionTables, quoteSqlLiteral } from \"./generate-postgres-ddl-logic\";\nimport { quoteRelation } from \"./introspect-db-queries\";\nimport { planSchema } from \"./plan/plan-schema\";\nimport type { SchemaPlan } from \"./plan/types\";\nimport {\n assertSafeIdentifier,\n describeHelperFunction,\n diffPlanAgainstCatalogue,\n helperFunctionKey,\n HELPER_FUNCTIONS,\n type ConstraintPolicy,\n type DeferredIndexBuild,\n type DiffOptions,\n type EnsureAction,\n type EnsurePlan,\n type ExistingSchema,\n type LegacyForeignKey,\n type OrphanedRequiredColumn,\n type SearchColumnDrift,\n type WithheldConstraint\n} from \"./plan/diff-plan\";\nimport { extractCauseMessage, extractPgError } from \"../utils/pg-error-utils\";\nimport { atomicBatch, isLockTimeout, isMultiStatementRefusal, joinStatements, sleep } from \"./boot-ddl\";\nimport {\n buildDeferredIndexes,\n TABLE_BUSY_REASON,\n type DeferredIndexOutcome,\n type OpenIndexBuildSession\n} from \"./deferred-index-build\";\nimport { columnTypeDriftMessage, type ColumnTypeDrift } from \"./column-type-drift\";\n\nexport type {\n ConstraintPolicy,\n DeferredIndexBuild,\n EnsureAction,\n EnsurePlan,\n ExistingSchema,\n LegacyForeignKey,\n OrphanedRequiredColumn,\n SearchColumnDrift,\n WithheldConstraint\n};\n\n/**\n * The subset of a database handle this needs: run a statement, get rows back.\n *\n * Deliberately parameterless. Everything here is DDL or catalogue reads keyed by\n * schema name, and schema names are identifiers — they cannot be bound as\n * parameters anyway. They are validated by `assertSafeIdentifier` before they\n * reach a statement, so a config that somehow carried a quote is refused rather\n * than concatenated.\n */\nexport interface Queryable {\n query<T = unknown>(sql: string): Promise<{ rows: T[] }>;\n}\n\nexport interface EnsureOptions extends DiffOptions {\n /**\n * Server extensions the project's databases gave Rebase leave to install —\n * `declaredDatabaseExtensions()`. Absent means none, which is a refusal and\n * is the right default for a planner given no configuration at all.\n *\n * Explicit rather than read from the resource registry in here, because the\n * planner is pure and several callers plan against fixtures: reaching for a\n * process-wide registry would make the plan depend on whatever some other\n * module happened to import. `ensureCollectionTables` is the boundary that\n * reads the world.\n */\n databaseExtensions?: readonly string[];\n /**\n * Run each statement that changes an existing table under this\n * `lock_timeout` (ms), retrying a timeout with backoff — see\n * `schema/boot-ddl`. The boot sets it; absent, statements wait on locks for\n * as long as the connection's `statement_timeout` allows, as the live schema\n * editor's confirmed changes do.\n */\n lockTimeoutMs?: number;\n /**\n * Apply the statements that build tables this run creates — and the enum\n * types they use — in transactions of up to {@link NEW_TABLE_BATCH}\n * statements instead of one autocommit each. The boot sets it; see\n * `ensureCollectionTables`.\n */\n batchNewTables?: boolean;\n /**\n * Leave the indexes a live table is missing (or holds INVALID) to the\n * caller, in {@link EnsureOutcome.deferredIndexes}, instead of building\n * them before returning. The boot sets it and builds them in the\n * background once it serves — see `deferred-index-build`. A UNIQUE one is\n * built before returning either way: see {@link DeferredIndexBuild.unique}.\n */\n deferIndexBuilds?: boolean;\n /**\n * The connection those builds run on when they are not deferred. Absent,\n * they share `client` — fine for a small table, and cut short by the\n * pool's `statement_timeout` on a large one, which is why the boot hands\n * over a dedicated connection.\n */\n openIndexBuildSession?: OpenIndexBuildSession;\n}\n\n/**\n * Most statements in one new-table transaction. Bounds how many locks one\n * transaction holds — each new table is several relations (the heap, its TOAST\n * table, its key index) — well inside the default `max_locks_per_transaction`.\n */\nconst NEW_TABLE_BATCH = 100;\n\nexport interface EnsureOutcome extends EnsurePlan {\n /**\n * Actions that could not be applied and are non-fatal by nature.\n *\n * Two kinds qualify. A foreign key can only fail on data that already\n * violates it, and the column it would police exists either way, so the\n * collection still serves; refusing to boot over one would turn a\n * pre-existing data problem into an outage. A column comment is the search\n * fingerprint, which needs table ownership — losing it costs drift\n * detection on the next boot, not the deployment. Both are reported loudly.\n */\n failures: { kind: EnsureAction[\"kind\"]; target: string; error: string }[];\n /**\n * What happened to the indexes built here rather than left to the caller —\n * every one of {@link EnsurePlan.deferredIndexes}, or under\n * `deferIndexBuilds` the UNIQUE ones only; absent when there were none.\n * An index on a table that already exists reports here, not in\n * {@link failures}.\n */\n indexBuilds?: DeferredIndexOutcome;\n}\n\n/** The schema a collection lives in, validated before it reaches any DDL. */\nfunction schemaOf(collection: CollectionConfig): string {\n const schema = (collection as { schema?: string }).schema || \"public\";\n return assertSafeIdentifier(schema, \"schema name\");\n}\n\n/**\n * Decide what to add. Pure — the caller supplies what exists and runs the\n * result.\n *\n * A plan and a diff: the collections are read once, by `planSchema`, into the\n * same {@link SchemaPlan} `db push` renders `schema.sql` from, and the diff\n * subtracts what the database already has. Before that split this function had\n * its own reading of every `Property`, and the audit found twelve places where\n * it disagreed with the two generators — a required `author_id` that was\n * NOT NULL after a push and nullable after a boot among them, on the one path\n * with no developer in the loop.\n */\nexport function planCollectionSchemaEnsure(\n allCollections: CollectionConfig[],\n existing: ExistingSchema,\n options: EnsureOptions = {}\n): EnsurePlan {\n const plan = planSchema(allCollections, { databaseExtensions: options.databaseExtensions });\n return diffPlanAgainstCatalogue(plan, existing, { constraints: options.constraints });\n}\n\nexport interface ReadSchemaOptions {\n /**\n * The existing tables worth probing for rows, given every table's columns.\n *\n * Whether a table holds rows decides one thing: whether a NOT NULL or a\n * UNIQUE can be added with a column *being added* to it. So the boot asks\n * only about tables gaining a column — none at all on a boot that changes\n * nothing — rather than probing every table in the schema, adopted and\n * unmanaged ones included, on every boot. A table that is not probed is\n * reported as holding rows: the answer that never plans a constraint that\n * could fail against data. Absent, every table is probed.\n */\n probeTables?: (tables: ReadonlyMap<string, ReadonlySet<string>>) => Iterable<string>;\n}\n\n/** Read what the database has, for the schemas the collections live in. */\nexport async function readExistingSchema(\n client: Queryable,\n schemas: string[],\n options: ReadSchemaOptions = {}\n): Promise<ExistingSchema> {\n const tables = new Map<string, Set<string>>();\n const enums = new Set<string>();\n if (schemas.length === 0) return { tables, enums };\n\n const inList = schemas\n .map(schema => `'${assertSafeIdentifier(schema, \"schema name\")}'`)\n .join(\", \");\n\n const notNullColumns = new Set<string>();\n // `udt_name` rather than `data_type`: `data_type` collapses every array to\n // the literal string `ARRAY` and every extension type to `USER-DEFINED`,\n // which cannot be compared with anything. `udt_name` names the actual type\n // — `int4`, `jsonb`, `_numeric`, `vector` — which is what the drift check\n // needs.\n const columnTypes = new Map<string, string>();\n // A NOT NULL column that has a DEFAULT is not a problem for a write that\n // omits it, so orphan detection needs the default as well as the nullability\n // — otherwise every `created_at DEFAULT now()` on a table whose collection\n // does not declare it would be reported as a blocker.\n const columnDefaults = new Set<string>();\n const { rows: columns } = await client.query<{\n table_schema: string;\n table_name: string;\n column_name: string;\n is_nullable: string;\n udt_name: string | null;\n column_default: string | null;\n numeric_precision: number | null;\n numeric_scale: number | null;\n }>(\n // `numeric_precision`/`numeric_scale` because `udt_name` is `numeric`\n // for both `NUMERIC` and `NUMERIC(10, 2)`, and a property that declares\n // a precision means it: money stored in an unbounded column keeps the\n // third decimal the rounding was supposed to remove. Only carried for\n // `numeric`, where the modifier changes what a value *is*; a `varchar`\n // width is a limit on the same family and `typesAgree` ignores it.\n `SELECT table_schema, table_name, column_name, is_nullable, udt_name, column_default,\n numeric_precision, numeric_scale\n FROM information_schema.columns\n WHERE table_schema IN (${inList})`\n );\n for (const row of columns) {\n const key = `${row.table_schema}.${row.table_name}`;\n if (!tables.has(key)) tables.set(key, new Set());\n tables.get(key)!.add(row.column_name);\n if (row.is_nullable === \"NO\") notNullColumns.add(`${key}.${row.column_name}`);\n if (row.udt_name) {\n const modifier = row.udt_name === \"numeric\" && row.numeric_precision !== null\n ? `(${row.numeric_precision},${row.numeric_scale ?? 0})`\n : \"\";\n columnTypes.set(`${key}.${row.column_name}`, `${row.udt_name}${modifier}`);\n }\n if (row.column_default !== null) columnDefaults.add(`${key}.${row.column_name}`);\n }\n\n // Trigger names, so `autoValue: \"on_update\"` is installed once rather than\n // re-issued on every boot. `tgisinternal` excludes the ones Postgres itself\n // creates to enforce foreign keys.\n const triggers = new Set<string>();\n const { rows: triggerRows } = await client.query<{ schema: string; table: string; name: string }>(\n `SELECT n.nspname AS schema, c.relname AS table, t.tgname AS name\n FROM pg_trigger t\n JOIN pg_class c ON t.tgrelid = c.oid\n JOIN pg_namespace n ON c.relnamespace = n.oid\n WHERE NOT t.tgisinternal AND n.nspname IN (${inList})`\n );\n for (const row of triggerRows) triggers.add(`${row.schema}.${row.table}.${row.name}`);\n\n // Which tables hold rows. This is the only fact that decides whether a\n // NOT NULL can be added without reading the data, so it is worth a query.\n //\n // `reltuples` would be cheaper and is wrong for this: it is a planner\n // estimate, it is -1 on a table that has never been analyzed, and a table\n // that was full an hour ago still reads as full after a DELETE. A wrong\n // \"empty\" here means a boot that aborts on a constraint violation, so the\n // estimate is not good enough. `EXISTS … LIMIT 1` stops at the first row,\n // which makes the true cost one page read per table.\n //\n // Restricted to ordinary and partitioned tables: `information_schema.columns`\n // also lists views and materialized views, and probing those runs whatever\n // query defines them.\n //\n // Indexes come back from the same read: an index already in the catalogue\n // under its planned name is not planned again (see `diffPlanAgainstCatalogue`).\n const populatedTables = new Set<string>();\n const indexes = new Set<string>();\n const { rows: relations } = await client.query<{ schema: string; name: string; kind: string }>(\n `SELECT n.nspname AS schema, c.relname AS name, c.relkind::text AS kind\n FROM pg_class c\n JOIN pg_namespace n ON c.relnamespace = n.oid\n WHERE c.relkind IN ('r', 'p', 'i', 'I') AND n.nspname IN (${inList})`\n );\n const allRealTables = relations.filter(row => row.kind === \"r\" || row.kind === \"p\");\n for (const row of relations) {\n if (row.kind === \"i\" || row.kind === \"I\") indexes.add(`${row.schema}.${row.name}`);\n }\n // Only the tables the caller will ask about; every other one reads as\n // holding rows — see `ReadSchemaOptions.probeTables`.\n const wanted = options.probeTables ? new Set(options.probeTables(tables)) : undefined;\n const realTables = wanted\n ? allRealTables.filter(row => wanted.has(`${row.schema}.${row.name}`))\n : allRealTables;\n if (wanted) {\n for (const row of allRealTables) {\n const key = `${row.schema}.${row.name}`;\n if (!wanted.has(key)) populatedTables.add(key);\n }\n }\n // The names come from `pg_class`, not from the configuration, so on an\n // adopted database they include tables nobody declared — `2024_archive`,\n // `order-items`, `Sales Data`. Those are quoted, not refused: refusing a\n // name here refuses the boot over a table Rebase will never touch.\n if (realTables.length > 0) {\n const probes = realTables.map(row =>\n `SELECT ${quoteSqlLiteral(`${row.schema}.${row.name}`)} AS key, ` +\n `EXISTS(SELECT 1 FROM ${quoteRelation(row.schema, row.name)} LIMIT 1) AS populated`\n );\n const { rows: populationRows } = await client.query<{ key: string; populated: boolean }>(\n probes.join(\" UNION ALL \")\n );\n for (const row of populationRows) {\n if (row.populated) populatedTables.add(row.key);\n }\n }\n\n const enumValues = new Map<string, string[]>();\n const { rows: enumValueRows } = await client.query<{\n schema: string;\n name: string;\n value: string;\n }>(\n // Ordered by `enumsortorder`, not by label: an enum's order is part of\n // its meaning (it is what `<` compares), and reading it back sorted\n // alphabetically would make a correct type look drifted.\n `SELECT n.nspname AS schema, t.typname AS name, e.enumlabel AS value\n FROM pg_enum e\n JOIN pg_type t ON e.enumtypid = t.oid\n JOIN pg_namespace n ON t.typnamespace = n.oid\n WHERE n.nspname IN (${inList})\n ORDER BY t.typname, e.enumsortorder`\n );\n for (const row of enumValueRows) {\n const key = `${row.schema}.${row.name}`;\n if (!enumValues.has(key)) enumValues.set(key, []);\n enumValues.get(key)!.push(row.value);\n }\n\n const { rows: enumRows } = await client.query<{ schema: string; name: string }>(\n `SELECT n.nspname AS schema, t.typname AS name\n FROM pg_type t\n JOIN pg_namespace n ON t.typnamespace = n.oid\n WHERE t.typtype = 'e' AND n.nspname IN (${inList})`\n );\n for (const row of enumRows) enums.add(`${row.schema}.${row.name}`);\n\n // `ADD CONSTRAINT` has no IF NOT EXISTS, so an existing foreign key is\n // skipped by name rather than guarded in SQL.\n const constraints = new Set<string>();\n const { rows: constraintRows } = await client.query<{\n schema: string;\n table: string;\n name: string;\n }>(\n `SELECT n.nspname AS schema, c.relname AS table, con.conname AS name\n FROM pg_constraint con\n JOIN pg_class c ON con.conrelid = c.oid\n JOIN pg_namespace n ON c.relnamespace = n.oid\n WHERE n.nspname IN (${inList})`\n );\n for (const row of constraintRows) constraints.add(`${row.schema}.${row.table}.${row.name}`);\n\n // Indexes a `CREATE INDEX CONCURRENTLY` left INVALID. They are in\n // `indexes` under their name, so without this a husk reads as built and\n // `IF NOT EXISTS` keeps skipping it — see `deferred-index-build`.\n const invalidIndexes = new Set<string>();\n const { rows: invalidRows } = await client.query<{ schema: string; name: string }>(\n `SELECT n.nspname AS schema, c.relname AS name\n FROM pg_index i\n JOIN pg_class c ON c.oid = i.indexrelid\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE NOT i.indisvalid AND n.nspname IN (${inList})`\n );\n for (const row of invalidRows) invalidIndexes.add(`${row.schema}.${row.name}`);\n\n // Column comments, which is where a generated search column records the\n // expression it was built from. `objsubid > 0` is what makes a row a\n // *column* comment rather than the table's own.\n const columnComments = new Map<string, string>();\n const { rows: commentRows } = await client.query<{\n schema: string;\n table: string;\n column: string;\n comment: string | null;\n }>(\n `SELECT n.nspname AS schema, c.relname AS table, a.attname AS column, d.description AS comment\n FROM pg_description d\n JOIN pg_class c ON d.objoid = c.oid\n JOIN pg_namespace n ON c.relnamespace = n.oid\n JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = d.objsubid\n WHERE d.objsubid > 0 AND n.nspname IN (${inList})`\n );\n for (const row of commentRows) {\n if (row.comment == null) continue;\n columnComments.set(`${row.schema}.${row.table}.${row.column}`, row.comment);\n }\n\n // The helper functions the plan `CREATE OR REPLACE`s, as the catalogue has\n // them, so an unchanged one is not rewritten on every boot. Looked up by\n // qualified name rather than by the schemas read above: `set_updated_at`\n // lives in `rebase`, which is auth's schema and not in that list.\n const functions = new Map<string, string>();\n const helperNames = HELPER_FUNCTIONS\n .map(qualified => qualified.split(\".\"))\n .map(([schema, name]) => `(${quoteSqlLiteral(schema)}, ${quoteSqlLiteral(name)})`)\n .join(\", \");\n const { rows: functionRows } = await client.query<{\n schema: string;\n name: string;\n args: string;\n returns: string;\n language: string;\n volatility: string;\n strict: boolean;\n parallel: string;\n security_definer: boolean;\n leakproof: boolean;\n cost: number | string;\n has_config: boolean;\n body: string;\n }>(\n `SELECT n.nspname AS schema, p.proname AS name, pg_get_function_identity_arguments(p.oid) AS args,\n format_type(p.prorettype, NULL) AS returns, l.lanname AS language,\n p.provolatile::text AS volatility, p.proisstrict AS strict, p.proparallel::text AS parallel,\n p.prosecdef AS security_definer, p.proleakproof AS leakproof, p.procost AS cost,\n p.proconfig IS NOT NULL AS has_config, p.prosrc AS body\n FROM pg_proc p\n JOIN pg_namespace n ON p.pronamespace = n.oid\n JOIN pg_language l ON p.prolang = l.oid\n WHERE (n.nspname, p.proname) IN (${helperNames})`\n );\n for (const row of functionRows) {\n functions.set(helperFunctionKey(row.schema, row.name, row.args), describeHelperFunction({\n returns: row.returns,\n language: row.language,\n volatility: row.volatility,\n strict: row.strict,\n parallel: row.parallel,\n securityDefiner: row.security_definer,\n leakproof: row.leakproof,\n cost: Number(row.cost),\n hasConfig: row.has_config,\n body: row.body\n }));\n }\n\n const extensions = new Set<string>();\n const { rows: extensionRows } = await client.query<{ name: string }>(\"SELECT extname AS name FROM pg_extension\");\n for (const row of extensionRows) extensions.add(row.name);\n\n return {\n tables, enums, constraints, columnComments, enumValues, notNullColumns, populatedTables, columnTypes,\n columnDefaults, triggers, indexes, invalidIndexes, extensions, functions\n };\n}\n\n/**\n * The planned tables that exist and lack at least one planned column — the\n * only ones whose emptiness the diff consults (a NOT NULL or UNIQUE arriving\n * with a new column). A superset, never a subset: a column the plan renames\n * from a legacy name reads as missing here too.\n */\nfunction tablesGainingColumns(plan: SchemaPlan, catalogue: ReadonlyMap<string, ReadonlySet<string>>): string[] {\n const gaining: string[] = [];\n for (const table of plan.tables) {\n const columns = catalogue.get(table.qualified);\n if (columns && table.columns.some(column => !columns.has(column.column))) gaining.push(table.qualified);\n }\n return gaining;\n}\n\n/**\n * What to tell an operator whose `search` block no longer matches its column.\n *\n * Every line here is doing work: naming the collection is not enough, because\n * the symptom (a search that finds nothing) points at the data, not the schema;\n * and the remediation has to be exact, because it is a table rewrite the\n * operator is being asked to schedule rather than discover.\n */\nfunction searchDriftMessage(drift: SearchColumnDrift[]): string {\n const blocks = drift.map(d =>\n ` \"${d.table}\".\"${d.column}\" was generated from a different \\`search\\` block ` +\n `(recorded ${d.found}, current ${d.expected}).\\n` +\n d.rebuild.map(s => ` ${s}`).join(\"\\n\")\n );\n return (\n \"The `search` block changed after its generated column was created, and Postgres cannot alter a \" +\n \"generated expression in place.\\n\" +\n \"Rebase will not rebuild it for you: dropping and re-adding a STORED generated column rewrites the whole \" +\n \"table under an ACCESS EXCLUSIVE lock and rebuilds its GIN index, which is an outage this unattended path \" +\n \"may not schedule on your behalf.\\n\" +\n \"Until it is rebuilt the column keeps indexing the previous fields, weights and language — searches for \" +\n \"anything added since return nothing, which reads from outside as \\\"no such row\\\".\\n\" +\n \"Run these (or revert the block to what the column was built from), then boot again:\\n\" +\n blocks.join(\"\\n\") +\n \"\\n The GIN index is dropped with the column and recreated concurrently on the next boot.\"\n );\n}\n\n\n/**\n * Read what the database looks like, for the schemas a set of collections\n * lives in.\n *\n * The same read `ensureCollectionTables` does at boot, exposed on its own for\n * the callers that want to *plan* against a real database without changing it —\n * the live schema editor, which has to tell somebody what a change would do\n * before they agree to it.\n */\nexport async function readSchemaFactsFor(\n client: Queryable,\n collections: CollectionConfig[]\n): Promise<ExistingSchema> {\n const relational = relationalCollections(collections);\n const junctions = planJunctionTables(relational);\n const schemas = Array.from(new Set([\n ...relational.map(schemaOf),\n ...junctions.map(junction => junction.schema)\n ]));\n // Row probes for the declared tables only: the classifier asks about the\n // collections it is given, never about a table nobody declared.\n const declared = [\n ...relational.map(collection => `${schemaOf(collection)}.${getTableName(collection)}`),\n ...junctions.map(junction => `${junction.schema}.${junction.table}`)\n ];\n return readExistingSchema(client, schemas, { probeTables: () => declared });\n}\n\n/**\n * Bring the database up to date. Returns what it did.\n *\n * Each statement runs on its own rather than in one transaction: they are all\n * independently safe and idempotent, and a single failure (an enum label that\n * cannot be added, say) should not roll back the tables that were created fine.\n * The error is surfaced with the statement that caused it.\n */\nexport async function ensureCollectionTables(\n client: Queryable,\n collections: CollectionConfig[],\n log?: (message: string) => void,\n options: EnsureOptions = {}\n): Promise<EnsureOutcome> {\n // This is the boundary, so this is where the world is read: the planner\n // itself takes the permission as an argument and never reaches for the\n // registry. By the time boot gets here `loadBundleResourceGraph` has\n // evaluated the project's `resources.ts`, so the declarations are there —\n // and a caller that already knows them can still say so.\n const schema = planSchema(collections, {\n databaseExtensions: options.databaseExtensions ?? declaredDatabaseExtensions()\n });\n\n // Junctions live alongside the collections that declare them, so their\n // schema has to be read too — otherwise an existing junction reads as\n // missing and its constraints as unplanned. Taken off the plan, which lists\n // both kinds of table.\n const schemas = Array.from(new Set(schema.tables.map(table => table.schema)));\n for (const name of schemas) {\n assertSafeIdentifier(name, \"schema name\");\n if (name !== \"public\") {\n await client.query(`CREATE SCHEMA IF NOT EXISTS \"${name}\";`);\n }\n }\n // `rebase` is where the `updated_at` trigger's function lives. Every real\n // boot has the schema by the time this runs (auth ensures it first) and\n // `db push` creates it in `schema.sql`, but this path is also driven by the\n // live schema editor and by tests, where \"the function's schema happens to\n // exist already\" is not a thing to rely on. Only when something needs it,\n // so a project with no trigger issues no statement — and not added to the\n // read set: what is *in* `rebase` is auth's business, not this planner's.\n if (schema.tables.some(table => table.triggers.length > 0)) {\n await client.query(`CREATE SCHEMA IF NOT EXISTS \"${REBASE_SCHEMA}\";`);\n }\n\n const existing = await readExistingSchema(client, schemas, {\n probeTables: catalogue => tablesGainingColumns(schema, catalogue)\n });\n // Every index on a table that already exists goes to the index builder,\n // which this function runs or hands to the caller — see `buildIndexes`.\n const plan = diffPlanAgainstCatalogue(schema, existing, { constraints: options.constraints, deferLiveIndexes: true });\n const failures: EnsureOutcome[\"failures\"] = [];\n\n // Reported, not warned: this is a rename the ensure is about to perform, and\n // the operator should be able to see in the log why a column changed name.\n // The interesting case is the one that no longer happens — before this,\n // ensure added the new column empty beside the populated old one, every\n // statement succeeded, and the relation read the empty one.\n for (const legacy of plan.legacyForeignKeys) {\n const message =\n `Renaming \"${legacy.table}\".\"${legacy.legacy}\" to \"${legacy.expected}\". The old name is ` +\n \"the one Rebase derived for this relation before it singularized properly; the column \" +\n \"keeps its data, indexes and constraints. To keep the old name instead, set \" +\n `\\`localKey: \"${legacy.legacy}\"\\` on the relation and this will stop.`;\n logger.info(`[schema] ${message}`);\n log?.(message);\n }\n\n // Before anything is applied: a `search` block that changed after its column\n // was generated cannot be honoured by an additive plan, and serving the old\n // index while the config describes a new one is the silent failure this\n // check exists to end. Refusing is the loud half — boot is fatal on purpose\n // (see `ensureCollectionSchema` in the server's boot) and the message\n // carries the exact statements that resolve it.\n if (plan.searchDrift.length > 0) {\n throw new Error(searchDriftMessage(plan.searchDrift));\n }\n\n // Said once per column, at the moment the stamp is applied: from here on a\n // change is detected, but whether *this* column matches the block it is\n // being stamped with is not knowable — it predates the stamp.\n for (const adopted of plan.searchAdopted) {\n const message =\n `Adopting the existing generated column \"${adopted.table}\".\"${adopted.column}\" and recording what the ` +\n \"current `search` block would generate. Any later change to that block will be detected and refused; a \" +\n \"change made *before* this version was deployed cannot be, so if search has been missing content, \" +\n `rebuild the column once: ALTER TABLE \"${adopted.table.split(\".\").join('\".\"')}\" DROP COLUMN \"${adopted.column}\"; and boot again.`;\n logger.info(`[schema] ${message}`);\n log?.(message);\n }\n\n // Said once per column, every boot: an unindexed vector column and an\n // indexed one behave identically apart from latency, so the only way anyone\n // learns which one they have is if the boot says so.\n for (const skip of plan.vectorIndexSkipped) {\n const message = `No ANN index on \"${skip.table}\".\"${skip.column}\": ${skip.reason}`;\n logger.warn(`[schema] ${message}`);\n log?.(message);\n }\n\n // Said once per column, every boot, because the alternative is what this\n // whole feature exists to end: a column the configuration calls required,\n // sitting there nullable, with every surface reporting success. The boot\n // does not fail over it — the column is usable and the data is intact — but\n // it stops being invisible.\n for (const withheld of plan.withheldConstraints) {\n const message =\n `No ${withheld.kind === \"unique\" ? \"UNIQUE\" : \"NOT NULL\"} on \"${withheld.target}\": ` +\n `${withheld.reason} ${withheld.remedy}`;\n logger.warn(`[schema] ${message}`);\n log?.(message);\n }\n\n // Said once per drifted column, every boot, and this is the one report here\n // that describes a system which currently *looks* healthy. Everything else\n // in this function explains a statement that did not run; this explains a\n // statement that was never planned, on a deploy that reported success, and\n // whose first symptom is a write rejected long afterwards.\n for (const drift of plan.columnTypeDrift) {\n const message = columnTypeDriftMessage(drift);\n logger.warn(`[schema] ${message}`);\n log?.(message);\n }\n\n // Said once per column, every boot, and it matters more than the rest here\n // because the table is not merely imperfect — it is unwritable, and the\n // error the API returns names a field the author has already deleted.\n // This is what a renamed property looks like to an additive provisioner.\n for (const orphan of plan.orphanedRequiredColumns) {\n const [schema, table] = orphan.table.split(\".\");\n const message =\n `\"${orphan.table}\".\"${orphan.column}\" is NOT NULL with no default, and the \"${orphan.slug}\" ` +\n \"collection no longer declares it. Every insert will be rejected for a field you cannot set \" +\n `(PG_23502, naming \"${orphan.column}\"). This is what a renamed property looks like: the new ` +\n \"column was added, and the old one cannot be dropped unattended. Fix it with ONE of:\\n\" +\n ` ALTER TABLE \"${schema}\".\"${table}\" DROP COLUMN \"${orphan.column}\"; -- the rename is complete\\n` +\n ` ALTER TABLE \"${schema}\".\"${table}\" ALTER COLUMN \"${orphan.column}\" DROP NOT NULL; -- keep the data, stop requiring it\\n` +\n \" ...or declare the property again, if the column is still wanted.\";\n logger.warn(`[schema] ${message}`);\n log?.(message);\n }\n\n // The indexes of tables that already exist. Built here unless the caller\n // defers them — and a UNIQUE one is built here regardless, before this\n // returns, so nothing serves the collection without the promise it makes.\n const builtHere = options.deferIndexBuilds\n ? plan.deferredIndexes.filter(index => index.unique === true)\n : plan.deferredIndexes;\n const leftToCaller = options.deferIndexBuilds\n ? plan.deferredIndexes.filter(index => index.unique !== true)\n : plan.deferredIndexes;\n\n // After every action, so the columns they index exist — whether this run\n // added them or a previous one did.\n const buildIndexes = async (): Promise<DeferredIndexOutcome | undefined> => {\n if (builtHere.length === 0) return undefined;\n const outcome = await buildDeferredIndexes(\n options.openIndexBuildSession ?? (async () => ({\n query: <T>(sql: string) => client.query<T>(sql),\n dedicated: false,\n release: () => undefined\n })),\n builtHere,\n { log, lockTimeoutMs: options.lockTimeoutMs }\n );\n assertUniqueIndexesBuilt(builtHere, outcome);\n return outcome;\n };\n const finish = async (): Promise<EnsureOutcome> => {\n const indexBuilds = await buildIndexes();\n return { ...plan, deferredIndexes: leftToCaller, failures, indexBuilds };\n };\n\n if (plan.actions.length === 0) {\n if (plan.deferredIndexes.length === 0) log?.(\"Schema is up to date; nothing to create.\");\n return finish();\n }\n\n const lockBudget = options.lockTimeoutMs !== undefined\n ? { lockTimeoutMs: options.lockTimeoutMs, atomic: true }\n : undefined;\n // Whether the handle takes a multi-statement string; shared with the lock\n // budget, which needs the same thing.\n const multi = lockBudget ?? { atomic: true };\n\n // A table this run creates has no rows, no readers and no writers, so its\n // statements — the CREATE, the ENABLE, every column, its triggers, the\n // indexes on it — can share a transaction: one commit instead of one per\n // statement. A first deploy of 200 collections was 3,767 autocommits, 11.7 s\n // on a disk that really flushes. If a batch fails for any reason, its\n // statements are applied again one at a time below, exactly as before, so\n // the races and survivable failures that path handles end the same way.\n const createdTables = new Set(plan.actions.filter(a => a.kind === \"create-table\").map(a => a.target));\n const batchable = (action: EnsureAction): boolean =>\n options.batchNewTables === true &&\n multi.atomic &&\n (action.kind === \"create-enum\" || createdTables.has(action.target.split(\".\").slice(0, 2).join(\".\"))) &&\n action.kind !== \"add-enum-value\" &&\n !CONCURRENTLY_RE.test(action.sql);\n\n const applyBatch = async (group: EnsureAction[]): Promise<boolean> => {\n const statements = group.map(action => action.sql);\n try {\n await client.query(lockBudget\n ? atomicBatch(statements, lockBudget.lockTimeoutMs)\n : joinStatements(statements));\n return true;\n } catch (err) {\n if (isMultiStatementRefusal(err)) {\n multi.atomic = false;\n } else {\n log?.(`${group.length} statement(s) for new tables failed as one transaction ` +\n `(${describeDriverError(err).split(\"\\n\")[0]}); applying them one at a time`);\n }\n return false;\n }\n };\n\n const applyOne = async (action: EnsureAction): Promise<void> => {\n try {\n if (await applyAction(client, action, lockBudget)) {\n log?.(`${action.kind}: ${action.target}`);\n } else {\n log?.(`${action.kind}: ${action.target} (already created by a peer)`);\n }\n } catch (err) {\n const message = describeDriverError(err);\n // A foreign key is the only action that can fail on the customer's\n // data rather than on the schema. The column it polices is already\n // there, so the collection serves either way — record it and carry\n // on rather than crash-looping the deployment.\n //\n // A comment is metadata about a column that was just created\n // successfully, and it can only fail on ownership (COMMENT requires\n // owning the table, which an adopted table may not grant). Losing\n // the stamp costs drift detection on the next boot; it must not cost\n // the deployment.\n //\n // An index that is not UNIQUE is an optimization, and losing an\n // optimization must not cost the deployment. This was the difference\n // between a slow collection and a tenant that would not boot: on a\n // managed runtime the throw killed the process, the pod never went\n // ready, and the deploy reported a 300-second readiness timeout — for\n // an ANN index whose absence changes latency and nothing else.\n //\n // UNIQUE is excluded because it is not an optimization. It is the\n // collection declaring that two rows cannot share a value, and\n // serving without it is serving a promise the database is not\n // keeping.\n const survivable =\n action.kind === \"add-constraint\" ||\n action.kind === \"comment-column\" ||\n (action.kind === \"create-index\" && !/CREATE\\s+UNIQUE\\s+INDEX/i.test(action.sql));\n if (survivable) {\n failures.push({ kind: action.kind, target: action.target, error: message });\n return;\n }\n const lockHint = isLockTimeout(err)\n ? \"\\n hint: another session held this table's lock for every attempt (a long transaction, a \" +\n \"report or a dump). Nothing was changed; boot again once it has finished.\"\n : \"\";\n throw new Error(\n `Failed to ${action.kind} ${action.target}: ${message}${vectorExtensionHint(message)}${lockHint}\\n ${action.sql}`\n );\n }\n };\n\n for (let i = 0; i < plan.actions.length;) {\n let end = i;\n while (end < plan.actions.length && end - i < NEW_TABLE_BATCH && batchable(plan.actions[end])) end++;\n if (end - i > 1 && await applyBatch(plan.actions.slice(i, end))) {\n for (const action of plan.actions.slice(i, end)) log?.(`${action.kind}: ${action.target}`);\n i = end;\n continue;\n }\n const last = Math.max(end, i + 1);\n for (; i < last; i++) await applyOne(plan.actions[i]);\n }\n return finish();\n}\n\n/**\n * Fail the ensure for a UNIQUE index on a live table that could not be built,\n * exactly as a failed UNIQUE `create-index` action always has (see `applyOne`).\n *\n * \"Another index on the table is being built\" counts: the table's lock is\n * held by a build that may run for minutes, and serving the collection\n * meanwhile would be serving a promise the database is not keeping. Another\n * instance building *this* index does not: once its build commits the\n * constraint holds, which is also what a peer's `IF NOT EXISTS` race meant.\n */\nfunction assertUniqueIndexesBuilt(indexes: readonly DeferredIndexBuild[], outcome: DeferredIndexOutcome): void {\n for (const index of indexes) {\n if (index.unique !== true) continue;\n const qualified = `${index.schema}.${index.indexName}`;\n const reason = outcome.failed.find(failure => failure.index === qualified)?.error\n ?? outcome.skipped.find(skip => skip.index === qualified && skip.reason === TABLE_BUSY_REASON)?.reason;\n if (reason === undefined) continue;\n throw new Error(`Failed to create-index ${index.schema}.${index.table}: ${qualified}: ${reason}\\n ${index.sql}`);\n }\n}\n\n/**\n * What actually went wrong, rather than what the ORM said about it.\n *\n * Drizzle wraps every driver error, and its `message` is literally\n * ``Failed query: ${sql}\\nparams: ${params}`` — the statement we already know,\n * and not one word of Postgres's answer. Reporting `err.message` therefore\n * produced a failure that named the statement twice and the *reason* zero\n * times, so four unrelated boot failures — a missing extension, a wrong\n * privilege, a bad index method, a constraint the rows violate — all read\n * identically to whoever had to fix them.\n *\n * Two consequences, both observed in the field before this existed:\n *\n * 1. On a managed deployment the reason never reaches anybody. Boot dies, the\n * pod never goes ready, and the deploy reports a readiness timeout — so the\n * one line that says why is the one line that was thrown away here.\n * 2. {@link vectorExtensionHint} matches on the error text. Against the\n * wrapper it matched nothing, so the hint written specifically to explain a\n * missing pgvector never fired on the path that raises it.\n *\n * `detail` and `hint` come along because they are the fields Postgres uses to\n * say which object it meant and what to do about it — `CREATE EXTENSION` names\n * the missing library there, and a rejected constraint names the row. This\n * writes into the tenant's own boot log, about the tenant's own database.\n */\nexport function describeDriverError(err: unknown): string {\n const pg = extractPgError(err);\n if (pg) {\n const parts = [pg.code ? `${pg.message} [${pg.code}]` : pg.message];\n if (pg.detail) parts.push(` detail: ${pg.detail}`);\n if (pg.hint) parts.push(` hint: ${pg.hint}`);\n return parts.join(\"\\n\");\n }\n // No SQLSTATE anywhere in the chain: not a database answer at all — a\n // socket that closed, a bug in this file. The deepest cause still beats the\n // wrapper, and the wrapper still beats nothing.\n const cause = extractCauseMessage(err);\n if (cause) return cause;\n return err instanceof Error ? err.message : String(err);\n}\n\n/** ` CONCURRENTLY ` as it appears in a rendered `CREATE [UNIQUE] INDEX`. */\nconst CONCURRENTLY_RE = /\\sCONCURRENTLY\\s/i;\n\n/**\n * `25001 active_sql_transaction` — \"cannot run inside a transaction block\".\n *\n * Only ever raised here by `CREATE INDEX CONCURRENTLY`, and only when the\n * connection we were handed is inside a transaction. Deliberately narrow: it\n * decides to retry a *different* statement, so it must not fire on anything it\n * has not been reasoned about.\n */\nfunction isTransactionBlockRefusal(err: unknown): boolean {\n return extractPgError(err)?.code === \"25001\";\n}\n\n/** Attempts per action, including the first. Matches the server's bootstraps. */\nconst DDL_ATTEMPTS = 4;\n\n/**\n * Attempts at an action whose table lock is held, including the first.\n *\n * Unlike a policy, a schema change cannot be deferred — the collection's code\n * expects the column — so a lock that never frees still fails the boot, as a\n * statement timeout did. Ten one-second attempts with backoff between them wait\n * about as long as the old 30 s statement timeout did, but a live query on that\n * table now waits at most one second at a time instead of thirty.\n */\nconst LOCK_ATTEMPTS = 10;\n\n/**\n * Whether an action may be sent as `SET LOCAL lock_timeout; <statement>` — one\n * implicit transaction. Not `CREATE INDEX CONCURRENTLY`, which Postgres refuses\n * inside any transaction block (and which waits on no lock a reader holds), and\n * not `ALTER TYPE … ADD VALUE`, which Postgres before 12 refuses there too and\n * which locks a type, not a table.\n */\nconst takesLockBudget = (action: EnsureAction, sql: string): boolean =>\n action.kind !== \"add-enum-value\" && !CONCURRENTLY_RE.test(sql);\n\n/**\n * Run one planned statement, surviving a simultaneous boot.\n *\n * Every statement in a plan is written to be idempotent, and that is not the\n * same as being safe to run concurrently: `CREATE … IF NOT EXISTS` reads the\n * catalog and then writes to it as two steps, so peers starting together both\n * see \"absent\" and the loser gets a duplicate key on a *catalog* index. Measured\n * against Postgres 18: five instances, 8 of 10 calls lost. `CREATE TYPE` is\n * worse, because Postgres has no `IF NOT EXISTS` for it at all.\n *\n * What made that fatal here rather than merely noisy is the loop this sits in.\n * A losing statement threw, and the throw abandoned **every remaining action in\n * the plan** — so a replica that lost one race came up missing tables it never\n * attempted, and the boot log blamed the one statement that failed.\n *\n * @returns `true` if this process applied the statement, `false` if a peer had\n * already created the object. The distinction is only for the log; both mean\n * the object is now there.\n * @throws the original error for anything that is not a race — a syntax error, a\n * permission failure, a unique constraint the customer's own rows violate.\n */\nasync function applyAction(\n client: Queryable,\n action: EnsureAction,\n lockBudget?: { lockTimeoutMs: number; atomic: boolean }\n): Promise<boolean> {\n let sqlText = action.sql;\n let lockAttempts = 0;\n for (let attempt = 1; ; attempt++) {\n const budgeted = lockBudget?.atomic === true && takesLockBudget(action, sqlText);\n try {\n await client.query(budgeted ? atomicBatch([sqlText], lockBudget.lockTimeoutMs) : sqlText);\n return true;\n } catch (err) {\n // A handle that takes one statement per query (extended protocol)\n // cannot carry the `SET LOCAL`. Nothing ran; say it once and send\n // every statement as it is from here on.\n if (budgeted && isMultiStatementRefusal(err)) {\n lockBudget.atomic = false;\n attempt--;\n continue;\n }\n // Something held the table for longer than the budget — a long\n // transaction, a report, a dump. The statement did nothing; let\n // the queue behind it drain, then ask again.\n if (budgeted && isLockTimeout(err) && ++lockAttempts < LOCK_ATTEMPTS) {\n logger.debug(\n `[schema] ${action.kind} ${action.target}: the table's lock is held by another session ` +\n `(attempt ${lockAttempts}/${LOCK_ATTEMPTS}) — retrying`\n );\n attempt--;\n await sleep(Math.min(500 * lockAttempts, 2_000) * (1 + Math.random()));\n continue;\n }\n // `CREATE INDEX CONCURRENTLY` is the one statement here Postgres\n // refuses inside a transaction block (25001), and whether we are in\n // one is not ours to know: the client is whatever handle the caller\n // bootstrapped with, and an application that builds its own adapter\n // can hand us a connection already inside a transaction. The plain\n // form is always correct and never refused — it takes a write lock\n // for the build, which is the cost of getting the index at all.\n if (isTransactionBlockRefusal(err) && CONCURRENTLY_RE.test(sqlText)) {\n sqlText = sqlText.replace(CONCURRENTLY_RE, \" \");\n logger.warn(\n `[schema] ${action.kind} ${action.target}: this connection is inside a transaction, ` +\n \"so the index is being built with a write lock rather than concurrently. Writes to \" +\n \"this table block until it finishes.\"\n );\n continue;\n }\n // Already there. Not \"retry\" — the end state this statement wanted\n // is the end state the database is in, so carry on to the next\n // action rather than spending three more attempts proving it.\n if (isDuplicateObjectRace(err)) {\n logger.debug(\n `[schema] ${action.kind} ${action.target}: already created by another instance`\n );\n return false;\n }\n // Retryable but not yet satisfied — a deadlock between two boots\n // taking catalog locks in step. The statement did nothing; run it\n // again after a jittered pause so peers that collided once do not\n // collide again in lockstep.\n if (isConcurrentDdlRace(err) && attempt < DDL_ATTEMPTS) {\n logger.debug(\n `[schema] ${action.kind} ${action.target}: lost a race with another instance ` +\n `(attempt ${attempt}/${DDL_ATTEMPTS}) — retrying`\n );\n await new Promise(resolve => setTimeout(resolve, 40 * attempt * (1 + Math.random())));\n continue;\n }\n throw err;\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ensure-collection-tables-CFIRq8ni.js","names":[],"sources":["../src/schema/ensure-collection-tables.ts"],"sourcesContent":["/**\n * Applying a schema plan to a database that is already running.\n *\n * ## What this module is\n *\n * A managed runtime boots someone else's compiled project against a database it\n * has never seen. Auth tables are ensured at boot already, but collection tables\n * were not created by anything: the platform ran the app and every `/api/data/*`\n * request answered 500 on a missing relation. `rebase db push` cannot help — it\n * is an Atlas-driven CLI command, and the runtime image ships no CLI.\n *\n * Two halves, and only one of them is here. *What the collections ask for* is\n * `schema/plan/plan-schema.ts`, shared with `db push` and the generated Drizzle\n * file; *what the database already has, and which of those statements are safe\n * to run against it* is `schema/plan/diff-plan.ts`. This module reads the\n * catalogue, runs the statements, and reports what happened.\n *\n * ## Why additive-only, forever\n *\n * This runs unattended, against a database with customers' data in it, with no\n * human reading a diff. So it may only ever do things that cannot lose data.\n * See `diff-plan.ts` for the whole of that argument.\n */\nimport {\n declaredDatabaseExtensions,\n REBASE_SCHEMA,\n type CollectionConfig\n} from \"@rebasepro/types\";\nimport { getTableName, relationalCollections } from \"@rebasepro/common\";\nimport { logger, isConcurrentDdlRace, isDuplicateObjectRace } from \"@rebasepro/server\";\nimport { SEARCH_STAMP_PREFIX } from \"./search-column\";\nimport { vectorExtensionHint } from \"./vector-index\";\nimport { planJunctionTables, quoteSqlLiteral } from \"./generate-postgres-ddl-logic\";\nimport { quoteRelation } from \"./introspect-db-queries\";\nimport { planSchema } from \"./plan/plan-schema\";\nimport type { SchemaPlan } from \"./plan/types\";\nimport {\n assertSafeIdentifier,\n describeHelperFunction,\n diffPlanAgainstCatalogue,\n helperFunctionKey,\n HELPER_FUNCTIONS,\n type ConstraintPolicy,\n type DeferredIndexBuild,\n type DiffOptions,\n type EnsureAction,\n type EnsurePlan,\n type ExistingSchema,\n type LegacyForeignKey,\n type OrphanedRequiredColumn,\n type SearchColumnDrift,\n type WithheldConstraint\n} from \"./plan/diff-plan\";\nimport { extractCauseMessage, extractPgError } from \"../utils/pg-error-utils\";\nimport { atomicBatch, isLockTimeout, isMultiStatementRefusal, joinStatements, sleep } from \"./boot-ddl\";\nimport {\n buildDeferredIndexes,\n TABLE_BUSY_REASON,\n type DeferredIndexOutcome,\n type OpenIndexBuildSession\n} from \"./deferred-index-build\";\nimport { columnTypeDriftMessage, type ColumnTypeDrift } from \"./column-type-drift\";\n\nexport type {\n ConstraintPolicy,\n DeferredIndexBuild,\n EnsureAction,\n EnsurePlan,\n ExistingSchema,\n LegacyForeignKey,\n OrphanedRequiredColumn,\n SearchColumnDrift,\n WithheldConstraint\n};\n\n/**\n * The subset of a database handle this needs: run a statement, get rows back.\n *\n * Deliberately parameterless. Everything here is DDL or catalogue reads keyed by\n * schema name, and schema names are identifiers — they cannot be bound as\n * parameters anyway. They are validated by `assertSafeIdentifier` before they\n * reach a statement, so a config that somehow carried a quote is refused rather\n * than concatenated.\n */\nexport interface Queryable {\n query<T = unknown>(sql: string): Promise<{ rows: T[] }>;\n}\n\nexport interface EnsureOptions extends DiffOptions {\n /**\n * Server extensions the project's databases gave Rebase leave to install —\n * `declaredDatabaseExtensions()`. Absent means none, which is a refusal and\n * is the right default for a planner given no configuration at all.\n *\n * Explicit rather than read from the resource registry in here, because the\n * planner is pure and several callers plan against fixtures: reaching for a\n * process-wide registry would make the plan depend on whatever some other\n * module happened to import. `ensureCollectionTables` is the boundary that\n * reads the world.\n */\n databaseExtensions?: readonly string[];\n /**\n * Run each statement that changes an existing table under this\n * `lock_timeout` (ms), retrying a timeout with backoff — see\n * `schema/boot-ddl`. The boot sets it; absent, statements wait on locks for\n * as long as the connection's `statement_timeout` allows, as the live schema\n * editor's confirmed changes do.\n */\n lockTimeoutMs?: number;\n /**\n * Apply the statements that build tables this run creates — and the enum\n * types they use — in transactions of up to {@link NEW_TABLE_BATCH}\n * statements instead of one autocommit each. The boot sets it; see\n * `ensureCollectionTables`.\n */\n batchNewTables?: boolean;\n /**\n * Leave the indexes a live table is missing (or holds INVALID) to the\n * caller, in {@link EnsureOutcome.deferredIndexes}, instead of building\n * them before returning. The boot sets it and builds them in the\n * background once it serves — see `deferred-index-build`. A UNIQUE one is\n * built before returning either way: see {@link DeferredIndexBuild.unique}.\n */\n deferIndexBuilds?: boolean;\n /**\n * The connection those builds run on when they are not deferred. Absent,\n * they share `client` — fine for a small table, and cut short by the\n * pool's `statement_timeout` on a large one, which is why the boot hands\n * over a dedicated connection.\n */\n openIndexBuildSession?: OpenIndexBuildSession;\n}\n\n/**\n * Most statements in one new-table transaction. Bounds how many locks one\n * transaction holds — each new table is several relations (the heap, its TOAST\n * table, its key index) — well inside the default `max_locks_per_transaction`.\n */\nconst NEW_TABLE_BATCH = 100;\n\nexport interface EnsureOutcome extends EnsurePlan {\n /**\n * Actions that could not be applied and are non-fatal by nature.\n *\n * Two kinds qualify. A foreign key can only fail on data that already\n * violates it, and the column it would police exists either way, so the\n * collection still serves; refusing to boot over one would turn a\n * pre-existing data problem into an outage. A column comment is the search\n * fingerprint, which needs table ownership — losing it costs drift\n * detection on the next boot, not the deployment. Both are reported loudly.\n */\n failures: { kind: EnsureAction[\"kind\"]; target: string; error: string }[];\n /**\n * What happened to the indexes built here rather than left to the caller —\n * every one of {@link EnsurePlan.deferredIndexes}, or under\n * `deferIndexBuilds` the UNIQUE ones only; absent when there were none.\n * An index on a table that already exists reports here, not in\n * {@link failures}.\n */\n indexBuilds?: DeferredIndexOutcome;\n}\n\n/** The schema a collection lives in, validated before it reaches any DDL. */\nfunction schemaOf(collection: CollectionConfig): string {\n const schema = (collection as { schema?: string }).schema || \"public\";\n return assertSafeIdentifier(schema, \"schema name\");\n}\n\n/**\n * Decide what to add. Pure — the caller supplies what exists and runs the\n * result.\n *\n * A plan and a diff: the collections are read once, by `planSchema`, into the\n * same {@link SchemaPlan} `db push` renders `schema.sql` from, and the diff\n * subtracts what the database already has. Before that split this function had\n * its own reading of every `Property`, and the audit found twelve places where\n * it disagreed with the two generators — a required `author_id` that was\n * NOT NULL after a push and nullable after a boot among them, on the one path\n * with no developer in the loop.\n */\nexport function planCollectionSchemaEnsure(\n allCollections: CollectionConfig[],\n existing: ExistingSchema,\n options: EnsureOptions = {}\n): EnsurePlan {\n const plan = planSchema(allCollections, { databaseExtensions: options.databaseExtensions });\n return diffPlanAgainstCatalogue(plan, existing, { constraints: options.constraints });\n}\n\nexport interface ReadSchemaOptions {\n /**\n * The existing tables worth probing for rows, given every table's columns.\n *\n * Whether a table holds rows decides one thing: whether a NOT NULL or a\n * UNIQUE can be added with a column *being added* to it. So the boot asks\n * only about tables gaining a column — none at all on a boot that changes\n * nothing — rather than probing every table in the schema, adopted and\n * unmanaged ones included, on every boot. A table that is not probed is\n * reported as holding rows: the answer that never plans a constraint that\n * could fail against data. Absent, every table is probed.\n */\n probeTables?: (tables: ReadonlyMap<string, ReadonlySet<string>>) => Iterable<string>;\n}\n\n/** Read what the database has, for the schemas the collections live in. */\nexport async function readExistingSchema(\n client: Queryable,\n schemas: string[],\n options: ReadSchemaOptions = {}\n): Promise<ExistingSchema> {\n const tables = new Map<string, Set<string>>();\n const enums = new Set<string>();\n if (schemas.length === 0) return { tables, enums };\n\n const inList = schemas\n .map(schema => `'${assertSafeIdentifier(schema, \"schema name\")}'`)\n .join(\", \");\n\n const notNullColumns = new Set<string>();\n // `udt_name` rather than `data_type`: `data_type` collapses every array to\n // the literal string `ARRAY` and every extension type to `USER-DEFINED`,\n // which cannot be compared with anything. `udt_name` names the actual type\n // — `int4`, `jsonb`, `_numeric`, `vector` — which is what the drift check\n // needs.\n const columnTypes = new Map<string, string>();\n // A NOT NULL column that has a DEFAULT is not a problem for a write that\n // omits it, so orphan detection needs the default as well as the nullability\n // — otherwise every `created_at DEFAULT now()` on a table whose collection\n // does not declare it would be reported as a blocker.\n const columnDefaults = new Set<string>();\n const { rows: columns } = await client.query<{\n table_schema: string;\n table_name: string;\n column_name: string;\n is_nullable: string;\n udt_name: string | null;\n column_default: string | null;\n numeric_precision: number | null;\n numeric_scale: number | null;\n }>(\n // `numeric_precision`/`numeric_scale` because `udt_name` is `numeric`\n // for both `NUMERIC` and `NUMERIC(10, 2)`, and a property that declares\n // a precision means it: money stored in an unbounded column keeps the\n // third decimal the rounding was supposed to remove. Only carried for\n // `numeric`, where the modifier changes what a value *is*; a `varchar`\n // width is a limit on the same family and `typesAgree` ignores it.\n `SELECT table_schema, table_name, column_name, is_nullable, udt_name, column_default,\n numeric_precision, numeric_scale\n FROM information_schema.columns\n WHERE table_schema IN (${inList})`\n );\n for (const row of columns) {\n const key = `${row.table_schema}.${row.table_name}`;\n if (!tables.has(key)) tables.set(key, new Set());\n tables.get(key)!.add(row.column_name);\n if (row.is_nullable === \"NO\") notNullColumns.add(`${key}.${row.column_name}`);\n if (row.udt_name) {\n const modifier = row.udt_name === \"numeric\" && row.numeric_precision !== null\n ? `(${row.numeric_precision},${row.numeric_scale ?? 0})`\n : \"\";\n columnTypes.set(`${key}.${row.column_name}`, `${row.udt_name}${modifier}`);\n }\n if (row.column_default !== null) columnDefaults.add(`${key}.${row.column_name}`);\n }\n\n // Trigger names, so `autoValue: \"on_update\"` is installed once rather than\n // re-issued on every boot. `tgisinternal` excludes the ones Postgres itself\n // creates to enforce foreign keys.\n const triggers = new Set<string>();\n const { rows: triggerRows } = await client.query<{ schema: string; table: string; name: string }>(\n `SELECT n.nspname AS schema, c.relname AS table, t.tgname AS name\n FROM pg_trigger t\n JOIN pg_class c ON t.tgrelid = c.oid\n JOIN pg_namespace n ON c.relnamespace = n.oid\n WHERE NOT t.tgisinternal AND n.nspname IN (${inList})`\n );\n for (const row of triggerRows) triggers.add(`${row.schema}.${row.table}.${row.name}`);\n\n // Which tables hold rows. This is the only fact that decides whether a\n // NOT NULL can be added without reading the data, so it is worth a query.\n //\n // `reltuples` would be cheaper and is wrong for this: it is a planner\n // estimate, it is -1 on a table that has never been analyzed, and a table\n // that was full an hour ago still reads as full after a DELETE. A wrong\n // \"empty\" here means a boot that aborts on a constraint violation, so the\n // estimate is not good enough. `EXISTS … LIMIT 1` stops at the first row,\n // which makes the true cost one page read per table.\n //\n // Restricted to ordinary and partitioned tables: `information_schema.columns`\n // also lists views and materialized views, and probing those runs whatever\n // query defines them.\n //\n // Indexes come back from the same read: an index already in the catalogue\n // under its planned name is not planned again (see `diffPlanAgainstCatalogue`).\n const populatedTables = new Set<string>();\n const indexes = new Set<string>();\n const { rows: relations } = await client.query<{ schema: string; name: string; kind: string }>(\n `SELECT n.nspname AS schema, c.relname AS name, c.relkind::text AS kind\n FROM pg_class c\n JOIN pg_namespace n ON c.relnamespace = n.oid\n WHERE c.relkind IN ('r', 'p', 'i', 'I') AND n.nspname IN (${inList})`\n );\n const allRealTables = relations.filter(row => row.kind === \"r\" || row.kind === \"p\");\n for (const row of relations) {\n if (row.kind === \"i\" || row.kind === \"I\") indexes.add(`${row.schema}.${row.name}`);\n }\n // Only the tables the caller will ask about; every other one reads as\n // holding rows — see `ReadSchemaOptions.probeTables`.\n const wanted = options.probeTables ? new Set(options.probeTables(tables)) : undefined;\n const realTables = wanted\n ? allRealTables.filter(row => wanted.has(`${row.schema}.${row.name}`))\n : allRealTables;\n if (wanted) {\n for (const row of allRealTables) {\n const key = `${row.schema}.${row.name}`;\n if (!wanted.has(key)) populatedTables.add(key);\n }\n }\n // The names come from `pg_class`, not from the configuration, so on an\n // adopted database they include tables nobody declared — `2024_archive`,\n // `order-items`, `Sales Data`. Those are quoted, not refused: refusing a\n // name here refuses the boot over a table Rebase will never touch.\n if (realTables.length > 0) {\n const probes = realTables.map(row =>\n `SELECT ${quoteSqlLiteral(`${row.schema}.${row.name}`)} AS key, ` +\n `EXISTS(SELECT 1 FROM ${quoteRelation(row.schema, row.name)} LIMIT 1) AS populated`\n );\n const { rows: populationRows } = await client.query<{ key: string; populated: boolean }>(\n probes.join(\" UNION ALL \")\n );\n for (const row of populationRows) {\n if (row.populated) populatedTables.add(row.key);\n }\n }\n\n const enumValues = new Map<string, string[]>();\n const { rows: enumValueRows } = await client.query<{\n schema: string;\n name: string;\n value: string;\n }>(\n // Ordered by `enumsortorder`, not by label: an enum's order is part of\n // its meaning (it is what `<` compares), and reading it back sorted\n // alphabetically would make a correct type look drifted.\n `SELECT n.nspname AS schema, t.typname AS name, e.enumlabel AS value\n FROM pg_enum e\n JOIN pg_type t ON e.enumtypid = t.oid\n JOIN pg_namespace n ON t.typnamespace = n.oid\n WHERE n.nspname IN (${inList})\n ORDER BY t.typname, e.enumsortorder`\n );\n for (const row of enumValueRows) {\n const key = `${row.schema}.${row.name}`;\n if (!enumValues.has(key)) enumValues.set(key, []);\n enumValues.get(key)!.push(row.value);\n }\n\n const { rows: enumRows } = await client.query<{ schema: string; name: string }>(\n `SELECT n.nspname AS schema, t.typname AS name\n FROM pg_type t\n JOIN pg_namespace n ON t.typnamespace = n.oid\n WHERE t.typtype = 'e' AND n.nspname IN (${inList})`\n );\n for (const row of enumRows) enums.add(`${row.schema}.${row.name}`);\n\n // `ADD CONSTRAINT` has no IF NOT EXISTS, so an existing foreign key is\n // skipped by name rather than guarded in SQL.\n const constraints = new Set<string>();\n const { rows: constraintRows } = await client.query<{\n schema: string;\n table: string;\n name: string;\n }>(\n `SELECT n.nspname AS schema, c.relname AS table, con.conname AS name\n FROM pg_constraint con\n JOIN pg_class c ON con.conrelid = c.oid\n JOIN pg_namespace n ON c.relnamespace = n.oid\n WHERE n.nspname IN (${inList})`\n );\n for (const row of constraintRows) constraints.add(`${row.schema}.${row.table}.${row.name}`);\n\n // Indexes a `CREATE INDEX CONCURRENTLY` left INVALID. They are in\n // `indexes` under their name, so without this a husk reads as built and\n // `IF NOT EXISTS` keeps skipping it — see `deferred-index-build`.\n const invalidIndexes = new Set<string>();\n const { rows: invalidRows } = await client.query<{ schema: string; name: string }>(\n `SELECT n.nspname AS schema, c.relname AS name\n FROM pg_index i\n JOIN pg_class c ON c.oid = i.indexrelid\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE NOT i.indisvalid AND n.nspname IN (${inList})`\n );\n for (const row of invalidRows) invalidIndexes.add(`${row.schema}.${row.name}`);\n\n // Column comments, which is where a generated search column records the\n // expression it was built from. `objsubid > 0` is what makes a row a\n // *column* comment rather than the table's own.\n const columnComments = new Map<string, string>();\n const { rows: commentRows } = await client.query<{\n schema: string;\n table: string;\n column: string;\n comment: string | null;\n }>(\n `SELECT n.nspname AS schema, c.relname AS table, a.attname AS column, d.description AS comment\n FROM pg_description d\n JOIN pg_class c ON d.objoid = c.oid\n JOIN pg_namespace n ON c.relnamespace = n.oid\n JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = d.objsubid\n WHERE d.objsubid > 0 AND n.nspname IN (${inList})`\n );\n for (const row of commentRows) {\n if (row.comment == null) continue;\n columnComments.set(`${row.schema}.${row.table}.${row.column}`, row.comment);\n }\n\n // The helper functions the plan `CREATE OR REPLACE`s, as the catalogue has\n // them, so an unchanged one is not rewritten on every boot. Looked up by\n // qualified name rather than by the schemas read above: `set_updated_at`\n // lives in `rebase`, which is auth's schema and not in that list.\n const functions = new Map<string, string>();\n const helperNames = HELPER_FUNCTIONS\n .map(qualified => qualified.split(\".\"))\n .map(([schema, name]) => `(${quoteSqlLiteral(schema)}, ${quoteSqlLiteral(name)})`)\n .join(\", \");\n const { rows: functionRows } = await client.query<{\n schema: string;\n name: string;\n args: string;\n returns: string;\n language: string;\n volatility: string;\n strict: boolean;\n parallel: string;\n security_definer: boolean;\n leakproof: boolean;\n cost: number | string;\n has_config: boolean;\n body: string;\n }>(\n `SELECT n.nspname AS schema, p.proname AS name, pg_get_function_identity_arguments(p.oid) AS args,\n format_type(p.prorettype, NULL) AS returns, l.lanname AS language,\n p.provolatile::text AS volatility, p.proisstrict AS strict, p.proparallel::text AS parallel,\n p.prosecdef AS security_definer, p.proleakproof AS leakproof, p.procost AS cost,\n p.proconfig IS NOT NULL AS has_config, p.prosrc AS body\n FROM pg_proc p\n JOIN pg_namespace n ON p.pronamespace = n.oid\n JOIN pg_language l ON p.prolang = l.oid\n WHERE (n.nspname, p.proname) IN (${helperNames})`\n );\n for (const row of functionRows) {\n functions.set(helperFunctionKey(row.schema, row.name, row.args), describeHelperFunction({\n returns: row.returns,\n language: row.language,\n volatility: row.volatility,\n strict: row.strict,\n parallel: row.parallel,\n securityDefiner: row.security_definer,\n leakproof: row.leakproof,\n cost: Number(row.cost),\n hasConfig: row.has_config,\n body: row.body\n }));\n }\n\n const extensions = new Set<string>();\n const { rows: extensionRows } = await client.query<{ name: string }>(\"SELECT extname AS name FROM pg_extension\");\n for (const row of extensionRows) extensions.add(row.name);\n\n return {\n tables, enums, constraints, columnComments, enumValues, notNullColumns, populatedTables, columnTypes,\n columnDefaults, triggers, indexes, invalidIndexes, extensions, functions\n };\n}\n\n/**\n * The planned tables that exist and lack at least one planned column — the\n * only ones whose emptiness the diff consults (a NOT NULL or UNIQUE arriving\n * with a new column). A superset, never a subset: a column the plan renames\n * from a legacy name reads as missing here too.\n */\nfunction tablesGainingColumns(plan: SchemaPlan, catalogue: ReadonlyMap<string, ReadonlySet<string>>): string[] {\n const gaining: string[] = [];\n for (const table of plan.tables) {\n const columns = catalogue.get(table.qualified);\n if (columns && table.columns.some(column => !columns.has(column.column))) gaining.push(table.qualified);\n }\n return gaining;\n}\n\n/**\n * What to tell an operator whose `search` block no longer matches its column.\n *\n * Every line here is doing work: naming the collection is not enough, because\n * the symptom (a search that finds nothing) points at the data, not the schema;\n * and the remediation has to be exact, because it is a table rewrite the\n * operator is being asked to schedule rather than discover.\n */\nfunction searchDriftMessage(drift: SearchColumnDrift[]): string {\n const blocks = drift.map(d =>\n ` \"${d.table}\".\"${d.column}\" was generated from a different \\`search\\` block ` +\n `(recorded ${d.found}, current ${d.expected}).\\n` +\n d.rebuild.map(s => ` ${s}`).join(\"\\n\")\n );\n return (\n \"The `search` block changed after its generated column was created, and Postgres cannot alter a \" +\n \"generated expression in place.\\n\" +\n \"Rebase will not rebuild it for you: dropping and re-adding a STORED generated column rewrites the whole \" +\n \"table under an ACCESS EXCLUSIVE lock and rebuilds its GIN index, which is an outage this unattended path \" +\n \"may not schedule on your behalf.\\n\" +\n \"Until it is rebuilt the column keeps indexing the previous fields, weights and language — searches for \" +\n \"anything added since return nothing, which reads from outside as \\\"no such row\\\".\\n\" +\n \"Run these (or revert the block to what the column was built from), then boot again:\\n\" +\n blocks.join(\"\\n\") +\n \"\\n The GIN index is dropped with the column and recreated concurrently on the next boot.\"\n );\n}\n\n\n/**\n * Read what the database looks like, for the schemas a set of collections\n * lives in.\n *\n * The same read `ensureCollectionTables` does at boot, exposed on its own for\n * the callers that want to *plan* against a real database without changing it —\n * the live schema editor, which has to tell somebody what a change would do\n * before they agree to it.\n */\nexport async function readSchemaFactsFor(\n client: Queryable,\n collections: CollectionConfig[]\n): Promise<ExistingSchema> {\n const relational = relationalCollections(collections);\n const junctions = planJunctionTables(relational);\n const schemas = Array.from(new Set([\n ...relational.map(schemaOf),\n ...junctions.map(junction => junction.schema)\n ]));\n // Row probes for the declared tables only: the classifier asks about the\n // collections it is given, never about a table nobody declared.\n const declared = [\n ...relational.map(collection => `${schemaOf(collection)}.${getTableName(collection)}`),\n ...junctions.map(junction => `${junction.schema}.${junction.table}`)\n ];\n return readExistingSchema(client, schemas, { probeTables: () => declared });\n}\n\n/**\n * Bring the database up to date. Returns what it did.\n *\n * Each statement runs on its own rather than in one transaction: they are all\n * independently safe and idempotent, and a single failure (an enum label that\n * cannot be added, say) should not roll back the tables that were created fine.\n * The error is surfaced with the statement that caused it.\n */\nexport async function ensureCollectionTables(\n client: Queryable,\n collections: CollectionConfig[],\n log?: (message: string) => void,\n options: EnsureOptions = {}\n): Promise<EnsureOutcome> {\n // This is the boundary, so this is where the world is read: the planner\n // itself takes the permission as an argument and never reaches for the\n // registry. By the time boot gets here `loadBundleResourceGraph` has\n // evaluated the project's `resources.ts`, so the declarations are there —\n // and a caller that already knows them can still say so.\n const schema = planSchema(collections, {\n databaseExtensions: options.databaseExtensions ?? declaredDatabaseExtensions()\n });\n\n // Junctions live alongside the collections that declare them, so their\n // schema has to be read too — otherwise an existing junction reads as\n // missing and its constraints as unplanned. Taken off the plan, which lists\n // both kinds of table.\n const schemas = Array.from(new Set(schema.tables.map(table => table.schema)));\n for (const name of schemas) {\n assertSafeIdentifier(name, \"schema name\");\n if (name !== \"public\") {\n await client.query(`CREATE SCHEMA IF NOT EXISTS \"${name}\";`);\n }\n }\n // `rebase` is where the `updated_at` trigger's function lives. Every real\n // boot has the schema by the time this runs (auth ensures it first) and\n // `db push` creates it in `schema.sql`, but this path is also driven by the\n // live schema editor and by tests, where \"the function's schema happens to\n // exist already\" is not a thing to rely on. Only when something needs it,\n // so a project with no trigger issues no statement — and not added to the\n // read set: what is *in* `rebase` is auth's business, not this planner's.\n if (schema.tables.some(table => table.triggers.length > 0)) {\n await client.query(`CREATE SCHEMA IF NOT EXISTS \"${REBASE_SCHEMA}\";`);\n }\n\n const existing = await readExistingSchema(client, schemas, {\n probeTables: catalogue => tablesGainingColumns(schema, catalogue)\n });\n // Every index on a table that already exists goes to the index builder,\n // which this function runs or hands to the caller — see `buildIndexes`.\n const plan = diffPlanAgainstCatalogue(schema, existing, { constraints: options.constraints, deferLiveIndexes: true });\n const failures: EnsureOutcome[\"failures\"] = [];\n\n // Reported, not warned: this is a rename the ensure is about to perform, and\n // the operator should be able to see in the log why a column changed name.\n // The interesting case is the one that no longer happens — before this,\n // ensure added the new column empty beside the populated old one, every\n // statement succeeded, and the relation read the empty one.\n for (const legacy of plan.legacyForeignKeys) {\n const message =\n `Renaming \"${legacy.table}\".\"${legacy.legacy}\" to \"${legacy.expected}\". The old name is ` +\n \"the one Rebase derived for this relation before it singularized properly; the column \" +\n \"keeps its data, indexes and constraints. To keep the old name instead, set \" +\n `\\`localKey: \"${legacy.legacy}\"\\` on the relation and this will stop.`;\n logger.info(`[schema] ${message}`);\n log?.(message);\n }\n\n // Before anything is applied: a `search` block that changed after its column\n // was generated cannot be honoured by an additive plan, and serving the old\n // index while the config describes a new one is the silent failure this\n // check exists to end. Refusing is the loud half — boot is fatal on purpose\n // (see `ensureCollectionSchema` in the server's boot) and the message\n // carries the exact statements that resolve it.\n if (plan.searchDrift.length > 0) {\n throw new Error(searchDriftMessage(plan.searchDrift));\n }\n\n // Said once per column, at the moment the stamp is applied: from here on a\n // change is detected, but whether *this* column matches the block it is\n // being stamped with is not knowable — it predates the stamp.\n for (const adopted of plan.searchAdopted) {\n const message =\n `Adopting the existing generated column \"${adopted.table}\".\"${adopted.column}\" and recording what the ` +\n \"current `search` block would generate. Any later change to that block will be detected and refused; a \" +\n \"change made *before* this version was deployed cannot be, so if search has been missing content, \" +\n `rebuild the column once: ALTER TABLE \"${adopted.table.split(\".\").join('\".\"')}\" DROP COLUMN \"${adopted.column}\"; and boot again.`;\n logger.info(`[schema] ${message}`);\n log?.(message);\n }\n\n // Said once per column, every boot: an unindexed vector column and an\n // indexed one behave identically apart from latency, so the only way anyone\n // learns which one they have is if the boot says so.\n for (const skip of plan.vectorIndexSkipped) {\n const message = `No ANN index on \"${skip.table}\".\"${skip.column}\": ${skip.reason}`;\n logger.warn(`[schema] ${message}`);\n log?.(message);\n }\n\n // Said once per column, every boot, because the alternative is what this\n // whole feature exists to end: a column the configuration calls required,\n // sitting there nullable, with every surface reporting success. The boot\n // does not fail over it — the column is usable and the data is intact — but\n // it stops being invisible.\n for (const withheld of plan.withheldConstraints) {\n const message =\n `No ${withheld.kind === \"unique\" ? \"UNIQUE\" : \"NOT NULL\"} on \"${withheld.target}\": ` +\n `${withheld.reason} ${withheld.remedy}`;\n logger.warn(`[schema] ${message}`);\n log?.(message);\n }\n\n // Said once per drifted column, every boot, and this is the one report here\n // that describes a system which currently *looks* healthy. Everything else\n // in this function explains a statement that did not run; this explains a\n // statement that was never planned, on a deploy that reported success, and\n // whose first symptom is a write rejected long afterwards.\n for (const drift of plan.columnTypeDrift) {\n const message = columnTypeDriftMessage(drift);\n logger.warn(`[schema] ${message}`);\n log?.(message);\n }\n\n // Said once per column, every boot, and it matters more than the rest here\n // because the table is not merely imperfect — it is unwritable, and the\n // error the API returns names a field the author has already deleted.\n // This is what a renamed property looks like to an additive provisioner.\n for (const orphan of plan.orphanedRequiredColumns) {\n const [schema, table] = orphan.table.split(\".\");\n const message =\n `\"${orphan.table}\".\"${orphan.column}\" is NOT NULL with no default, and the \"${orphan.slug}\" ` +\n \"collection no longer declares it. Every insert will be rejected for a field you cannot set \" +\n `(PG_23502, naming \"${orphan.column}\"). This is what a renamed property looks like: the new ` +\n \"column was added, and the old one cannot be dropped unattended. Fix it with ONE of:\\n\" +\n ` ALTER TABLE \"${schema}\".\"${table}\" DROP COLUMN \"${orphan.column}\"; -- the rename is complete\\n` +\n ` ALTER TABLE \"${schema}\".\"${table}\" ALTER COLUMN \"${orphan.column}\" DROP NOT NULL; -- keep the data, stop requiring it\\n` +\n \" ...or declare the property again, if the column is still wanted.\";\n logger.warn(`[schema] ${message}`);\n log?.(message);\n }\n\n // The indexes of tables that already exist. Built here unless the caller\n // defers them — and a UNIQUE one is built here regardless, before this\n // returns, so nothing serves the collection without the promise it makes.\n const builtHere = options.deferIndexBuilds\n ? plan.deferredIndexes.filter(index => index.unique === true)\n : plan.deferredIndexes;\n const leftToCaller = options.deferIndexBuilds\n ? plan.deferredIndexes.filter(index => index.unique !== true)\n : plan.deferredIndexes;\n\n // After every action, so the columns they index exist — whether this run\n // added them or a previous one did.\n const buildIndexes = async (): Promise<DeferredIndexOutcome | undefined> => {\n if (builtHere.length === 0) return undefined;\n const outcome = await buildDeferredIndexes(\n options.openIndexBuildSession ?? (async () => ({\n query: <T>(sql: string) => client.query<T>(sql),\n dedicated: false,\n release: () => undefined\n })),\n builtHere,\n { log, lockTimeoutMs: options.lockTimeoutMs }\n );\n assertUniqueIndexesBuilt(builtHere, outcome);\n return outcome;\n };\n const finish = async (): Promise<EnsureOutcome> => {\n const indexBuilds = await buildIndexes();\n return { ...plan, deferredIndexes: leftToCaller, failures, indexBuilds };\n };\n\n if (plan.actions.length === 0) {\n if (plan.deferredIndexes.length === 0) log?.(\"Schema is up to date; nothing to create.\");\n return finish();\n }\n\n const lockBudget = options.lockTimeoutMs !== undefined\n ? { lockTimeoutMs: options.lockTimeoutMs, atomic: true }\n : undefined;\n // Whether the handle takes a multi-statement string; shared with the lock\n // budget, which needs the same thing.\n const multi = lockBudget ?? { atomic: true };\n\n // A table this run creates has no rows, no readers and no writers, so its\n // statements — the CREATE, the ENABLE, every column, its triggers, the\n // indexes on it — can share a transaction: one commit instead of one per\n // statement. A first deploy of 200 collections was 3,767 autocommits, 11.7 s\n // on a disk that really flushes. If a batch fails for any reason, its\n // statements are applied again one at a time below, exactly as before, so\n // the races and survivable failures that path handles end the same way.\n const createdTables = new Set(plan.actions.filter(a => a.kind === \"create-table\").map(a => a.target));\n const batchable = (action: EnsureAction): boolean =>\n options.batchNewTables === true &&\n multi.atomic &&\n (action.kind === \"create-enum\" || createdTables.has(action.target.split(\".\").slice(0, 2).join(\".\"))) &&\n action.kind !== \"add-enum-value\" &&\n !CONCURRENTLY_RE.test(action.sql);\n\n const applyBatch = async (group: EnsureAction[]): Promise<boolean> => {\n const statements = group.map(action => action.sql);\n try {\n await client.query(lockBudget\n ? atomicBatch(statements, lockBudget.lockTimeoutMs)\n : joinStatements(statements));\n return true;\n } catch (err) {\n if (isMultiStatementRefusal(err)) {\n multi.atomic = false;\n } else {\n log?.(`${group.length} statement(s) for new tables failed as one transaction ` +\n `(${describeDriverError(err).split(\"\\n\")[0]}); applying them one at a time`);\n }\n return false;\n }\n };\n\n const applyOne = async (action: EnsureAction): Promise<void> => {\n try {\n if (await applyAction(client, action, lockBudget)) {\n log?.(`${action.kind}: ${action.target}`);\n } else {\n log?.(`${action.kind}: ${action.target} (already created by a peer)`);\n }\n } catch (err) {\n const message = describeDriverError(err);\n // A foreign key is the only action that can fail on the customer's\n // data rather than on the schema. The column it polices is already\n // there, so the collection serves either way — record it and carry\n // on rather than crash-looping the deployment.\n //\n // A comment is metadata about a column that was just created\n // successfully, and it can only fail on ownership (COMMENT requires\n // owning the table, which an adopted table may not grant). Losing\n // the stamp costs drift detection on the next boot; it must not cost\n // the deployment.\n //\n // An index that is not UNIQUE is an optimization, and losing an\n // optimization must not cost the deployment. This was the difference\n // between a slow collection and a tenant that would not boot: on a\n // managed runtime the throw killed the process, the pod never went\n // ready, and the deploy reported a 300-second readiness timeout — for\n // an ANN index whose absence changes latency and nothing else.\n //\n // UNIQUE is excluded because it is not an optimization. It is the\n // collection declaring that two rows cannot share a value, and\n // serving without it is serving a promise the database is not\n // keeping.\n const survivable =\n action.kind === \"add-constraint\" ||\n action.kind === \"comment-column\" ||\n (action.kind === \"create-index\" && !/CREATE\\s+UNIQUE\\s+INDEX/i.test(action.sql));\n if (survivable) {\n failures.push({ kind: action.kind, target: action.target, error: message });\n return;\n }\n const lockHint = isLockTimeout(err)\n ? \"\\n hint: another session held this table's lock for every attempt (a long transaction, a \" +\n \"report or a dump). Nothing was changed; boot again once it has finished.\"\n : \"\";\n throw new Error(\n `Failed to ${action.kind} ${action.target}: ${message}${vectorExtensionHint(message)}${lockHint}\\n ${action.sql}`\n );\n }\n };\n\n for (let i = 0; i < plan.actions.length;) {\n let end = i;\n while (end < plan.actions.length && end - i < NEW_TABLE_BATCH && batchable(plan.actions[end])) end++;\n if (end - i > 1 && await applyBatch(plan.actions.slice(i, end))) {\n for (const action of plan.actions.slice(i, end)) log?.(`${action.kind}: ${action.target}`);\n i = end;\n continue;\n }\n const last = Math.max(end, i + 1);\n for (; i < last; i++) await applyOne(plan.actions[i]);\n }\n return finish();\n}\n\n/**\n * Fail the ensure for a UNIQUE index on a live table that could not be built,\n * exactly as a failed UNIQUE `create-index` action always has (see `applyOne`).\n *\n * \"Another index on the table is being built\" counts: the table's lock is\n * held by a build that may run for minutes, and serving the collection\n * meanwhile would be serving a promise the database is not keeping. Another\n * instance building *this* index does not: once its build commits the\n * constraint holds, which is also what a peer's `IF NOT EXISTS` race meant.\n */\nfunction assertUniqueIndexesBuilt(indexes: readonly DeferredIndexBuild[], outcome: DeferredIndexOutcome): void {\n for (const index of indexes) {\n if (index.unique !== true) continue;\n const qualified = `${index.schema}.${index.indexName}`;\n const reason = outcome.failed.find(failure => failure.index === qualified)?.error\n ?? outcome.skipped.find(skip => skip.index === qualified && skip.reason === TABLE_BUSY_REASON)?.reason;\n if (reason === undefined) continue;\n throw new Error(`Failed to create-index ${index.schema}.${index.table}: ${qualified}: ${reason}\\n ${index.sql}`);\n }\n}\n\n/**\n * What actually went wrong, rather than what the ORM said about it.\n *\n * Drizzle wraps every driver error, and its `message` is literally\n * ``Failed query: ${sql}\\nparams: ${params}`` — the statement we already know,\n * and not one word of Postgres's answer. Reporting `err.message` therefore\n * produced a failure that named the statement twice and the *reason* zero\n * times, so four unrelated boot failures — a missing extension, a wrong\n * privilege, a bad index method, a constraint the rows violate — all read\n * identically to whoever had to fix them.\n *\n * Two consequences, both observed in the field before this existed:\n *\n * 1. On a managed deployment the reason never reaches anybody. Boot dies, the\n * pod never goes ready, and the deploy reports a readiness timeout — so the\n * one line that says why is the one line that was thrown away here.\n * 2. {@link vectorExtensionHint} matches on the error text. Against the\n * wrapper it matched nothing, so the hint written specifically to explain a\n * missing pgvector never fired on the path that raises it.\n *\n * `detail` and `hint` come along because they are the fields Postgres uses to\n * say which object it meant and what to do about it — `CREATE EXTENSION` names\n * the missing library there, and a rejected constraint names the row. This\n * writes into the tenant's own boot log, about the tenant's own database.\n */\nexport function describeDriverError(err: unknown): string {\n const pg = extractPgError(err);\n if (pg) {\n const parts = [pg.code ? `${pg.message} [${pg.code}]` : pg.message];\n if (pg.detail) parts.push(` detail: ${pg.detail}`);\n if (pg.hint) parts.push(` hint: ${pg.hint}`);\n return parts.join(\"\\n\");\n }\n // No SQLSTATE anywhere in the chain: not a database answer at all — a\n // socket that closed, a bug in this file. The deepest cause still beats the\n // wrapper, and the wrapper still beats nothing.\n const cause = extractCauseMessage(err);\n if (cause) return cause;\n return err instanceof Error ? err.message : String(err);\n}\n\n/** ` CONCURRENTLY ` as it appears in a rendered `CREATE [UNIQUE] INDEX`. */\nconst CONCURRENTLY_RE = /\\sCONCURRENTLY\\s/i;\n\n/**\n * `25001 active_sql_transaction` — \"cannot run inside a transaction block\".\n *\n * Only ever raised here by `CREATE INDEX CONCURRENTLY`, and only when the\n * connection we were handed is inside a transaction. Deliberately narrow: it\n * decides to retry a *different* statement, so it must not fire on anything it\n * has not been reasoned about.\n */\nfunction isTransactionBlockRefusal(err: unknown): boolean {\n return extractPgError(err)?.code === \"25001\";\n}\n\n/** Attempts per action, including the first. Matches the server's bootstraps. */\nconst DDL_ATTEMPTS = 4;\n\n/**\n * Attempts at an action whose table lock is held, including the first.\n *\n * Unlike a policy, a schema change cannot be deferred — the collection's code\n * expects the column — so a lock that never frees still fails the boot, as a\n * statement timeout did. Ten one-second attempts with backoff between them wait\n * about as long as the old 30 s statement timeout did, but a live query on that\n * table now waits at most one second at a time instead of thirty.\n */\nconst LOCK_ATTEMPTS = 10;\n\n/**\n * Whether an action may be sent as `SET LOCAL lock_timeout; <statement>` — one\n * implicit transaction. Not `CREATE INDEX CONCURRENTLY`, which Postgres refuses\n * inside any transaction block (and which waits on no lock a reader holds), and\n * not `ALTER TYPE … ADD VALUE`, which Postgres before 12 refuses there too and\n * which locks a type, not a table.\n */\nconst takesLockBudget = (action: EnsureAction, sql: string): boolean =>\n action.kind !== \"add-enum-value\" && !CONCURRENTLY_RE.test(sql);\n\n/**\n * Run one planned statement, surviving a simultaneous boot.\n *\n * Every statement in a plan is written to be idempotent, and that is not the\n * same as being safe to run concurrently: `CREATE … IF NOT EXISTS` reads the\n * catalog and then writes to it as two steps, so peers starting together both\n * see \"absent\" and the loser gets a duplicate key on a *catalog* index. Measured\n * against Postgres 18: five instances, 8 of 10 calls lost. `CREATE TYPE` is\n * worse, because Postgres has no `IF NOT EXISTS` for it at all.\n *\n * What made that fatal here rather than merely noisy is the loop this sits in.\n * A losing statement threw, and the throw abandoned **every remaining action in\n * the plan** — so a replica that lost one race came up missing tables it never\n * attempted, and the boot log blamed the one statement that failed.\n *\n * @returns `true` if this process applied the statement, `false` if a peer had\n * already created the object. The distinction is only for the log; both mean\n * the object is now there.\n * @throws the original error for anything that is not a race — a syntax error, a\n * permission failure, a unique constraint the customer's own rows violate.\n */\nasync function applyAction(\n client: Queryable,\n action: EnsureAction,\n lockBudget?: { lockTimeoutMs: number; atomic: boolean }\n): Promise<boolean> {\n let sqlText = action.sql;\n let lockAttempts = 0;\n for (let attempt = 1; ; attempt++) {\n const budgeted = lockBudget?.atomic === true && takesLockBudget(action, sqlText);\n try {\n await client.query(budgeted ? atomicBatch([sqlText], lockBudget.lockTimeoutMs) : sqlText);\n return true;\n } catch (err) {\n // A handle that takes one statement per query (extended protocol)\n // cannot carry the `SET LOCAL`. Nothing ran; say it once and send\n // every statement as it is from here on.\n if (budgeted && isMultiStatementRefusal(err)) {\n lockBudget.atomic = false;\n attempt--;\n continue;\n }\n // Something held the table for longer than the budget — a long\n // transaction, a report, a dump. The statement did nothing; let\n // the queue behind it drain, then ask again.\n if (budgeted && isLockTimeout(err) && ++lockAttempts < LOCK_ATTEMPTS) {\n logger.debug(\n `[schema] ${action.kind} ${action.target}: the table's lock is held by another session ` +\n `(attempt ${lockAttempts}/${LOCK_ATTEMPTS}) — retrying`\n );\n attempt--;\n await sleep(Math.min(500 * lockAttempts, 2_000) * (1 + Math.random()));\n continue;\n }\n // `CREATE INDEX CONCURRENTLY` is the one statement here Postgres\n // refuses inside a transaction block (25001), and whether we are in\n // one is not ours to know: the client is whatever handle the caller\n // bootstrapped with, and an application that builds its own adapter\n // can hand us a connection already inside a transaction. The plain\n // form is always correct and never refused — it takes a write lock\n // for the build, which is the cost of getting the index at all.\n if (isTransactionBlockRefusal(err) && CONCURRENTLY_RE.test(sqlText)) {\n sqlText = sqlText.replace(CONCURRENTLY_RE, \" \");\n logger.warn(\n `[schema] ${action.kind} ${action.target}: this connection is inside a transaction, ` +\n \"so the index is being built with a write lock rather than concurrently. Writes to \" +\n \"this table block until it finishes.\"\n );\n continue;\n }\n // Already there. Not \"retry\" — the end state this statement wanted\n // is the end state the database is in, so carry on to the next\n // action rather than spending three more attempts proving it.\n if (isDuplicateObjectRace(err)) {\n logger.debug(\n `[schema] ${action.kind} ${action.target}: already created by another instance`\n );\n return false;\n }\n // Retryable but not yet satisfied — a deadlock between two boots\n // taking catalog locks in step. The statement did nothing; run it\n // again after a jittered pause so peers that collided once do not\n // collide again in lockstep.\n if (isConcurrentDdlRace(err) && attempt < DDL_ATTEMPTS) {\n logger.debug(\n `[schema] ${action.kind} ${action.target}: lost a race with another instance ` +\n `(attempt ${attempt}/${DDL_ATTEMPTS}) — retrying`\n );\n await new Promise(resolve => setTimeout(resolve, 40 * attempt * (1 + Math.random())));\n continue;\n }\n throw err;\n }\n }\n}\n"],"mappings":"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"}
|