@rebasepro/server-postgres 0.24.0 → 0.24.1-canary.g56af4a4
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-BRt78gfa.js → BranchService-Bsn2wfaj.js} +3 -2
- package/dist/{BranchService-BRt78gfa.js.map → BranchService-Bsn2wfaj.js.map} +1 -1
- package/dist/PostgresBackendDriver.d.ts +83 -2
- package/dist/auth/ensure-tables.d.ts +12 -0
- package/dist/auth/services.d.ts +6 -0
- package/dist/backup/backup-cli.d.ts +14 -0
- package/dist/backup/backup-service.d.ts +2 -2
- package/dist/backup/pg-binary.d.ts +5 -0
- package/dist/{backup-cli-DW5p9_zv.js → backup-cli-aUfRv16h.js} +37 -29
- package/dist/backup-cli-aUfRv16h.js.map +1 -0
- package/dist/{backup-service-EwcDVG-8.js → backup-service-BTBB7RXn.js} +55 -21
- package/dist/backup-service-BTBB7RXn.js.map +1 -0
- package/dist/boot-ddl-DVpsFN6r.js +105 -0
- package/dist/boot-ddl-DVpsFN6r.js.map +1 -0
- package/dist/cli-errors-CaI4ntyD.js +1919 -0
- package/dist/cli-errors-CaI4ntyD.js.map +1 -0
- package/dist/cli-errors.d.ts +51 -5
- package/dist/{cli-flags-BglvpjHv.js → cli-flags-B-Dawocl.js} +5 -2
- package/dist/cli-flags-B-Dawocl.js.map +1 -0
- package/dist/cli-helpers.d.ts +40 -0
- package/dist/cli-output.d.ts +16 -28
- package/dist/cli-push.d.ts +87 -0
- package/dist/cli.js +563 -182
- package/dist/cli.js.map +1 -1
- package/dist/{column-plan-helpers-1-LQD0yI.js → column-plan-helpers-drZIsSNf.js} +10 -4
- package/dist/{column-plan-helpers-1-LQD0yI.js.map → column-plan-helpers-drZIsSNf.js.map} +1 -1
- package/dist/{databasePoolManager-Bj5FbeAs.js → connection-BPoNILnD.js} +19 -110
- package/dist/connection-BPoNILnD.js.map +1 -0
- package/dist/connection.d.ts +9 -1
- package/dist/databasePoolManager-BkEy2w18.js +119 -0
- package/dist/databasePoolManager-BkEy2w18.js.map +1 -0
- package/dist/{ensure-collection-tables-kkkHk8oo.js → deferred-index-build-DP1_SNFo.js} +484 -346
- package/dist/deferred-index-build-DP1_SNFo.js.map +1 -0
- package/dist/{doctor-C-sYWbmt.js → doctor-C4WiftHm.js} +63 -52
- package/dist/doctor-C4WiftHm.js.map +1 -0
- package/dist/ensure-collection-policies-CXpbeNag.js +541 -0
- package/dist/ensure-collection-policies-CXpbeNag.js.map +1 -0
- package/dist/ensure-collection-tables-BhsU6qUE.js +532 -0
- package/dist/ensure-collection-tables-BhsU6qUE.js.map +1 -0
- package/dist/{ensure-tables-BmI_tRxc.js → ensure-tables-CHsgyLqg.js} +172 -76
- package/dist/ensure-tables-CHsgyLqg.js.map +1 -0
- package/dist/enum-labels-kept-D8fI1MoX.js +56 -0
- package/dist/enum-labels-kept-D8fI1MoX.js.map +1 -0
- package/dist/generate-drizzle-schema-Bz52jH2h.js +57 -0
- package/dist/generate-drizzle-schema-Bz52jH2h.js.map +1 -0
- package/dist/{generate-drizzle-schema-logic-sSFDp6LR.js → generate-drizzle-schema-logic-B74WN37S.js} +5 -5
- package/dist/generate-drizzle-schema-logic-B74WN37S.js.map +1 -0
- package/dist/generate-files-CdEckX6c.js +199 -0
- package/dist/generate-files-CdEckX6c.js.map +1 -0
- package/dist/{generate-postgres-ddl-logic-BJsLaVNX.js → generate-postgres-ddl-logic-B64O2oZF.js} +3 -3
- package/dist/{generate-postgres-ddl-logic-BJsLaVNX.js.map → generate-postgres-ddl-logic-B64O2oZF.js.map} +1 -1
- package/dist/generated-sql.d.ts +8 -2
- package/dist/history/HistoryService.d.ts +38 -6
- package/dist/history/ensure-history-table.d.ts +42 -4
- package/dist/index.es.js +2922 -7475
- package/dist/index.es.js.map +1 -1
- package/dist/{introspect-db-logic-kCETE8TY.js → introspect-db-logic-C-Di8dA9.js} +44 -6
- package/dist/introspect-db-logic-C-Di8dA9.js.map +1 -0
- package/dist/{introspect-db-queries-C_Q5VgQw.js → introspect-db-queries-D6kAqReI.js} +23 -1
- package/dist/introspect-db-queries-D6kAqReI.js.map +1 -0
- package/dist/pg-error-utils-vJezlXdI.js +388 -0
- package/dist/pg-error-utils-vJezlXdI.js.map +1 -0
- package/dist/{plan-schema-DU9exq6C.js → plan-schema-B1YF_tYq.js} +151 -41
- package/dist/plan-schema-B1YF_tYq.js.map +1 -0
- package/dist/{policy-drift-B-J2hhm0.js → policy-drift-Dm8i-s1J.js} +23 -6
- package/dist/policy-drift-Dm8i-s1J.js.map +1 -0
- package/dist/push-index-prebuild-Db4AQF4i.js +45 -0
- package/dist/push-index-prebuild-Db4AQF4i.js.map +1 -0
- package/dist/push-legacy-renames-BK6ilQq2.js +34 -0
- package/dist/push-legacy-renames-BK6ilQq2.js.map +1 -0
- package/dist/{render-ddl-Ds2t_d9V.js → render-ddl-CtC3y9Ol.js} +8 -4
- package/dist/render-ddl-CtC3y9Ol.js.map +1 -0
- package/dist/{rls-bootstrap-sql-_KNnjanK.js → rls-bootstrap-sql-RQUljnJ1.js} +125 -1270
- package/dist/rls-bootstrap-sql-RQUljnJ1.js.map +1 -0
- package/dist/{rls-enforcement-CfXOJJaW.js → rls-enforcement-DvzqlaL1.js} +129 -17
- package/dist/rls-enforcement-DvzqlaL1.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +2 -2
- package/dist/schema/boot-ddl.d.ts +47 -0
- package/dist/schema/collection-index.d.ts +45 -0
- package/dist/schema/column-plan-helpers.d.ts +5 -0
- package/dist/schema/deferred-index-build.d.ts +131 -0
- package/dist/schema/destructive-sql.d.ts +7 -0
- package/dist/schema/doctor-cli.js +110 -6
- package/dist/schema/doctor-cli.js.map +1 -1
- package/dist/schema/doctor-search-scale.d.ts +45 -0
- package/dist/schema/doctor.d.ts +20 -1
- package/dist/schema/ensure-collection-policies.d.ts +73 -35
- package/dist/schema/ensure-collection-tables.d.ts +56 -3
- package/dist/schema/enum-labels-kept.d.ts +26 -0
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generate-files.d.ts +44 -0
- package/dist/schema/generate-next-step.d.ts +0 -13
- package/dist/schema/generate-postgres-ddl.js +10 -81
- package/dist/schema/generate-postgres-ddl.js.map +1 -1
- package/dist/schema/introspect-db-logic.d.ts +12 -0
- package/dist/schema/introspect-db-queries.d.ts +11 -1
- package/dist/schema/introspect-db-structure.d.ts +10 -0
- package/dist/schema/introspect-db.js +4 -5
- package/dist/schema/introspect-db.js.map +1 -1
- package/dist/schema/migration-post-process.d.ts +34 -0
- package/dist/schema/plan/diff-plan.d.ts +126 -0
- package/dist/schema/plan/types.d.ts +8 -0
- package/dist/schema/push-index-prebuild.d.ts +25 -0
- package/dist/schema/push-legacy-renames.d.ts +20 -0
- package/dist/schema/rls-bootstrap-sql.d.ts +15 -0
- package/dist/security/rls-enforcement.d.ts +32 -4
- package/dist/services/FetchService.d.ts +58 -8
- package/dist/services/PersistService.d.ts +15 -0
- package/dist/services/RelationService.d.ts +88 -3
- package/dist/services/cdc/CdcListener.d.ts +17 -6
- package/dist/services/cdc/trigger-cdc.d.ts +151 -27
- package/dist/services/channel-history.d.ts +15 -4
- package/dist/services/collection-helpers.d.ts +15 -0
- package/dist/services/field-viewer.d.ts +15 -1
- package/dist/services/pg-notify-listener.d.ts +7 -0
- package/dist/services/realtimeService.d.ts +314 -14
- package/dist/services/row-pipeline.d.ts +16 -9
- package/dist/services/socket-liveness.d.ts +42 -2
- package/dist/services/sql-script.d.ts +207 -3
- package/dist/services/vector-iterative-scan.d.ts +40 -0
- package/dist/services/write-depth.d.ts +6 -0
- package/dist/services/write-transaction-scope.d.ts +9 -0
- package/dist/socket-liveness-D3UD5_BW.js +7570 -0
- package/dist/socket-liveness-D3UD5_BW.js.map +1 -0
- package/dist/socket-opt-ins.d.ts +64 -0
- package/dist/source-DBDbBTL4.js +811 -0
- package/dist/source-DBDbBTL4.js.map +1 -0
- package/dist/tsx-loader.d.ts +14 -0
- package/dist/utils/drizzle-conditions.d.ts +21 -1
- package/dist/utils/error-cause.d.ts +7 -0
- package/dist/utils/pg-error-utils.d.ts +2 -4
- package/dist/utils/result-rows.d.ts +10 -0
- package/dist/websocket.d.ts +13 -0
- package/package.json +7 -7
- package/dist/backup-cli-DW5p9_zv.js.map +0 -1
- package/dist/backup-service-EwcDVG-8.js.map +0 -1
- package/dist/cli-errors-DsA-K9uP.js +0 -893
- package/dist/cli-errors-DsA-K9uP.js.map +0 -1
- package/dist/cli-flags-BglvpjHv.js.map +0 -1
- package/dist/cli-output-CNdMql-L.js +0 -47
- package/dist/cli-output-CNdMql-L.js.map +0 -1
- package/dist/collection-index-DdnsxiJ_.js +0 -286
- package/dist/collection-index-DdnsxiJ_.js.map +0 -1
- package/dist/databasePoolManager-Bj5FbeAs.js.map +0 -1
- package/dist/doctor-C-sYWbmt.js.map +0 -1
- package/dist/ensure-collection-policies-B1ureSIV.js +0 -160
- package/dist/ensure-collection-policies-B1ureSIV.js.map +0 -1
- package/dist/ensure-collection-tables-kkkHk8oo.js.map +0 -1
- package/dist/ensure-tables-BmI_tRxc.js.map +0 -1
- package/dist/generate-drizzle-schema-93M0lUxK.js +0 -123
- package/dist/generate-drizzle-schema-93M0lUxK.js.map +0 -1
- package/dist/generate-drizzle-schema-logic-sSFDp6LR.js.map +0 -1
- package/dist/introspect-db-logic-kCETE8TY.js.map +0 -1
- package/dist/introspect-db-queries-C_Q5VgQw.js.map +0 -1
- package/dist/plan-schema-DU9exq6C.js.map +0 -1
- package/dist/policy-drift-B-J2hhm0.js.map +0 -1
- package/dist/render-ddl-Ds2t_d9V.js.map +0 -1
- package/dist/rls-bootstrap-sql-_KNnjanK.js.map +0 -1
- package/dist/rls-enforcement-CfXOJJaW.js.map +0 -1
- package/dist/source-Br7L7GOI.js +0 -410
- package/dist/source-Br7L7GOI.js.map +0 -1
- package/dist/utils/sql-redaction.d.ts +0 -21
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from "module";
|
|
2
|
+
__createRequire(import.meta.url);
|
|
3
|
+
import { r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
|
|
4
|
+
import { K as extractCauseMessage } from "./cli-errors-CaI4ntyD.js";
|
|
5
|
+
import { n as extractPgError } from "./pg-error-utils-vJezlXdI.js";
|
|
6
|
+
import { s as vectorExtensionHint } from "./vector-index-CMJKb3C9.js";
|
|
7
|
+
import { n as planSchema, r as quoteSqlLiteral } from "./plan-schema-B1YF_tYq.js";
|
|
8
|
+
import { c as planJunctionTables } from "./generate-postgres-ddl-logic-B64O2oZF.js";
|
|
9
|
+
import { r as quoteRelation } from "./introspect-db-queries-D6kAqReI.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-DP1_SNFo.js";
|
|
11
|
+
import { a as isLockTimeout, l as sleep, o as isMultiStatementRefusal, r as atomicBatch, s as joinStatements } from "./boot-ddl-DVpsFN6r.js";
|
|
12
|
+
import { isConcurrentDdlRace, isDuplicateObjectRace, logger } from "@rebasepro/server";
|
|
13
|
+
import { REBASE_SCHEMA, declaredDatabaseExtensions } from "@rebasepro/types";
|
|
14
|
+
import { getTableName, relationalCollections } from "@rebasepro/common";
|
|
15
|
+
//#region src/schema/ensure-collection-tables.ts
|
|
16
|
+
/**
|
|
17
|
+
* Applying a schema plan to a database that is already running.
|
|
18
|
+
*
|
|
19
|
+
* ## What this module is
|
|
20
|
+
*
|
|
21
|
+
* A managed runtime boots someone else's compiled project against a database it
|
|
22
|
+
* has never seen. Auth tables are ensured at boot already, but collection tables
|
|
23
|
+
* were not created by anything: the platform ran the app and every `/api/data/*`
|
|
24
|
+
* request answered 500 on a missing relation. `rebase db push` cannot help — it
|
|
25
|
+
* is an Atlas-driven CLI command, and the runtime image ships no CLI.
|
|
26
|
+
*
|
|
27
|
+
* Two halves, and only one of them is here. *What the collections ask for* is
|
|
28
|
+
* `schema/plan/plan-schema.ts`, shared with `db push` and the generated Drizzle
|
|
29
|
+
* file; *what the database already has, and which of those statements are safe
|
|
30
|
+
* to run against it* is `schema/plan/diff-plan.ts`. This module reads the
|
|
31
|
+
* catalogue, runs the statements, and reports what happened.
|
|
32
|
+
*
|
|
33
|
+
* ## Why additive-only, forever
|
|
34
|
+
*
|
|
35
|
+
* This runs unattended, against a database with customers' data in it, with no
|
|
36
|
+
* human reading a diff. So it may only ever do things that cannot lose data.
|
|
37
|
+
* See `diff-plan.ts` for the whole of that argument.
|
|
38
|
+
*/
|
|
39
|
+
var ensure_collection_tables_exports = /* @__PURE__ */ __exportAll({
|
|
40
|
+
describeDriverError: () => describeDriverError,
|
|
41
|
+
ensureCollectionTables: () => ensureCollectionTables,
|
|
42
|
+
planCollectionSchemaEnsure: () => planCollectionSchemaEnsure,
|
|
43
|
+
readExistingSchema: () => readExistingSchema,
|
|
44
|
+
readSchemaFactsFor: () => readSchemaFactsFor
|
|
45
|
+
});
|
|
46
|
+
/**
|
|
47
|
+
* Most statements in one new-table transaction. Bounds how many locks one
|
|
48
|
+
* transaction holds — each new table is several relations (the heap, its TOAST
|
|
49
|
+
* table, its key index) — well inside the default `max_locks_per_transaction`.
|
|
50
|
+
*/
|
|
51
|
+
var NEW_TABLE_BATCH = 100;
|
|
52
|
+
/** The schema a collection lives in, validated before it reaches any DDL. */
|
|
53
|
+
function schemaOf(collection) {
|
|
54
|
+
return assertSafeIdentifier(collection.schema || "public", "schema name");
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Decide what to add. Pure — the caller supplies what exists and runs the
|
|
58
|
+
* result.
|
|
59
|
+
*
|
|
60
|
+
* A plan and a diff: the collections are read once, by `planSchema`, into the
|
|
61
|
+
* same {@link SchemaPlan} `db push` renders `schema.sql` from, and the diff
|
|
62
|
+
* subtracts what the database already has. Before that split this function had
|
|
63
|
+
* its own reading of every `Property`, and the audit found twelve places where
|
|
64
|
+
* it disagreed with the two generators — a required `author_id` that was
|
|
65
|
+
* NOT NULL after a push and nullable after a boot among them, on the one path
|
|
66
|
+
* with no developer in the loop.
|
|
67
|
+
*/
|
|
68
|
+
function planCollectionSchemaEnsure(allCollections, existing, options = {}) {
|
|
69
|
+
return diffPlanAgainstCatalogue(planSchema(allCollections, { databaseExtensions: options.databaseExtensions }), existing, { constraints: options.constraints });
|
|
70
|
+
}
|
|
71
|
+
/** Read what the database has, for the schemas the collections live in. */
|
|
72
|
+
async function readExistingSchema(client, schemas, options = {}) {
|
|
73
|
+
const tables = /* @__PURE__ */ new Map();
|
|
74
|
+
const enums = /* @__PURE__ */ new Set();
|
|
75
|
+
if (schemas.length === 0) return {
|
|
76
|
+
tables,
|
|
77
|
+
enums
|
|
78
|
+
};
|
|
79
|
+
const inList = schemas.map((schema) => `'${assertSafeIdentifier(schema, "schema name")}'`).join(", ");
|
|
80
|
+
const notNullColumns = /* @__PURE__ */ new Set();
|
|
81
|
+
const columnTypes = /* @__PURE__ */ new Map();
|
|
82
|
+
const columnDefaults = /* @__PURE__ */ new Set();
|
|
83
|
+
const { rows: columns } = await client.query(`SELECT table_schema, table_name, column_name, is_nullable, udt_name, column_default,
|
|
84
|
+
numeric_precision, numeric_scale
|
|
85
|
+
FROM information_schema.columns
|
|
86
|
+
WHERE table_schema IN (${inList})`);
|
|
87
|
+
for (const row of columns) {
|
|
88
|
+
const key = `${row.table_schema}.${row.table_name}`;
|
|
89
|
+
if (!tables.has(key)) tables.set(key, /* @__PURE__ */ new Set());
|
|
90
|
+
tables.get(key).add(row.column_name);
|
|
91
|
+
if (row.is_nullable === "NO") notNullColumns.add(`${key}.${row.column_name}`);
|
|
92
|
+
if (row.udt_name) {
|
|
93
|
+
const modifier = row.udt_name === "numeric" && row.numeric_precision !== null ? `(${row.numeric_precision},${row.numeric_scale ?? 0})` : "";
|
|
94
|
+
columnTypes.set(`${key}.${row.column_name}`, `${row.udt_name}${modifier}`);
|
|
95
|
+
}
|
|
96
|
+
if (row.column_default !== null) columnDefaults.add(`${key}.${row.column_name}`);
|
|
97
|
+
}
|
|
98
|
+
const triggers = /* @__PURE__ */ new Set();
|
|
99
|
+
const { rows: triggerRows } = await client.query(`SELECT n.nspname AS schema, c.relname AS table, t.tgname AS name
|
|
100
|
+
FROM pg_trigger t
|
|
101
|
+
JOIN pg_class c ON t.tgrelid = c.oid
|
|
102
|
+
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
103
|
+
WHERE NOT t.tgisinternal AND n.nspname IN (${inList})`);
|
|
104
|
+
for (const row of triggerRows) triggers.add(`${row.schema}.${row.table}.${row.name}`);
|
|
105
|
+
const populatedTables = /* @__PURE__ */ new Set();
|
|
106
|
+
const indexes = /* @__PURE__ */ new Set();
|
|
107
|
+
const { rows: relations } = await client.query(`SELECT n.nspname AS schema, c.relname AS name, c.relkind::text AS kind
|
|
108
|
+
FROM pg_class c
|
|
109
|
+
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
110
|
+
WHERE c.relkind IN ('r', 'p', 'i', 'I') AND n.nspname IN (${inList})`);
|
|
111
|
+
const allRealTables = relations.filter((row) => row.kind === "r" || row.kind === "p");
|
|
112
|
+
for (const row of relations) if (row.kind === "i" || row.kind === "I") indexes.add(`${row.schema}.${row.name}`);
|
|
113
|
+
const wanted = options.probeTables ? new Set(options.probeTables(tables)) : void 0;
|
|
114
|
+
const realTables = wanted ? allRealTables.filter((row) => wanted.has(`${row.schema}.${row.name}`)) : allRealTables;
|
|
115
|
+
if (wanted) for (const row of allRealTables) {
|
|
116
|
+
const key = `${row.schema}.${row.name}`;
|
|
117
|
+
if (!wanted.has(key)) populatedTables.add(key);
|
|
118
|
+
}
|
|
119
|
+
if (realTables.length > 0) {
|
|
120
|
+
const probes = realTables.map((row) => `SELECT ${quoteSqlLiteral(`${row.schema}.${row.name}`)} AS key, EXISTS(SELECT 1 FROM ${quoteRelation(row.schema, row.name)} LIMIT 1) AS populated`);
|
|
121
|
+
const { rows: populationRows } = await client.query(probes.join(" UNION ALL "));
|
|
122
|
+
for (const row of populationRows) if (row.populated) populatedTables.add(row.key);
|
|
123
|
+
}
|
|
124
|
+
const enumValues = /* @__PURE__ */ new Map();
|
|
125
|
+
const { rows: enumValueRows } = await client.query(`SELECT n.nspname AS schema, t.typname AS name, e.enumlabel AS value
|
|
126
|
+
FROM pg_enum e
|
|
127
|
+
JOIN pg_type t ON e.enumtypid = t.oid
|
|
128
|
+
JOIN pg_namespace n ON t.typnamespace = n.oid
|
|
129
|
+
WHERE n.nspname IN (${inList})
|
|
130
|
+
ORDER BY t.typname, e.enumsortorder`);
|
|
131
|
+
for (const row of enumValueRows) {
|
|
132
|
+
const key = `${row.schema}.${row.name}`;
|
|
133
|
+
if (!enumValues.has(key)) enumValues.set(key, []);
|
|
134
|
+
enumValues.get(key).push(row.value);
|
|
135
|
+
}
|
|
136
|
+
const { rows: enumRows } = await client.query(`SELECT n.nspname AS schema, t.typname AS name
|
|
137
|
+
FROM pg_type t
|
|
138
|
+
JOIN pg_namespace n ON t.typnamespace = n.oid
|
|
139
|
+
WHERE t.typtype = 'e' AND n.nspname IN (${inList})`);
|
|
140
|
+
for (const row of enumRows) enums.add(`${row.schema}.${row.name}`);
|
|
141
|
+
const constraints = /* @__PURE__ */ new Set();
|
|
142
|
+
const { rows: constraintRows } = await client.query(`SELECT n.nspname AS schema, c.relname AS table, con.conname AS name
|
|
143
|
+
FROM pg_constraint con
|
|
144
|
+
JOIN pg_class c ON con.conrelid = c.oid
|
|
145
|
+
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
146
|
+
WHERE n.nspname IN (${inList})`);
|
|
147
|
+
for (const row of constraintRows) constraints.add(`${row.schema}.${row.table}.${row.name}`);
|
|
148
|
+
const invalidIndexes = /* @__PURE__ */ new Set();
|
|
149
|
+
const { rows: invalidRows } = await client.query(`SELECT n.nspname AS schema, c.relname AS name
|
|
150
|
+
FROM pg_index i
|
|
151
|
+
JOIN pg_class c ON c.oid = i.indexrelid
|
|
152
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
153
|
+
WHERE NOT i.indisvalid AND n.nspname IN (${inList})`);
|
|
154
|
+
for (const row of invalidRows) invalidIndexes.add(`${row.schema}.${row.name}`);
|
|
155
|
+
const columnComments = /* @__PURE__ */ new Map();
|
|
156
|
+
const { rows: commentRows } = await client.query(`SELECT n.nspname AS schema, c.relname AS table, a.attname AS column, d.description AS comment
|
|
157
|
+
FROM pg_description d
|
|
158
|
+
JOIN pg_class c ON d.objoid = c.oid
|
|
159
|
+
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
160
|
+
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = d.objsubid
|
|
161
|
+
WHERE d.objsubid > 0 AND n.nspname IN (${inList})`);
|
|
162
|
+
for (const row of commentRows) {
|
|
163
|
+
if (row.comment == null) continue;
|
|
164
|
+
columnComments.set(`${row.schema}.${row.table}.${row.column}`, row.comment);
|
|
165
|
+
}
|
|
166
|
+
const functions = /* @__PURE__ */ new Map();
|
|
167
|
+
const helperNames = HELPER_FUNCTIONS.map((qualified) => qualified.split(".")).map(([schema, name]) => `(${quoteSqlLiteral(schema)}, ${quoteSqlLiteral(name)})`).join(", ");
|
|
168
|
+
const { rows: functionRows } = await client.query(`SELECT n.nspname AS schema, p.proname AS name, pg_get_function_identity_arguments(p.oid) AS args,
|
|
169
|
+
format_type(p.prorettype, NULL) AS returns, l.lanname AS language,
|
|
170
|
+
p.provolatile::text AS volatility, p.proisstrict AS strict, p.proparallel::text AS parallel,
|
|
171
|
+
p.prosecdef AS security_definer, p.proleakproof AS leakproof, p.procost AS cost,
|
|
172
|
+
p.proconfig IS NOT NULL AS has_config, p.prosrc AS body
|
|
173
|
+
FROM pg_proc p
|
|
174
|
+
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
175
|
+
JOIN pg_language l ON p.prolang = l.oid
|
|
176
|
+
WHERE (n.nspname, p.proname) IN (${helperNames})`);
|
|
177
|
+
for (const row of functionRows) functions.set(helperFunctionKey(row.schema, row.name, row.args), describeHelperFunction({
|
|
178
|
+
returns: row.returns,
|
|
179
|
+
language: row.language,
|
|
180
|
+
volatility: row.volatility,
|
|
181
|
+
strict: row.strict,
|
|
182
|
+
parallel: row.parallel,
|
|
183
|
+
securityDefiner: row.security_definer,
|
|
184
|
+
leakproof: row.leakproof,
|
|
185
|
+
cost: Number(row.cost),
|
|
186
|
+
hasConfig: row.has_config,
|
|
187
|
+
body: row.body
|
|
188
|
+
}));
|
|
189
|
+
const extensions = /* @__PURE__ */ new Set();
|
|
190
|
+
const { rows: extensionRows } = await client.query("SELECT extname AS name FROM pg_extension");
|
|
191
|
+
for (const row of extensionRows) extensions.add(row.name);
|
|
192
|
+
return {
|
|
193
|
+
tables,
|
|
194
|
+
enums,
|
|
195
|
+
constraints,
|
|
196
|
+
columnComments,
|
|
197
|
+
enumValues,
|
|
198
|
+
notNullColumns,
|
|
199
|
+
populatedTables,
|
|
200
|
+
columnTypes,
|
|
201
|
+
columnDefaults,
|
|
202
|
+
triggers,
|
|
203
|
+
indexes,
|
|
204
|
+
invalidIndexes,
|
|
205
|
+
extensions,
|
|
206
|
+
functions
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* The planned tables that exist and lack at least one planned column — the
|
|
211
|
+
* only ones whose emptiness the diff consults (a NOT NULL or UNIQUE arriving
|
|
212
|
+
* with a new column). A superset, never a subset: a column the plan renames
|
|
213
|
+
* from a legacy name reads as missing here too.
|
|
214
|
+
*/
|
|
215
|
+
function tablesGainingColumns(plan, catalogue) {
|
|
216
|
+
const gaining = [];
|
|
217
|
+
for (const table of plan.tables) {
|
|
218
|
+
const columns = catalogue.get(table.qualified);
|
|
219
|
+
if (columns && table.columns.some((column) => !columns.has(column.column))) gaining.push(table.qualified);
|
|
220
|
+
}
|
|
221
|
+
return gaining;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* What to tell an operator whose `search` block no longer matches its column.
|
|
225
|
+
*
|
|
226
|
+
* Every line here is doing work: naming the collection is not enough, because
|
|
227
|
+
* the symptom (a search that finds nothing) points at the data, not the schema;
|
|
228
|
+
* and the remediation has to be exact, because it is a table rewrite the
|
|
229
|
+
* operator is being asked to schedule rather than discover.
|
|
230
|
+
*/
|
|
231
|
+
function searchDriftMessage(drift) {
|
|
232
|
+
return "The `search` block changed after its generated column was created, and Postgres cannot alter a generated expression in place.\nRebase will not rebuild it for you: dropping and re-adding a STORED generated column rewrites the whole table under an ACCESS EXCLUSIVE lock and rebuilds its GIN index, which is an outage this unattended path may not schedule on your behalf.\nUntil it is rebuilt the column keeps indexing the previous fields, weights and language — searches for anything added since return nothing, which reads from outside as \"no such row\".\nRun these (or revert the block to what the column was built from), then boot again:\n" + drift.map((d) => ` "${d.table}"."${d.column}" was generated from a different \`search\` block (recorded ${d.found}, current ${d.expected}).\n` + d.rebuild.map((s) => ` ${s}`).join("\n")).join("\n") + "\n The GIN index is dropped with the column and recreated concurrently on the next boot.";
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Read what the database looks like, for the schemas a set of collections
|
|
236
|
+
* lives in.
|
|
237
|
+
*
|
|
238
|
+
* The same read `ensureCollectionTables` does at boot, exposed on its own for
|
|
239
|
+
* the callers that want to *plan* against a real database without changing it —
|
|
240
|
+
* the live schema editor, which has to tell somebody what a change would do
|
|
241
|
+
* before they agree to it.
|
|
242
|
+
*/
|
|
243
|
+
async function readSchemaFactsFor(client, collections) {
|
|
244
|
+
const relational = relationalCollections(collections);
|
|
245
|
+
const junctions = planJunctionTables(relational);
|
|
246
|
+
const schemas = Array.from(/* @__PURE__ */ new Set([...relational.map(schemaOf), ...junctions.map((junction) => junction.schema)]));
|
|
247
|
+
const declared = [...relational.map((collection) => `${schemaOf(collection)}.${getTableName(collection)}`), ...junctions.map((junction) => `${junction.schema}.${junction.table}`)];
|
|
248
|
+
return readExistingSchema(client, schemas, { probeTables: () => declared });
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Bring the database up to date. Returns what it did.
|
|
252
|
+
*
|
|
253
|
+
* Each statement runs on its own rather than in one transaction: they are all
|
|
254
|
+
* independently safe and idempotent, and a single failure (an enum label that
|
|
255
|
+
* cannot be added, say) should not roll back the tables that were created fine.
|
|
256
|
+
* The error is surfaced with the statement that caused it.
|
|
257
|
+
*/
|
|
258
|
+
async function ensureCollectionTables(client, collections, log, options = {}) {
|
|
259
|
+
const schema = planSchema(collections, { databaseExtensions: options.databaseExtensions ?? declaredDatabaseExtensions() });
|
|
260
|
+
const schemas = Array.from(new Set(schema.tables.map((table) => table.schema)));
|
|
261
|
+
for (const name of schemas) {
|
|
262
|
+
assertSafeIdentifier(name, "schema name");
|
|
263
|
+
if (name !== "public") await client.query(`CREATE SCHEMA IF NOT EXISTS "${name}";`);
|
|
264
|
+
}
|
|
265
|
+
if (schema.tables.some((table) => table.triggers.length > 0)) await client.query(`CREATE SCHEMA IF NOT EXISTS "${REBASE_SCHEMA}";`);
|
|
266
|
+
const plan = diffPlanAgainstCatalogue(schema, await readExistingSchema(client, schemas, { probeTables: (catalogue) => tablesGainingColumns(schema, catalogue) }), {
|
|
267
|
+
constraints: options.constraints,
|
|
268
|
+
deferLiveIndexes: true
|
|
269
|
+
});
|
|
270
|
+
const failures = [];
|
|
271
|
+
for (const legacy of plan.legacyForeignKeys) {
|
|
272
|
+
const message = `Renaming "${legacy.table}"."${legacy.legacy}" to "${legacy.expected}". The old name is the one Rebase derived for this relation before it singularized properly; the column keeps its data, indexes and constraints. To keep the old name instead, set \`localKey: "${legacy.legacy}"\` on the relation and this will stop.`;
|
|
273
|
+
logger.info(`[schema] ${message}`);
|
|
274
|
+
log?.(message);
|
|
275
|
+
}
|
|
276
|
+
if (plan.searchDrift.length > 0) throw new Error(searchDriftMessage(plan.searchDrift));
|
|
277
|
+
for (const adopted of plan.searchAdopted) {
|
|
278
|
+
const message = `Adopting the existing generated column "${adopted.table}"."${adopted.column}" and recording what the current \`search\` block would generate. Any later change to that block will be detected and refused; a change made *before* this version was deployed cannot be, so if search has been missing content, rebuild the column once: ALTER TABLE "${adopted.table.split(".").join("\".\"")}" DROP COLUMN "${adopted.column}"; and boot again.`;
|
|
279
|
+
logger.info(`[schema] ${message}`);
|
|
280
|
+
log?.(message);
|
|
281
|
+
}
|
|
282
|
+
for (const skip of plan.vectorIndexSkipped) {
|
|
283
|
+
const message = `No ANN index on "${skip.table}"."${skip.column}": ${skip.reason}`;
|
|
284
|
+
logger.warn(`[schema] ${message}`);
|
|
285
|
+
log?.(message);
|
|
286
|
+
}
|
|
287
|
+
for (const withheld of plan.withheldConstraints) {
|
|
288
|
+
const message = `No ${withheld.kind === "unique" ? "UNIQUE" : "NOT NULL"} on "${withheld.target}": ${withheld.reason} ${withheld.remedy}`;
|
|
289
|
+
logger.warn(`[schema] ${message}`);
|
|
290
|
+
log?.(message);
|
|
291
|
+
}
|
|
292
|
+
for (const drift of plan.columnTypeDrift) {
|
|
293
|
+
const message = columnTypeDriftMessage(drift);
|
|
294
|
+
logger.warn(`[schema] ${message}`);
|
|
295
|
+
log?.(message);
|
|
296
|
+
}
|
|
297
|
+
for (const orphan of plan.orphanedRequiredColumns) {
|
|
298
|
+
const [schema, table] = orphan.table.split(".");
|
|
299
|
+
const message = `"${orphan.table}"."${orphan.column}" is NOT NULL with no default, and the "${orphan.slug}" collection no longer declares it. Every insert will be rejected for a field you cannot set (PG_23502, naming "${orphan.column}"). This is what a renamed property looks like: the new column was added, and the old one cannot be dropped unattended. Fix it with ONE of:
|
|
300
|
+
ALTER TABLE "${schema}"."${table}" DROP COLUMN "${orphan.column}"; -- the rename is complete\n ALTER TABLE "${schema}"."${table}" ALTER COLUMN "${orphan.column}" DROP NOT NULL; -- keep the data, stop requiring it\n ...or declare the property again, if the column is still wanted.`;
|
|
301
|
+
logger.warn(`[schema] ${message}`);
|
|
302
|
+
log?.(message);
|
|
303
|
+
}
|
|
304
|
+
const builtHere = options.deferIndexBuilds ? plan.deferredIndexes.filter((index) => index.unique === true) : plan.deferredIndexes;
|
|
305
|
+
const leftToCaller = options.deferIndexBuilds ? plan.deferredIndexes.filter((index) => index.unique !== true) : plan.deferredIndexes;
|
|
306
|
+
const buildIndexes = async () => {
|
|
307
|
+
if (builtHere.length === 0) return void 0;
|
|
308
|
+
const outcome = await buildDeferredIndexes(options.openIndexBuildSession ?? (async () => ({
|
|
309
|
+
query: (sql) => client.query(sql),
|
|
310
|
+
dedicated: false,
|
|
311
|
+
release: () => void 0
|
|
312
|
+
})), builtHere, {
|
|
313
|
+
log,
|
|
314
|
+
lockTimeoutMs: options.lockTimeoutMs
|
|
315
|
+
});
|
|
316
|
+
assertUniqueIndexesBuilt(builtHere, outcome);
|
|
317
|
+
return outcome;
|
|
318
|
+
};
|
|
319
|
+
const finish = async () => {
|
|
320
|
+
const indexBuilds = await buildIndexes();
|
|
321
|
+
return {
|
|
322
|
+
...plan,
|
|
323
|
+
deferredIndexes: leftToCaller,
|
|
324
|
+
failures,
|
|
325
|
+
indexBuilds
|
|
326
|
+
};
|
|
327
|
+
};
|
|
328
|
+
if (plan.actions.length === 0) {
|
|
329
|
+
if (plan.deferredIndexes.length === 0) log?.("Schema is up to date; nothing to create.");
|
|
330
|
+
return finish();
|
|
331
|
+
}
|
|
332
|
+
const lockBudget = options.lockTimeoutMs !== void 0 ? {
|
|
333
|
+
lockTimeoutMs: options.lockTimeoutMs,
|
|
334
|
+
atomic: true
|
|
335
|
+
} : void 0;
|
|
336
|
+
const multi = lockBudget ?? { atomic: true };
|
|
337
|
+
const createdTables = new Set(plan.actions.filter((a) => a.kind === "create-table").map((a) => a.target));
|
|
338
|
+
const batchable = (action) => options.batchNewTables === true && multi.atomic && (action.kind === "create-enum" || createdTables.has(action.target.split(".").slice(0, 2).join("."))) && action.kind !== "add-enum-value" && !CONCURRENTLY_RE.test(action.sql);
|
|
339
|
+
const applyBatch = async (group) => {
|
|
340
|
+
const statements = group.map((action) => action.sql);
|
|
341
|
+
try {
|
|
342
|
+
await client.query(lockBudget ? atomicBatch(statements, lockBudget.lockTimeoutMs) : joinStatements(statements));
|
|
343
|
+
return true;
|
|
344
|
+
} catch (err) {
|
|
345
|
+
if (isMultiStatementRefusal(err)) multi.atomic = false;
|
|
346
|
+
else log?.(`${group.length} statement(s) for new tables failed as one transaction (${describeDriverError(err).split("\n")[0]}); applying them one at a time`);
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
const applyOne = async (action) => {
|
|
351
|
+
try {
|
|
352
|
+
if (await applyAction(client, action, lockBudget)) log?.(`${action.kind}: ${action.target}`);
|
|
353
|
+
else log?.(`${action.kind}: ${action.target} (already created by a peer)`);
|
|
354
|
+
} catch (err) {
|
|
355
|
+
const message = describeDriverError(err);
|
|
356
|
+
if (action.kind === "add-constraint" || action.kind === "comment-column" || action.kind === "create-index" && !/CREATE\s+UNIQUE\s+INDEX/i.test(action.sql)) {
|
|
357
|
+
failures.push({
|
|
358
|
+
kind: action.kind,
|
|
359
|
+
target: action.target,
|
|
360
|
+
error: message
|
|
361
|
+
});
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const lockHint = isLockTimeout(err) ? "\n hint: another session held this table's lock for every attempt (a long transaction, a report or a dump). Nothing was changed; boot again once it has finished." : "";
|
|
365
|
+
throw new Error(`Failed to ${action.kind} ${action.target}: ${message}${vectorExtensionHint(message)}${lockHint}\n ${action.sql}`);
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
for (let i = 0; i < plan.actions.length;) {
|
|
369
|
+
let end = i;
|
|
370
|
+
while (end < plan.actions.length && end - i < NEW_TABLE_BATCH && batchable(plan.actions[end])) end++;
|
|
371
|
+
if (end - i > 1 && await applyBatch(plan.actions.slice(i, end))) {
|
|
372
|
+
for (const action of plan.actions.slice(i, end)) log?.(`${action.kind}: ${action.target}`);
|
|
373
|
+
i = end;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
const last = Math.max(end, i + 1);
|
|
377
|
+
for (; i < last; i++) await applyOne(plan.actions[i]);
|
|
378
|
+
}
|
|
379
|
+
return finish();
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Fail the ensure for a UNIQUE index on a live table that could not be built,
|
|
383
|
+
* exactly as a failed UNIQUE `create-index` action always has (see `applyOne`).
|
|
384
|
+
*
|
|
385
|
+
* "Another index on the table is being built" counts: the table's lock is
|
|
386
|
+
* held by a build that may run for minutes, and serving the collection
|
|
387
|
+
* meanwhile would be serving a promise the database is not keeping. Another
|
|
388
|
+
* instance building *this* index does not: once its build commits the
|
|
389
|
+
* constraint holds, which is also what a peer's `IF NOT EXISTS` race meant.
|
|
390
|
+
*/
|
|
391
|
+
function assertUniqueIndexesBuilt(indexes, outcome) {
|
|
392
|
+
for (const index of indexes) {
|
|
393
|
+
if (index.unique !== true) continue;
|
|
394
|
+
const qualified = `${index.schema}.${index.indexName}`;
|
|
395
|
+
const reason = outcome.failed.find((failure) => failure.index === qualified)?.error ?? outcome.skipped.find((skip) => skip.index === qualified && skip.reason === "another index on the table is being built")?.reason;
|
|
396
|
+
if (reason === void 0) continue;
|
|
397
|
+
throw new Error(`Failed to create-index ${index.schema}.${index.table}: ${qualified}: ${reason}\n ${index.sql}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* What actually went wrong, rather than what the ORM said about it.
|
|
402
|
+
*
|
|
403
|
+
* Drizzle wraps every driver error, and its `message` is literally
|
|
404
|
+
* ``Failed query: ${sql}\nparams: ${params}`` — the statement we already know,
|
|
405
|
+
* and not one word of Postgres's answer. Reporting `err.message` therefore
|
|
406
|
+
* produced a failure that named the statement twice and the *reason* zero
|
|
407
|
+
* times, so four unrelated boot failures — a missing extension, a wrong
|
|
408
|
+
* privilege, a bad index method, a constraint the rows violate — all read
|
|
409
|
+
* identically to whoever had to fix them.
|
|
410
|
+
*
|
|
411
|
+
* Two consequences, both observed in the field before this existed:
|
|
412
|
+
*
|
|
413
|
+
* 1. On a managed deployment the reason never reaches anybody. Boot dies, the
|
|
414
|
+
* pod never goes ready, and the deploy reports a readiness timeout — so the
|
|
415
|
+
* one line that says why is the one line that was thrown away here.
|
|
416
|
+
* 2. {@link vectorExtensionHint} matches on the error text. Against the
|
|
417
|
+
* wrapper it matched nothing, so the hint written specifically to explain a
|
|
418
|
+
* missing pgvector never fired on the path that raises it.
|
|
419
|
+
*
|
|
420
|
+
* `detail` and `hint` come along because they are the fields Postgres uses to
|
|
421
|
+
* say which object it meant and what to do about it — `CREATE EXTENSION` names
|
|
422
|
+
* the missing library there, and a rejected constraint names the row. This
|
|
423
|
+
* writes into the tenant's own boot log, about the tenant's own database.
|
|
424
|
+
*/
|
|
425
|
+
function describeDriverError(err) {
|
|
426
|
+
const pg = extractPgError(err);
|
|
427
|
+
if (pg) {
|
|
428
|
+
const parts = [pg.code ? `${pg.message} [${pg.code}]` : pg.message];
|
|
429
|
+
if (pg.detail) parts.push(` detail: ${pg.detail}`);
|
|
430
|
+
if (pg.hint) parts.push(` hint: ${pg.hint}`);
|
|
431
|
+
return parts.join("\n");
|
|
432
|
+
}
|
|
433
|
+
const cause = extractCauseMessage(err);
|
|
434
|
+
if (cause) return cause;
|
|
435
|
+
return err instanceof Error ? err.message : String(err);
|
|
436
|
+
}
|
|
437
|
+
/** ` CONCURRENTLY ` as it appears in a rendered `CREATE [UNIQUE] INDEX`. */
|
|
438
|
+
var CONCURRENTLY_RE = /\sCONCURRENTLY\s/i;
|
|
439
|
+
/**
|
|
440
|
+
* `25001 active_sql_transaction` — "cannot run inside a transaction block".
|
|
441
|
+
*
|
|
442
|
+
* Only ever raised here by `CREATE INDEX CONCURRENTLY`, and only when the
|
|
443
|
+
* connection we were handed is inside a transaction. Deliberately narrow: it
|
|
444
|
+
* decides to retry a *different* statement, so it must not fire on anything it
|
|
445
|
+
* has not been reasoned about.
|
|
446
|
+
*/
|
|
447
|
+
function isTransactionBlockRefusal(err) {
|
|
448
|
+
return extractPgError(err)?.code === "25001";
|
|
449
|
+
}
|
|
450
|
+
/** Attempts per action, including the first. Matches the server's bootstraps. */
|
|
451
|
+
var DDL_ATTEMPTS = 4;
|
|
452
|
+
/**
|
|
453
|
+
* Attempts at an action whose table lock is held, including the first.
|
|
454
|
+
*
|
|
455
|
+
* Unlike a policy, a schema change cannot be deferred — the collection's code
|
|
456
|
+
* expects the column — so a lock that never frees still fails the boot, as a
|
|
457
|
+
* statement timeout did. Ten one-second attempts with backoff between them wait
|
|
458
|
+
* about as long as the old 30 s statement timeout did, but a live query on that
|
|
459
|
+
* table now waits at most one second at a time instead of thirty.
|
|
460
|
+
*/
|
|
461
|
+
var LOCK_ATTEMPTS = 10;
|
|
462
|
+
/**
|
|
463
|
+
* Whether an action may be sent as `SET LOCAL lock_timeout; <statement>` — one
|
|
464
|
+
* implicit transaction. Not `CREATE INDEX CONCURRENTLY`, which Postgres refuses
|
|
465
|
+
* inside any transaction block (and which waits on no lock a reader holds), and
|
|
466
|
+
* not `ALTER TYPE … ADD VALUE`, which Postgres before 12 refuses there too and
|
|
467
|
+
* which locks a type, not a table.
|
|
468
|
+
*/
|
|
469
|
+
var takesLockBudget = (action, sql) => action.kind !== "add-enum-value" && !CONCURRENTLY_RE.test(sql);
|
|
470
|
+
/**
|
|
471
|
+
* Run one planned statement, surviving a simultaneous boot.
|
|
472
|
+
*
|
|
473
|
+
* Every statement in a plan is written to be idempotent, and that is not the
|
|
474
|
+
* same as being safe to run concurrently: `CREATE … IF NOT EXISTS` reads the
|
|
475
|
+
* catalog and then writes to it as two steps, so peers starting together both
|
|
476
|
+
* see "absent" and the loser gets a duplicate key on a *catalog* index. Measured
|
|
477
|
+
* against Postgres 18: five instances, 8 of 10 calls lost. `CREATE TYPE` is
|
|
478
|
+
* worse, because Postgres has no `IF NOT EXISTS` for it at all.
|
|
479
|
+
*
|
|
480
|
+
* What made that fatal here rather than merely noisy is the loop this sits in.
|
|
481
|
+
* A losing statement threw, and the throw abandoned **every remaining action in
|
|
482
|
+
* the plan** — so a replica that lost one race came up missing tables it never
|
|
483
|
+
* attempted, and the boot log blamed the one statement that failed.
|
|
484
|
+
*
|
|
485
|
+
* @returns `true` if this process applied the statement, `false` if a peer had
|
|
486
|
+
* already created the object. The distinction is only for the log; both mean
|
|
487
|
+
* the object is now there.
|
|
488
|
+
* @throws the original error for anything that is not a race — a syntax error, a
|
|
489
|
+
* permission failure, a unique constraint the customer's own rows violate.
|
|
490
|
+
*/
|
|
491
|
+
async function applyAction(client, action, lockBudget) {
|
|
492
|
+
let sqlText = action.sql;
|
|
493
|
+
let lockAttempts = 0;
|
|
494
|
+
for (let attempt = 1;; attempt++) {
|
|
495
|
+
const budgeted = lockBudget?.atomic === true && takesLockBudget(action, sqlText);
|
|
496
|
+
try {
|
|
497
|
+
await client.query(budgeted ? atomicBatch([sqlText], lockBudget.lockTimeoutMs) : sqlText);
|
|
498
|
+
return true;
|
|
499
|
+
} catch (err) {
|
|
500
|
+
if (budgeted && isMultiStatementRefusal(err)) {
|
|
501
|
+
lockBudget.atomic = false;
|
|
502
|
+
attempt--;
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
if (budgeted && isLockTimeout(err) && ++lockAttempts < LOCK_ATTEMPTS) {
|
|
506
|
+
logger.debug(`[schema] ${action.kind} ${action.target}: the table's lock is held by another session (attempt ${lockAttempts}/${LOCK_ATTEMPTS}) — retrying`);
|
|
507
|
+
attempt--;
|
|
508
|
+
await sleep(Math.min(500 * lockAttempts, 2e3) * (1 + Math.random()));
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if (isTransactionBlockRefusal(err) && CONCURRENTLY_RE.test(sqlText)) {
|
|
512
|
+
sqlText = sqlText.replace(CONCURRENTLY_RE, " ");
|
|
513
|
+
logger.warn(`[schema] ${action.kind} ${action.target}: this connection is inside a transaction, so the index is being built with a write lock rather than concurrently. Writes to this table block until it finishes.`);
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
if (isDuplicateObjectRace(err)) {
|
|
517
|
+
logger.debug(`[schema] ${action.kind} ${action.target}: already created by another instance`);
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
if (isConcurrentDdlRace(err) && attempt < DDL_ATTEMPTS) {
|
|
521
|
+
logger.debug(`[schema] ${action.kind} ${action.target}: lost a race with another instance (attempt ${attempt}/${DDL_ATTEMPTS}) — retrying`);
|
|
522
|
+
await new Promise((resolve) => setTimeout(resolve, 40 * attempt * (1 + Math.random())));
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
throw err;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
//#endregion
|
|
530
|
+
export { readSchemaFactsFor as a, readExistingSchema as i, ensure_collection_tables_exports as n, planCollectionSchemaEnsure as r, describeDriverError as t };
|
|
531
|
+
|
|
532
|
+
//# sourceMappingURL=ensure-collection-tables-BhsU6qUE.js.map
|