@rebasepro/server-postgres 0.24.0 → 0.24.1-canary.gcb918fc
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/README.md +16 -0
- 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 +91 -2
- package/dist/PostgresBootstrapper.d.ts +101 -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-DnpJi6I7.js} +37 -29
- package/dist/backup-cli-DnpJi6I7.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-console-Ca6ZlAVU.js +154 -0
- package/dist/cli-console-Ca6ZlAVU.js.map +1 -0
- package/dist/cli-console.d.ts +39 -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-JNI-pwSf.js} +6 -3
- package/dist/cli-flags-JNI-pwSf.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 +566 -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-C9mT8bgf.js} +24 -112
- package/dist/connection-C9mT8bgf.js.map +1 -0
- package/dist/connection.d.ts +13 -2
- package/dist/databasePoolManager-BIuewidb.js +119 -0
- package/dist/databasePoolManager-BIuewidb.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-CVm_yvzb.js} +173 -77
- package/dist/ensure-tables-CVm_yvzb.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 +64 -5
- package/dist/history/ensure-history-table.d.ts +63 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +139 -19236
- 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/open-rebase-CbOur0TU.js +20858 -0
- package/dist/open-rebase-CbOur0TU.js.map +1 -0
- package/dist/open-rebase.d.ts +86 -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-bus/index.d.ts +1 -1
- 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-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-BLGc5yhs.js +1604 -0
- package/dist/websocket-BLGc5yhs.js.map +1 -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
|
@@ -2,16 +2,13 @@ import { createRequire as __createRequire } from "module";
|
|
|
2
2
|
__createRequire(import.meta.url);
|
|
3
3
|
import { r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
|
|
4
4
|
import { h as searchIndexStatements, n as SEARCH_TEXT_FN, r as SEARCH_UNACCENT_FN, u as searchColumnStamps } from "./search-column-BM-GV6vH.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { a as triggerStatements, i as SET_UPDATED_AT_FN,
|
|
9
|
-
import { n as renderEnableRls, r as renderPgType, s as renderPrimaryKeyConstraint, t as renderColumnDefinition } from "./render-ddl-
|
|
10
|
-
import { c as
|
|
11
|
-
import {
|
|
12
|
-
import { isConcurrentDdlRace, isDuplicateObjectRace, logger } from "@rebasepro/server";
|
|
13
|
-
import { REBASE_SCHEMA, declaredDatabaseExtensions } from "@rebasepro/types";
|
|
14
|
-
import { relationalCollections } from "@rebasepro/common";
|
|
5
|
+
import { n as extractPgError } from "./pg-error-utils-vJezlXdI.js";
|
|
6
|
+
import { u as vectorIndexStatement } from "./vector-index-CMJKb3C9.js";
|
|
7
|
+
import { h as sortIndexSpecs, s as collectionIndexStatement } from "./source-DBDbBTL4.js";
|
|
8
|
+
import { a as triggerStatements, i as SET_UPDATED_AT_FN, r as quoteSqlLiteral } from "./plan-schema-B1YF_tYq.js";
|
|
9
|
+
import { n as renderEnableRls, r as renderPgType, s as renderPrimaryKeyConstraint, t as renderColumnDefinition } from "./render-ddl-CtC3y9Ol.js";
|
|
10
|
+
import { a as isLockTimeout, c as resolveBootLockOptions, l as sleep, o as isMultiStatementRefusal, r as atomicBatch } from "./boot-ddl-DVpsFN6r.js";
|
|
11
|
+
import { logger } from "@rebasepro/server";
|
|
15
12
|
//#region src/schema/column-type-drift.ts
|
|
16
13
|
/**
|
|
17
14
|
* Types that mean the same thing to everything above the driver.
|
|
@@ -159,6 +156,68 @@ function assertSafeIdentifier(value, what) {
|
|
|
159
156
|
return value;
|
|
160
157
|
}
|
|
161
158
|
/**
|
|
159
|
+
* The functions the plan creates with `CREATE OR REPLACE`, which the catalogue
|
|
160
|
+
* reader looks up so a boot that finds them unchanged plans nothing.
|
|
161
|
+
*
|
|
162
|
+
* A helper missing from this list is not wrong, only re-created on every boot,
|
|
163
|
+
* which is what all of them were before.
|
|
164
|
+
*/
|
|
165
|
+
var HELPER_FUNCTIONS = [
|
|
166
|
+
SEARCH_TEXT_FN,
|
|
167
|
+
SEARCH_UNACCENT_FN,
|
|
168
|
+
SET_UPDATED_AT_FN
|
|
169
|
+
];
|
|
170
|
+
var helperFunctionKey = (schema, name, args) => `${schema}.${name}(${args.replace(/\s+/g, " ").trim().toLowerCase()})`;
|
|
171
|
+
var describeHelperFunction = (facts) => JSON.stringify([
|
|
172
|
+
facts.returns.toLowerCase(),
|
|
173
|
+
facts.language.toLowerCase(),
|
|
174
|
+
facts.volatility,
|
|
175
|
+
facts.strict,
|
|
176
|
+
facts.parallel,
|
|
177
|
+
facts.securityDefiner,
|
|
178
|
+
facts.leakproof,
|
|
179
|
+
Number(facts.cost),
|
|
180
|
+
facts.hasConfig,
|
|
181
|
+
facts.body
|
|
182
|
+
]);
|
|
183
|
+
var HELPER_FUNCTION_RE = /* @__PURE__ */ new RegExp("^\\s*CREATE OR REPLACE FUNCTION\\s+([A-Za-z_]\\w*)\\.([A-Za-z_]\\w*)\\(([^)]*)\\)\\s+RETURNS\\s+([\\w\\[\\]]+)\\s+LANGUAGE\\s+(\\w+)((?:\\s+(?:IMMUTABLE|STABLE|VOLATILE|STRICT|PARALLEL\\s+(?:SAFE|RESTRICTED|UNSAFE)))*)\\s+AS\\s+\\$\\$([\\s\\S]*)\\$\\$;?\\s*$", "i");
|
|
184
|
+
/**
|
|
185
|
+
* What a helper's `CREATE OR REPLACE FUNCTION` statement would leave in the
|
|
186
|
+
* catalogue, or `undefined` for a statement this does not recognise — which is
|
|
187
|
+
* then planned unconditionally, as every statement was before.
|
|
188
|
+
*
|
|
189
|
+
* Only ever given statements this package renders (`searchHelperFunctions`,
|
|
190
|
+
* `setUpdatedAtFunction`), so a strict pattern is the right tool: anything it
|
|
191
|
+
* does not match exactly falls back to the safe behaviour.
|
|
192
|
+
*/
|
|
193
|
+
function parseHelperFunction(statement) {
|
|
194
|
+
const match = HELPER_FUNCTION_RE.exec(statement);
|
|
195
|
+
if (!match) return void 0;
|
|
196
|
+
const [, schema, name, args, returns, language, attributes, body] = match;
|
|
197
|
+
const words = attributes.toUpperCase();
|
|
198
|
+
const parallel = /PARALLEL\s+SAFE/.test(words) ? "s" : /PARALLEL\s+RESTRICTED/.test(words) ? "r" : "u";
|
|
199
|
+
const volatility = /\bIMMUTABLE\b/.test(words) ? "i" : /\bSTABLE\b/.test(words) ? "s" : "v";
|
|
200
|
+
return {
|
|
201
|
+
key: helperFunctionKey(schema, name, args),
|
|
202
|
+
definition: describeHelperFunction({
|
|
203
|
+
returns,
|
|
204
|
+
language,
|
|
205
|
+
volatility,
|
|
206
|
+
strict: /\bSTRICT\b/.test(words),
|
|
207
|
+
parallel,
|
|
208
|
+
securityDefiner: false,
|
|
209
|
+
leakproof: false,
|
|
210
|
+
cost: 100,
|
|
211
|
+
hasConfig: false,
|
|
212
|
+
body
|
|
213
|
+
})
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/** The name a rendered `CREATE [UNIQUE] INDEX [CONCURRENTLY] IF NOT EXISTS "<name>"` creates. */
|
|
217
|
+
var indexNameOf = (statement) => /^\s*CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:CONCURRENTLY\s+)?IF\s+NOT\s+EXISTS\s+"([^"]+)"/i.exec(statement)?.[1];
|
|
218
|
+
/** The extension a rendered `CREATE EXTENSION IF NOT EXISTS <name> …` installs. */
|
|
219
|
+
var extensionNameOf = (statement) => /^\s*CREATE\s+EXTENSION\s+IF\s+NOT\s+EXISTS\s+"?([\w-]+)"?/i.exec(statement)?.[1];
|
|
220
|
+
/**
|
|
162
221
|
* Whether a column is part of the table's primary key.
|
|
163
222
|
*
|
|
164
223
|
* Those are created with the table, in step 2, and never added afterwards: a
|
|
@@ -170,6 +229,12 @@ function assertSafeIdentifier(value, what) {
|
|
|
170
229
|
* key would be created twice.
|
|
171
230
|
*/
|
|
172
231
|
var isKeyColumn = (table, column) => !column.columnOwnedByProperty && table.primaryKey.includes(column.column);
|
|
232
|
+
/**
|
|
233
|
+
* A declared key whose default is the built-in uuid generator: the default
|
|
234
|
+
* `isId: "uuid"` plans. It is the one declared-key default boot sets on an
|
|
235
|
+
* existing table, because it is the one that cannot fail there.
|
|
236
|
+
*/
|
|
237
|
+
var isUuidKeyDefault = (column) => column.source.kind === "property" && column.default?.kind === "sql" && column.default.expression === "gen_random_uuid()";
|
|
173
238
|
/** Which pass a column belongs to. The order below is the order they run in. */
|
|
174
239
|
var isScalarProperty = (table, column) => (column.source.kind === "property" || column.source.kind === "implicit-id") && !isKeyColumn(table, column);
|
|
175
240
|
/**
|
|
@@ -222,16 +287,24 @@ function diffPlanAgainstCatalogue(plan, existing, options = {}) {
|
|
|
222
287
|
sql: `CREATE TYPE "${enumPlan.schema}"."${enumPlan.name}" AS ENUM (${enumPlan.labels.map(quoteSqlLiteral).join(", ")});`
|
|
223
288
|
});
|
|
224
289
|
}
|
|
225
|
-
for (const statement of plan.extensions)
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
290
|
+
for (const statement of plan.extensions) {
|
|
291
|
+
const extension = extensionNameOf(statement);
|
|
292
|
+
if (extension && existing.extensions?.has(extension)) continue;
|
|
293
|
+
actions.push({
|
|
294
|
+
kind: "create-extension",
|
|
295
|
+
target: statement.replace(/^CREATE EXTENSION IF NOT EXISTS |;$/g, ""),
|
|
296
|
+
sql: statement
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
for (const statement of plan.functions) {
|
|
300
|
+
const helper = parseHelperFunction(statement);
|
|
301
|
+
if (helper && existing.functions?.get(helper.key) === helper.definition) continue;
|
|
302
|
+
actions.push({
|
|
303
|
+
kind: "create-function",
|
|
304
|
+
target: functionTarget(statement),
|
|
305
|
+
sql: statement
|
|
306
|
+
});
|
|
307
|
+
}
|
|
235
308
|
const created = /* @__PURE__ */ new Set();
|
|
236
309
|
for (const table of collectionTables) {
|
|
237
310
|
if (existing.tables.has(table.qualified) || created.has(table.qualified)) continue;
|
|
@@ -418,7 +491,8 @@ function diffPlanAgainstCatalogue(plan, existing, options = {}) {
|
|
|
418
491
|
planColumn(table, column);
|
|
419
492
|
}
|
|
420
493
|
for (const column of table.columns) {
|
|
421
|
-
if (
|
|
494
|
+
if (!column.primaryKey) continue;
|
|
495
|
+
if (column.source.kind !== "implicit-id" && !isUuidKeyDefault(column)) continue;
|
|
422
496
|
if (existing.tables.get(table.qualified)?.has(column.column) !== true) continue;
|
|
423
497
|
planMissingDefault(table, column);
|
|
424
498
|
}
|
|
@@ -494,32 +568,92 @@ function diffPlanAgainstCatalogue(plan, existing, options = {}) {
|
|
|
494
568
|
});
|
|
495
569
|
}
|
|
496
570
|
const concurrent = (schema, table) => !created.has(`${schema}.${table}`);
|
|
571
|
+
const indexExists = (schema, name) => name !== void 0 && existing.indexes?.has(`${schema}.${name}`) === true;
|
|
572
|
+
const deferredIndexes = [];
|
|
573
|
+
const toBuilder = (schema, table, derived) => concurrent(schema, table) && (derived || options.deferLiveIndexes === true);
|
|
574
|
+
const deferIndex = (build) => {
|
|
575
|
+
const invalid = existing.invalidIndexes?.has(`${build.schema}.${build.indexName}`) === true;
|
|
576
|
+
if (indexExists(build.schema, build.indexName) && !invalid) return;
|
|
577
|
+
deferredIndexes.push({
|
|
578
|
+
...build,
|
|
579
|
+
invalid
|
|
580
|
+
});
|
|
581
|
+
};
|
|
582
|
+
/** A search or vector statement in its concurrent form, or undefined if it has no plain `CREATE INDEX IF NOT EXISTS`. */
|
|
583
|
+
const concurrentForm = (statement) => {
|
|
584
|
+
const rewritten = statement.replace("CREATE INDEX IF NOT EXISTS", "CREATE INDEX CONCURRENTLY IF NOT EXISTS");
|
|
585
|
+
return rewritten === statement ? void 0 : rewritten;
|
|
586
|
+
};
|
|
497
587
|
for (const table of collectionTables) {
|
|
498
588
|
if (!table.search) continue;
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
sql
|
|
503
|
-
|
|
589
|
+
const { schema, table: tableName } = table.search;
|
|
590
|
+
for (const statement of searchIndexStatements(table.search)) {
|
|
591
|
+
const name = indexNameOf(statement);
|
|
592
|
+
const sql = concurrentForm(statement);
|
|
593
|
+
if (name !== void 0 && sql !== void 0 && toBuilder(schema, tableName, false)) {
|
|
594
|
+
deferIndex({
|
|
595
|
+
schema,
|
|
596
|
+
table: tableName,
|
|
597
|
+
indexName: name,
|
|
598
|
+
sql
|
|
599
|
+
});
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (indexExists(table.schema, name)) continue;
|
|
603
|
+
actions.push({
|
|
604
|
+
kind: "create-index",
|
|
605
|
+
target: table.qualified,
|
|
606
|
+
sql: concurrent(table.schema, table.table) ? statement.replace("CREATE INDEX IF NOT EXISTS", "CREATE INDEX CONCURRENTLY IF NOT EXISTS") : statement
|
|
607
|
+
});
|
|
608
|
+
}
|
|
504
609
|
}
|
|
505
610
|
const vectorIndexSkipped = [];
|
|
506
611
|
for (const table of collectionTables) {
|
|
507
612
|
if (!table.vector) continue;
|
|
508
|
-
for (const spec of table.vector.specs)
|
|
613
|
+
for (const spec of table.vector.specs) {
|
|
614
|
+
const sql = concurrentForm(vectorIndexStatement(spec));
|
|
615
|
+
if (sql !== void 0 && toBuilder(spec.schema, spec.table, false)) {
|
|
616
|
+
deferIndex({
|
|
617
|
+
schema: spec.schema,
|
|
618
|
+
table: spec.table,
|
|
619
|
+
indexName: spec.indexName,
|
|
620
|
+
sql
|
|
621
|
+
});
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
if (indexExists(spec.schema, spec.indexName)) continue;
|
|
625
|
+
actions.push({
|
|
626
|
+
kind: "create-index",
|
|
627
|
+
target: `${spec.schema}.${spec.table}`,
|
|
628
|
+
sql: concurrent(spec.schema, spec.table) ? vectorIndexStatement(spec).replace("CREATE INDEX IF NOT EXISTS", "CREATE INDEX CONCURRENTLY IF NOT EXISTS") : vectorIndexStatement(spec)
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
vectorIndexSkipped.push(...table.vector.skipped);
|
|
632
|
+
}
|
|
633
|
+
for (const spec of sortIndexSpecs([...collectionTables, ...junctionTables].flatMap((t) => t.indexes))) {
|
|
634
|
+
if (toBuilder(spec.schema, spec.table, spec.derived === "foreign-key")) {
|
|
635
|
+
deferIndex({
|
|
636
|
+
schema: spec.schema,
|
|
637
|
+
table: spec.table,
|
|
638
|
+
indexName: spec.indexName,
|
|
639
|
+
sql: collectionIndexStatement(spec, {
|
|
640
|
+
concurrently: true,
|
|
641
|
+
ifNotExists: true
|
|
642
|
+
}),
|
|
643
|
+
...spec.unique ? { unique: true } : {}
|
|
644
|
+
});
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
if (indexExists(spec.schema, spec.indexName)) continue;
|
|
648
|
+
actions.push({
|
|
509
649
|
kind: "create-index",
|
|
510
650
|
target: `${spec.schema}.${spec.table}`,
|
|
511
|
-
sql:
|
|
651
|
+
sql: collectionIndexStatement(spec, {
|
|
652
|
+
concurrently: concurrent(spec.schema, spec.table),
|
|
653
|
+
ifNotExists: true
|
|
654
|
+
})
|
|
512
655
|
});
|
|
513
|
-
vectorIndexSkipped.push(...table.vector.skipped);
|
|
514
656
|
}
|
|
515
|
-
for (const spec of sortIndexSpecs(collectionTables.flatMap((t) => t.indexes))) actions.push({
|
|
516
|
-
kind: "create-index",
|
|
517
|
-
target: `${spec.schema}.${spec.table}`,
|
|
518
|
-
sql: collectionIndexStatement(spec, {
|
|
519
|
-
concurrently: concurrent(spec.schema, spec.table),
|
|
520
|
-
ifNotExists: true
|
|
521
|
-
})
|
|
522
|
-
});
|
|
523
657
|
for (const table of collectionTables) for (const trigger of table.triggers) {
|
|
524
658
|
if (existing.triggers?.has(`${table.qualified}.${trigger.name}`)) continue;
|
|
525
659
|
for (const statement of triggerStatements(trigger)) actions.push({
|
|
@@ -558,7 +692,8 @@ function diffPlanAgainstCatalogue(plan, existing, options = {}) {
|
|
|
558
692
|
vectorIndexSkipped,
|
|
559
693
|
withheldConstraints,
|
|
560
694
|
columnTypeDrift,
|
|
561
|
-
orphanedRequiredColumns
|
|
695
|
+
orphanedRequiredColumns,
|
|
696
|
+
deferredIndexes
|
|
562
697
|
};
|
|
563
698
|
}
|
|
564
699
|
/** What a `CREATE FUNCTION` statement is called, for the action log. */
|
|
@@ -567,347 +702,350 @@ var functionTarget = (statement) => {
|
|
|
567
702
|
return statement.includes("unaccent") ? SEARCH_UNACCENT_FN : SEARCH_TEXT_FN;
|
|
568
703
|
};
|
|
569
704
|
//#endregion
|
|
570
|
-
//#region src/schema/
|
|
705
|
+
//#region src/schema/deferred-index-build.ts
|
|
706
|
+
var deferred_index_build_exports = /* @__PURE__ */ __exportAll({
|
|
707
|
+
TABLE_BUSY_REASON: () => TABLE_BUSY_REASON,
|
|
708
|
+
buildDeferredIndexes: () => buildDeferredIndexes,
|
|
709
|
+
isPgPool: () => isPgPool,
|
|
710
|
+
poolIndexBuildSession: () => poolIndexBuildSession,
|
|
711
|
+
scheduleDeferredIndexBuild: () => scheduleDeferredIndexBuild
|
|
712
|
+
});
|
|
713
|
+
/** A client-side read timeout no index build reaches. A pool's is 60 s. */
|
|
714
|
+
var INDEX_BUILD_READ_TIMEOUT_MS = 1440 * 60 * 1e3;
|
|
715
|
+
/** A `pg.Pool` — what `connect()` checks a client out of — and not a `pg.Client`, whose `connect()` opens a socket. */
|
|
716
|
+
var isPgPool = (value) => typeof value === "object" && value !== null && "connect" in value && typeof value.connect === "function" && "totalCount" in value;
|
|
571
717
|
/**
|
|
572
|
-
*
|
|
573
|
-
*
|
|
574
|
-
* ## What this module is
|
|
575
|
-
*
|
|
576
|
-
* A managed runtime boots someone else's compiled project against a database it
|
|
577
|
-
* has never seen. Auth tables are ensured at boot already, but collection tables
|
|
578
|
-
* were not created by anything: the platform ran the app and every `/api/data/*`
|
|
579
|
-
* request answered 500 on a missing relation. `rebase db push` cannot help — it
|
|
580
|
-
* is an Atlas-driven CLI command, and the runtime image ships no CLI.
|
|
581
|
-
*
|
|
582
|
-
* Two halves, and only one of them is here. *What the collections ask for* is
|
|
583
|
-
* `schema/plan/plan-schema.ts`, shared with `db push` and the generated Drizzle
|
|
584
|
-
* file; *what the database already has, and which of those statements are safe
|
|
585
|
-
* to run against it* is `schema/plan/diff-plan.ts`. This module reads the
|
|
586
|
-
* catalogue, runs the statements, and reports what happened.
|
|
718
|
+
* Sessions checked out of a `pg.Pool`, one connection each.
|
|
587
719
|
*
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
*
|
|
592
|
-
*
|
|
720
|
+
* Its own connection rather than the pool's next free one, because a build
|
|
721
|
+
* has to outlive both of a pool's timeouts: the server-side
|
|
722
|
+
* `statement_timeout` (30 s on this framework's pools), which the builder
|
|
723
|
+
* lifts for the session — something it may only do on a connection nobody
|
|
724
|
+
* else is handed until it is reset — and the client-side read timeout, which
|
|
725
|
+
* is lifted here per query.
|
|
593
726
|
*/
|
|
594
|
-
var
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
}
|
|
727
|
+
var poolIndexBuildSession = (pool) => async () => {
|
|
728
|
+
const client = await pool.connect();
|
|
729
|
+
return {
|
|
730
|
+
dedicated: true,
|
|
731
|
+
async query(text) {
|
|
732
|
+
const config = {
|
|
733
|
+
text,
|
|
734
|
+
query_timeout: INDEX_BUILD_READ_TIMEOUT_MS
|
|
735
|
+
};
|
|
736
|
+
return { rows: (await client.query(config)).rows };
|
|
737
|
+
},
|
|
738
|
+
release: () => client.release()
|
|
739
|
+
};
|
|
740
|
+
};
|
|
741
|
+
/** `42P01 undefined_table` — `pg_stat_progress_create_index` before Postgres 12. */
|
|
742
|
+
var isUndefinedTable = (err) => extractPgError(err)?.code === "42P01";
|
|
743
|
+
/** `25001 active_sql_transaction` — `CONCURRENTLY` on a connection inside a transaction block. */
|
|
744
|
+
var isTransactionBlockRefusal = (err) => extractPgError(err)?.code === "25001";
|
|
605
745
|
/**
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
* A plan and a diff: the collections are read once, by `planSchema`, into the
|
|
610
|
-
* same {@link SchemaPlan} `db push` renders `schema.sql` from, and the diff
|
|
611
|
-
* subtracts what the database already has. Before that split this function had
|
|
612
|
-
* its own reading of every `Property`, and the audit found twelve places where
|
|
613
|
-
* it disagreed with the two generators — a required `author_id` that was
|
|
614
|
-
* NOT NULL after a push and nullable after a boot among them, on the one path
|
|
615
|
-
* with no developer in the loop.
|
|
746
|
+
* The rows themselves violate a UNIQUE index being built: `23505` naming the
|
|
747
|
+
* index (a lost catalogue race names a `pg_` index instead). Building it again
|
|
748
|
+
* would only fail again, so it is not retried.
|
|
616
749
|
*/
|
|
617
|
-
|
|
618
|
-
|
|
750
|
+
var isDuplicateRows = (err, index) => {
|
|
751
|
+
const pg = extractPgError(err);
|
|
752
|
+
return pg?.code === "23505" && (pg.constraint === index.indexName || /could not create unique index/i.test(pg.message));
|
|
753
|
+
};
|
|
754
|
+
var CONCURRENTLY_RE = /\sCONCURRENTLY\s/i;
|
|
755
|
+
/** Why an index is skipped while a build of another index holds its table. */
|
|
756
|
+
var TABLE_BUSY_REASON = "another index on the table is being built";
|
|
757
|
+
/**
|
|
758
|
+
* Run a `CONCURRENTLY` statement — or its plain form, when the connection
|
|
759
|
+
* turns out to be inside a transaction block. Only a shared handle can be: an
|
|
760
|
+
* application that builds its own adapter may hand over a connection already
|
|
761
|
+
* in one. The plain form is always accepted and takes a write lock for the
|
|
762
|
+
* build, which is the cost of getting the index at all — the same fallback the
|
|
763
|
+
* ensure's own index actions have.
|
|
764
|
+
*/
|
|
765
|
+
async function runConcurrently(session, sql, qualified) {
|
|
766
|
+
try {
|
|
767
|
+
await session.query(sql);
|
|
768
|
+
} catch (err) {
|
|
769
|
+
if (!isTransactionBlockRefusal(err) || !CONCURRENTLY_RE.test(sql)) throw err;
|
|
770
|
+
logger.warn(`[schema] ${qualified}: this connection is inside a transaction, so the index is built with a write lock rather than concurrently. Writes to the table block until it finishes.`);
|
|
771
|
+
await session.query(sql.replace(CONCURRENTLY_RE, " "));
|
|
772
|
+
}
|
|
619
773
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
const
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
774
|
+
async function readIndexState(session, index) {
|
|
775
|
+
const schema = quoteSqlLiteral(index.schema);
|
|
776
|
+
const name = quoteSqlLiteral(index.indexName);
|
|
777
|
+
const { rows } = await session.query(`SELECT i.indisvalid AS valid
|
|
778
|
+
FROM pg_index i
|
|
779
|
+
JOIN pg_class c ON c.oid = i.indexrelid
|
|
780
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
781
|
+
WHERE n.nspname = ${schema} AND c.relname = ${name}`);
|
|
782
|
+
const valid = rows.length === 0 ? null : rows[0].valid === true;
|
|
783
|
+
if (valid !== false) return {
|
|
784
|
+
valid,
|
|
785
|
+
building: false
|
|
627
786
|
};
|
|
628
|
-
const inList = schemas.map((schema) => `'${assertSafeIdentifier(schema, "schema name")}'`).join(", ");
|
|
629
|
-
const notNullColumns = /* @__PURE__ */ new Set();
|
|
630
|
-
const columnTypes = /* @__PURE__ */ new Map();
|
|
631
|
-
const columnDefaults = /* @__PURE__ */ new Set();
|
|
632
|
-
const { rows: columns } = await client.query(`SELECT table_schema, table_name, column_name, is_nullable, udt_name, column_default,
|
|
633
|
-
numeric_precision, numeric_scale
|
|
634
|
-
FROM information_schema.columns
|
|
635
|
-
WHERE table_schema IN (${inList})`);
|
|
636
|
-
for (const row of columns) {
|
|
637
|
-
const key = `${row.table_schema}.${row.table_name}`;
|
|
638
|
-
if (!tables.has(key)) tables.set(key, /* @__PURE__ */ new Set());
|
|
639
|
-
tables.get(key).add(row.column_name);
|
|
640
|
-
if (row.is_nullable === "NO") notNullColumns.add(`${key}.${row.column_name}`);
|
|
641
|
-
if (row.udt_name) {
|
|
642
|
-
const modifier = row.udt_name === "numeric" && row.numeric_precision !== null ? `(${row.numeric_precision},${row.numeric_scale ?? 0})` : "";
|
|
643
|
-
columnTypes.set(`${key}.${row.column_name}`, `${row.udt_name}${modifier}`);
|
|
644
|
-
}
|
|
645
|
-
if (row.column_default !== null) columnDefaults.add(`${key}.${row.column_name}`);
|
|
646
|
-
}
|
|
647
|
-
const triggers = /* @__PURE__ */ new Set();
|
|
648
|
-
const { rows: triggerRows } = await client.query(`SELECT n.nspname AS schema, c.relname AS table, t.tgname AS name
|
|
649
|
-
FROM pg_trigger t
|
|
650
|
-
JOIN pg_class c ON t.tgrelid = c.oid
|
|
651
|
-
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
652
|
-
WHERE NOT t.tgisinternal AND n.nspname IN (${inList})`);
|
|
653
|
-
for (const row of triggerRows) triggers.add(`${row.schema}.${row.table}.${row.name}`);
|
|
654
|
-
const populatedTables = /* @__PURE__ */ new Set();
|
|
655
|
-
const { rows: realTables } = await client.query(`SELECT n.nspname AS schema, c.relname AS name
|
|
656
|
-
FROM pg_class c
|
|
657
|
-
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
658
|
-
WHERE c.relkind IN ('r', 'p') AND n.nspname IN (${inList})`);
|
|
659
|
-
if (realTables.length > 0) {
|
|
660
|
-
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`);
|
|
661
|
-
const { rows: populationRows } = await client.query(probes.join(" UNION ALL "));
|
|
662
|
-
for (const row of populationRows) if (row.populated) populatedTables.add(row.key);
|
|
663
|
-
}
|
|
664
|
-
const enumValues = /* @__PURE__ */ new Map();
|
|
665
|
-
const { rows: enumValueRows } = await client.query(`SELECT n.nspname AS schema, t.typname AS name, e.enumlabel AS value
|
|
666
|
-
FROM pg_enum e
|
|
667
|
-
JOIN pg_type t ON e.enumtypid = t.oid
|
|
668
|
-
JOIN pg_namespace n ON t.typnamespace = n.oid
|
|
669
|
-
WHERE n.nspname IN (${inList})
|
|
670
|
-
ORDER BY t.typname, e.enumsortorder`);
|
|
671
|
-
for (const row of enumValueRows) {
|
|
672
|
-
const key = `${row.schema}.${row.name}`;
|
|
673
|
-
if (!enumValues.has(key)) enumValues.set(key, []);
|
|
674
|
-
enumValues.get(key).push(row.value);
|
|
675
|
-
}
|
|
676
|
-
const { rows: enumRows } = await client.query(`SELECT n.nspname AS schema, t.typname AS name
|
|
677
|
-
FROM pg_type t
|
|
678
|
-
JOIN pg_namespace n ON t.typnamespace = n.oid
|
|
679
|
-
WHERE t.typtype = 'e' AND n.nspname IN (${inList})`);
|
|
680
|
-
for (const row of enumRows) enums.add(`${row.schema}.${row.name}`);
|
|
681
|
-
const constraints = /* @__PURE__ */ new Set();
|
|
682
|
-
const { rows: constraintRows } = await client.query(`SELECT n.nspname AS schema, c.relname AS table, con.conname AS name
|
|
683
|
-
FROM pg_constraint con
|
|
684
|
-
JOIN pg_class c ON con.conrelid = c.oid
|
|
685
|
-
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
686
|
-
WHERE n.nspname IN (${inList})`);
|
|
687
|
-
for (const row of constraintRows) constraints.add(`${row.schema}.${row.table}.${row.name}`);
|
|
688
|
-
const columnComments = /* @__PURE__ */ new Map();
|
|
689
|
-
const { rows: commentRows } = await client.query(`SELECT n.nspname AS schema, c.relname AS table, a.attname AS column, d.description AS comment
|
|
690
|
-
FROM pg_description d
|
|
691
|
-
JOIN pg_class c ON d.objoid = c.oid
|
|
692
|
-
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
693
|
-
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = d.objsubid
|
|
694
|
-
WHERE d.objsubid > 0 AND n.nspname IN (${inList})`);
|
|
695
|
-
for (const row of commentRows) {
|
|
696
|
-
if (row.comment == null) continue;
|
|
697
|
-
columnComments.set(`${row.schema}.${row.table}.${row.column}`, row.comment);
|
|
698
|
-
}
|
|
699
787
|
return {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
constraints,
|
|
703
|
-
columnComments,
|
|
704
|
-
enumValues,
|
|
705
|
-
notNullColumns,
|
|
706
|
-
populatedTables,
|
|
707
|
-
columnTypes,
|
|
708
|
-
columnDefaults,
|
|
709
|
-
triggers
|
|
788
|
+
valid,
|
|
789
|
+
building: await tableIsBeingIndexed(session, index)
|
|
710
790
|
};
|
|
711
791
|
}
|
|
712
792
|
/**
|
|
713
|
-
*
|
|
793
|
+
* Wait, within the boot's lock budget, until the table's lock is free.
|
|
714
794
|
*
|
|
715
|
-
*
|
|
716
|
-
*
|
|
717
|
-
*
|
|
718
|
-
*
|
|
795
|
+
* A probe rather than a `lock_timeout` on the build itself: the build waits on
|
|
796
|
+
* lock *queues* in three phases, and only the first — getting the table — is
|
|
797
|
+
* a wait worth bounding. The other two wait for transactions that were
|
|
798
|
+
* already running, which block nobody while they run and must simply be let
|
|
799
|
+
* finish. `true` when the table was free (or cannot be probed on this
|
|
800
|
+
* handle), `false` when something held it for the whole budget.
|
|
719
801
|
*/
|
|
720
|
-
function
|
|
721
|
-
|
|
802
|
+
async function tableLockIsFree(session, index, options) {
|
|
803
|
+
const lock = resolveBootLockOptions(options);
|
|
804
|
+
const probe = atomicBatch([`LOCK TABLE "${index.schema}"."${index.table}" IN SHARE UPDATE EXCLUSIVE MODE`], lock.lockTimeoutMs);
|
|
805
|
+
const deadline = Date.now() + (options.retryBudgetMs ?? 1e4);
|
|
806
|
+
for (let round = 1;; round++) try {
|
|
807
|
+
await session.query(probe);
|
|
808
|
+
return true;
|
|
809
|
+
} catch (err) {
|
|
810
|
+
if (isMultiStatementRefusal(err)) return true;
|
|
811
|
+
if (!isLockTimeout(err)) throw err;
|
|
812
|
+
if (round >= lock.attempts || Date.now() >= deadline) return false;
|
|
813
|
+
await sleep(Math.min(lock.backoffMs(round), Math.max(0, deadline - Date.now())));
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
/** Whether another backend is running `CREATE INDEX` on the index's table right now. */
|
|
817
|
+
async function tableIsBeingIndexed(session, index) {
|
|
818
|
+
const table = quoteSqlLiteral(`"${index.schema}"."${index.table}"`);
|
|
819
|
+
try {
|
|
820
|
+
const { rows } = await session.query(`SELECT EXISTS (
|
|
821
|
+
SELECT 1 FROM pg_stat_progress_create_index
|
|
822
|
+
WHERE relid = to_regclass(${table}) AND pid <> pg_backend_pid()
|
|
823
|
+
) AS building`);
|
|
824
|
+
return rows[0]?.building === true;
|
|
825
|
+
} catch (err) {
|
|
826
|
+
if (isUndefinedTable(err)) return false;
|
|
827
|
+
throw err;
|
|
828
|
+
}
|
|
722
829
|
}
|
|
723
830
|
/**
|
|
724
|
-
*
|
|
725
|
-
* lives in.
|
|
831
|
+
* The advisory-lock key one index is claimed under, across every instance.
|
|
726
832
|
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
*
|
|
730
|
-
*
|
|
833
|
+
* Without a claim, instances booting together all issue the same
|
|
834
|
+
* `CREATE INDEX CONCURRENTLY`: one wins, the others skip by name — reporting
|
|
835
|
+
* success while the winner is still building — or wait on each other's
|
|
836
|
+
* transactions until Postgres breaks the deadlock by cancelling one, which
|
|
837
|
+
* leaves a husk. With it, one instance builds and the rest step aside.
|
|
731
838
|
*/
|
|
732
|
-
|
|
733
|
-
const relational = relationalCollections(collections);
|
|
734
|
-
return readExistingSchema(client, Array.from(/* @__PURE__ */ new Set([...relational.map(schemaOf), ...planJunctionTables(relational).map((junction) => junction.schema)])));
|
|
735
|
-
}
|
|
839
|
+
var claimKey = (index) => `hashtext(${quoteSqlLiteral(`rebase_fk_index:${index.schema}.${index.indexName}`)})`;
|
|
736
840
|
/**
|
|
737
|
-
*
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
* cannot be added, say) should not roll back the tables that were created fine.
|
|
742
|
-
* The error is surfaced with the statement that caused it.
|
|
841
|
+
* Build one index under its claim — session-level, so only on a connection
|
|
842
|
+
* of our own. On a shared handle the claim could be taken on one backend and
|
|
843
|
+
* released on another, so there is none, and concurrent instances fall back
|
|
844
|
+
* on the state checks below.
|
|
743
845
|
*/
|
|
744
|
-
async function
|
|
745
|
-
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
for (const legacy of plan.legacyForeignKeys) {
|
|
755
|
-
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.`;
|
|
756
|
-
logger.info(`[schema] ${message}`);
|
|
757
|
-
log?.(message);
|
|
758
|
-
}
|
|
759
|
-
if (plan.searchDrift.length > 0) throw new Error(searchDriftMessage(plan.searchDrift));
|
|
760
|
-
for (const adopted of plan.searchAdopted) {
|
|
761
|
-
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.`;
|
|
762
|
-
logger.info(`[schema] ${message}`);
|
|
763
|
-
log?.(message);
|
|
764
|
-
}
|
|
765
|
-
for (const skip of plan.vectorIndexSkipped) {
|
|
766
|
-
const message = `No ANN index on "${skip.table}"."${skip.column}": ${skip.reason}`;
|
|
767
|
-
logger.warn(`[schema] ${message}`);
|
|
768
|
-
log?.(message);
|
|
769
|
-
}
|
|
770
|
-
for (const withheld of plan.withheldConstraints) {
|
|
771
|
-
const message = `No ${withheld.kind === "unique" ? "UNIQUE" : "NOT NULL"} on "${withheld.target}": ${withheld.reason} ${withheld.remedy}`;
|
|
772
|
-
logger.warn(`[schema] ${message}`);
|
|
773
|
-
log?.(message);
|
|
774
|
-
}
|
|
775
|
-
for (const drift of plan.columnTypeDrift) {
|
|
776
|
-
const message = columnTypeDriftMessage(drift);
|
|
777
|
-
logger.warn(`[schema] ${message}`);
|
|
778
|
-
log?.(message);
|
|
846
|
+
async function buildClaimed(session, index, options, outcome) {
|
|
847
|
+
if (!session.dedicated) return buildOne(session, index, options, outcome);
|
|
848
|
+
const { rows } = await session.query(`SELECT pg_try_advisory_lock(${claimKey(index)}) AS claimed`);
|
|
849
|
+
if (rows[0]?.claimed !== true) {
|
|
850
|
+
outcome.skipped.push({
|
|
851
|
+
index: `${index.schema}.${index.indexName}`,
|
|
852
|
+
reason: "another instance is building it"
|
|
853
|
+
});
|
|
854
|
+
options.log?.(`${index.schema}.${index.indexName}: another instance is building it — leaving it to that one`);
|
|
855
|
+
return;
|
|
779
856
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
logger.warn(`[schema] ${message}`);
|
|
785
|
-
log?.(message);
|
|
857
|
+
try {
|
|
858
|
+
await buildOne(session, index, options, outcome);
|
|
859
|
+
} finally {
|
|
860
|
+
await session.query(`SELECT pg_advisory_unlock(${claimKey(index)})`);
|
|
786
861
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
if (
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
if (action.kind === "add-constraint" || action.kind === "comment-column" || action.kind === "create-index" && !/CREATE\s+UNIQUE\s+INDEX/i.test(action.sql)) {
|
|
800
|
-
failures.push({
|
|
801
|
-
kind: action.kind,
|
|
802
|
-
target: action.target,
|
|
803
|
-
error: message
|
|
862
|
+
}
|
|
863
|
+
/** One index, from whatever state it is in to valid — or a reason it is not. */
|
|
864
|
+
async function buildOne(session, index, options, outcome) {
|
|
865
|
+
const qualified = `${index.schema}.${index.indexName}`;
|
|
866
|
+
const target = `"${index.schema}"."${index.indexName}"`;
|
|
867
|
+
const say = (message) => options.log?.(message);
|
|
868
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
869
|
+
const before = await readIndexState(session, index);
|
|
870
|
+
if (before.valid === true) {
|
|
871
|
+
if (attempt === 1) outcome.skipped.push({
|
|
872
|
+
index: qualified,
|
|
873
|
+
reason: "already built"
|
|
804
874
|
});
|
|
805
|
-
|
|
875
|
+
else outcome.built.push(qualified);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
if (before.building) {
|
|
879
|
+
outcome.skipped.push({
|
|
880
|
+
index: qualified,
|
|
881
|
+
reason: "another instance is building it"
|
|
882
|
+
});
|
|
883
|
+
say(`${qualified}: another instance is building it — leaving it to that one`);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
if (!await tableLockIsFree(session, index, options)) {
|
|
887
|
+
if (await tableIsBeingIndexed(session, index)) {
|
|
888
|
+
outcome.skipped.push({
|
|
889
|
+
index: qualified,
|
|
890
|
+
reason: TABLE_BUSY_REASON
|
|
891
|
+
});
|
|
892
|
+
say(`${qualified}: another index on "${index.schema}"."${index.table}" is being built — leaving it for now`);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
outcome.failed.push({
|
|
896
|
+
index: qualified,
|
|
897
|
+
error: `"${index.schema}"."${index.table}" stayed locked by another session for the whole lock budget (an index build, a VACUUM, an ALTER TABLE). Nothing was changed; the next boot tries again.`
|
|
898
|
+
});
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
if (before.valid === false) {
|
|
902
|
+
say(`${qualified}: an earlier build left it INVALID — dropping it to build it again`);
|
|
903
|
+
await runConcurrently(session, `DROP INDEX CONCURRENTLY IF EXISTS ${target};`, qualified);
|
|
904
|
+
}
|
|
905
|
+
const started = Date.now();
|
|
906
|
+
say(`${qualified}: building on "${index.schema}"."${index.table}"…`);
|
|
907
|
+
try {
|
|
908
|
+
await runConcurrently(session, index.sql, qualified);
|
|
909
|
+
} catch (err) {
|
|
910
|
+
const after = await readIndexState(session, index).catch(() => void 0);
|
|
911
|
+
if (after?.valid === true || after?.building) {
|
|
912
|
+
outcome.skipped.push({
|
|
913
|
+
index: qualified,
|
|
914
|
+
reason: "another instance built it"
|
|
915
|
+
});
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
if (after?.valid === false && attempt === 1 && !isDuplicateRows(err, index)) {
|
|
919
|
+
say(`${qualified}: the build failed (${describe(err)}); retrying once`);
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
outcome.failed.push({
|
|
923
|
+
index: qualified,
|
|
924
|
+
error: describe(err)
|
|
925
|
+
});
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
const after = await readIndexState(session, index);
|
|
929
|
+
if (after.valid === true) {
|
|
930
|
+
outcome.built.push(qualified);
|
|
931
|
+
say(`${qualified}: built in ${((Date.now() - started) / 1e3).toFixed(1)} s`);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
if (after.building) {
|
|
935
|
+
outcome.skipped.push({
|
|
936
|
+
index: qualified,
|
|
937
|
+
reason: "another instance is building it"
|
|
938
|
+
});
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
if (attempt === 2) {
|
|
942
|
+
outcome.failed.push({
|
|
943
|
+
index: qualified,
|
|
944
|
+
error: "left INVALID by two builds in a row"
|
|
945
|
+
});
|
|
946
|
+
return;
|
|
806
947
|
}
|
|
807
|
-
throw new Error(`Failed to ${action.kind} ${action.target}: ${message}${vectorExtensionHint(message)}\n ${action.sql}`);
|
|
808
948
|
}
|
|
809
|
-
return {
|
|
810
|
-
...plan,
|
|
811
|
-
failures
|
|
812
|
-
};
|
|
813
949
|
}
|
|
814
|
-
|
|
815
|
-
* What actually went wrong, rather than what the ORM said about it.
|
|
816
|
-
*
|
|
817
|
-
* Drizzle wraps every driver error, and its `message` is literally
|
|
818
|
-
* ``Failed query: ${sql}\nparams: ${params}`` — the statement we already know,
|
|
819
|
-
* and not one word of Postgres's answer. Reporting `err.message` therefore
|
|
820
|
-
* produced a failure that named the statement twice and the *reason* zero
|
|
821
|
-
* times, so four unrelated boot failures — a missing extension, a wrong
|
|
822
|
-
* privilege, a bad index method, a constraint the rows violate — all read
|
|
823
|
-
* identically to whoever had to fix them.
|
|
824
|
-
*
|
|
825
|
-
* Two consequences, both observed in the field before this existed:
|
|
826
|
-
*
|
|
827
|
-
* 1. On a managed deployment the reason never reaches anybody. Boot dies, the
|
|
828
|
-
* pod never goes ready, and the deploy reports a readiness timeout — so the
|
|
829
|
-
* one line that says why is the one line that was thrown away here.
|
|
830
|
-
* 2. {@link vectorExtensionHint} matches on the error text. Against the
|
|
831
|
-
* wrapper it matched nothing, so the hint written specifically to explain a
|
|
832
|
-
* missing pgvector never fired on the path that raises it.
|
|
833
|
-
*
|
|
834
|
-
* `detail` and `hint` come along because they are the fields Postgres uses to
|
|
835
|
-
* say which object it meant and what to do about it — `CREATE EXTENSION` names
|
|
836
|
-
* the missing library there, and a rejected constraint names the row. This
|
|
837
|
-
* writes into the tenant's own boot log, about the tenant's own database.
|
|
838
|
-
*/
|
|
839
|
-
function describeDriverError(err) {
|
|
950
|
+
var describe = (err) => {
|
|
840
951
|
const pg = extractPgError(err);
|
|
841
|
-
if (pg) {
|
|
842
|
-
const parts = [pg.code ? `${pg.message} [${pg.code}]` : pg.message];
|
|
843
|
-
if (pg.detail) parts.push(` detail: ${pg.detail}`);
|
|
844
|
-
if (pg.hint) parts.push(` hint: ${pg.hint}`);
|
|
845
|
-
return parts.join("\n");
|
|
846
|
-
}
|
|
847
|
-
const cause = extractCauseMessage(err);
|
|
848
|
-
if (cause) return cause;
|
|
952
|
+
if (pg) return pg.code ? `${pg.message} [${pg.code}]` : pg.message;
|
|
849
953
|
return err instanceof Error ? err.message : String(err);
|
|
850
|
-
}
|
|
851
|
-
/** ` CONCURRENTLY ` as it appears in a rendered `CREATE [UNIQUE] INDEX`. */
|
|
852
|
-
var CONCURRENTLY_RE = /\sCONCURRENTLY\s/i;
|
|
954
|
+
};
|
|
853
955
|
/**
|
|
854
|
-
*
|
|
956
|
+
* Build every deferred index, one at a time, on one connection.
|
|
855
957
|
*
|
|
856
|
-
*
|
|
857
|
-
*
|
|
858
|
-
* decides to retry a *different* statement, so it must not fire on anything it
|
|
859
|
-
* has not been reasoned about.
|
|
958
|
+
* Never throws for an index: each failure is recorded and the next one is
|
|
959
|
+
* attempted. Throws only when no connection can be had at all.
|
|
860
960
|
*/
|
|
861
|
-
function
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
*/
|
|
887
|
-
async function applyAction(client, action) {
|
|
888
|
-
let sqlText = action.sql;
|
|
889
|
-
for (let attempt = 1;; attempt++) try {
|
|
890
|
-
await client.query(sqlText);
|
|
891
|
-
return true;
|
|
892
|
-
} catch (err) {
|
|
893
|
-
if (isTransactionBlockRefusal(err) && CONCURRENTLY_RE.test(sqlText)) {
|
|
894
|
-
sqlText = sqlText.replace(CONCURRENTLY_RE, " ");
|
|
895
|
-
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.`);
|
|
896
|
-
continue;
|
|
897
|
-
}
|
|
898
|
-
if (isDuplicateObjectRace(err)) {
|
|
899
|
-
logger.debug(`[schema] ${action.kind} ${action.target}: already created by another instance`);
|
|
900
|
-
return false;
|
|
961
|
+
async function buildDeferredIndexes(openSession, indexes, options = {}) {
|
|
962
|
+
const outcome = {
|
|
963
|
+
built: [],
|
|
964
|
+
skipped: [],
|
|
965
|
+
failed: []
|
|
966
|
+
};
|
|
967
|
+
if (indexes.length === 0) return outcome;
|
|
968
|
+
for (const index of indexes) {
|
|
969
|
+
assertSafeIdentifier(index.schema, "schema name");
|
|
970
|
+
assertSafeIdentifier(index.table, "table name");
|
|
971
|
+
assertSafeIdentifier(index.indexName, "index name");
|
|
972
|
+
}
|
|
973
|
+
const session = await openSession();
|
|
974
|
+
try {
|
|
975
|
+
if (session.dedicated) await session.query("SET statement_timeout = 0");
|
|
976
|
+
for (const index of indexes) {
|
|
977
|
+
if (options.cancelled?.()) break;
|
|
978
|
+
try {
|
|
979
|
+
await buildClaimed(session, index, options, outcome);
|
|
980
|
+
} catch (err) {
|
|
981
|
+
outcome.failed.push({
|
|
982
|
+
index: `${index.schema}.${index.indexName}`,
|
|
983
|
+
error: describe(err)
|
|
984
|
+
});
|
|
985
|
+
}
|
|
901
986
|
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
987
|
+
} finally {
|
|
988
|
+
try {
|
|
989
|
+
if (session.dedicated) await session.query("RESET statement_timeout");
|
|
990
|
+
} finally {
|
|
991
|
+
await session.release();
|
|
906
992
|
}
|
|
907
|
-
throw err;
|
|
908
993
|
}
|
|
994
|
+
for (const failure of outcome.failed) logger.warn(`[schema] Index ${failure.index} was not built: ${failure.error}. Queries it would serve scan the table until it is; the next boot tries again.`);
|
|
995
|
+
return outcome;
|
|
996
|
+
}
|
|
997
|
+
/** Every deferred build in this process, chained so they run one at a time. */
|
|
998
|
+
var queue = Promise.resolve();
|
|
999
|
+
function scheduleDeferredIndexBuild(openSession, indexes, options = {}) {
|
|
1000
|
+
let cancelled = false;
|
|
1001
|
+
let timer;
|
|
1002
|
+
let resolveDone;
|
|
1003
|
+
const done = new Promise((resolve) => {
|
|
1004
|
+
resolveDone = resolve;
|
|
1005
|
+
});
|
|
1006
|
+
const run = () => {
|
|
1007
|
+
if (timer) clearTimeout(timer);
|
|
1008
|
+
timer = void 0;
|
|
1009
|
+
if (cancelled) {
|
|
1010
|
+
resolveDone(void 0);
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
const job = queue.then(async () => {
|
|
1014
|
+
if (cancelled) return void 0;
|
|
1015
|
+
logger.info(`[schema] Building ${indexes.length} index(es) in the background, one at a time (CREATE INDEX CONCURRENTLY — reads and writes are not blocked).`);
|
|
1016
|
+
return buildDeferredIndexes(openSession, indexes, {
|
|
1017
|
+
...options,
|
|
1018
|
+
cancelled: () => cancelled
|
|
1019
|
+
});
|
|
1020
|
+
}).catch((err) => {
|
|
1021
|
+
logger.warn(`[schema] Indexes were not built this boot: ${describe(err)}`);
|
|
1022
|
+
});
|
|
1023
|
+
queue = job;
|
|
1024
|
+
job.then(resolveDone);
|
|
1025
|
+
};
|
|
1026
|
+
const arm = (ms) => {
|
|
1027
|
+
if (timer) clearTimeout(timer);
|
|
1028
|
+
timer = setTimeout(run, ms);
|
|
1029
|
+
timer.unref?.();
|
|
1030
|
+
};
|
|
1031
|
+
arm(options.fallbackMs ?? 6e4);
|
|
1032
|
+
return {
|
|
1033
|
+
start: () => {
|
|
1034
|
+
if (cancelled || timer === void 0) return;
|
|
1035
|
+
arm(options.delayMs ?? 0);
|
|
1036
|
+
},
|
|
1037
|
+
cancel: () => {
|
|
1038
|
+
cancelled = true;
|
|
1039
|
+
if (timer) {
|
|
1040
|
+
clearTimeout(timer);
|
|
1041
|
+
timer = void 0;
|
|
1042
|
+
resolveDone(void 0);
|
|
1043
|
+
}
|
|
1044
|
+
},
|
|
1045
|
+
done
|
|
1046
|
+
};
|
|
909
1047
|
}
|
|
910
1048
|
//#endregion
|
|
911
|
-
export {
|
|
1049
|
+
export { assertSafeIdentifier as a, helperFunctionKey as c, HELPER_FUNCTIONS as i, columnTypeDriftMessage as l, buildDeferredIndexes as n, describeHelperFunction as o, deferred_index_build_exports as r, diffPlanAgainstCatalogue as s, TABLE_BUSY_REASON as t, typesAgree as u };
|
|
912
1050
|
|
|
913
|
-
//# sourceMappingURL=
|
|
1051
|
+
//# sourceMappingURL=deferred-index-build-DP1_SNFo.js.map
|