@rebasepro/server-postgres 0.17.3 → 0.18.1
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/LICENSE +0 -1
- package/README.md +31 -0
- package/dist/BranchService-ByLfM_Tt.js +360 -0
- package/dist/BranchService-ByLfM_Tt.js.map +1 -0
- package/dist/__vite-browser-external-BnuHet1e.js +46 -0
- package/dist/__vite-browser-external-BnuHet1e.js.map +1 -0
- package/dist/arg-Dni7MzLB.js +87 -0
- package/dist/arg-Dni7MzLB.js.map +1 -0
- package/dist/auth-users-columns-D2LBFrMH.js +116 -0
- package/dist/auth-users-columns-D2LBFrMH.js.map +1 -0
- package/dist/backup-argv.d.ts +19 -0
- package/dist/backup-cli-oM9WQVlV.js +408 -0
- package/dist/backup-cli-oM9WQVlV.js.map +1 -0
- package/dist/backup-service-Br3tqwsy.js +715 -0
- package/dist/backup-service-Br3tqwsy.js.map +1 -0
- package/dist/branch-argv.d.ts +28 -0
- package/dist/branch-prune.d.ts +77 -0
- package/dist/cli-collections-path.d.ts +31 -0
- package/dist/cli-errors-DxN4yzdH.js +657 -0
- package/dist/cli-errors-DxN4yzdH.js.map +1 -0
- package/dist/cli-errors.d.ts +123 -0
- package/dist/cli-flags.d.ts +101 -0
- package/dist/cli-helpers.d.ts +94 -10
- package/dist/cli-output-CNdMql-L.js +47 -0
- package/dist/cli-output-CNdMql-L.js.map +1 -0
- package/dist/cli.js +2020 -0
- package/dist/cli.js.map +1 -0
- package/dist/collection-index-BHgT_TKd.js +276 -0
- package/dist/collection-index-BHgT_TKd.js.map +1 -0
- package/dist/{collections-schema-version-BMeu3cgv.js → collections-schema-version-T6cGvrDi.js} +1 -2
- package/dist/{collections-schema-version-BMeu3cgv.js.map → collections-schema-version-T6cGvrDi.js.map} +1 -1
- package/dist/{connection-GOKU3Hu5.js → databasePoolManager-Bj5FbeAs.js} +112 -40
- package/dist/databasePoolManager-Bj5FbeAs.js.map +1 -0
- package/dist/databasePoolManager.d.ts +27 -0
- package/dist/doctor-D7HWmGzs.js +974 -0
- package/dist/doctor-D7HWmGzs.js.map +1 -0
- package/dist/{ensure-collection-policies-UCqgv_8c.js → ensure-collection-policies-2c2NV8zX.js} +7 -27
- package/dist/ensure-collection-policies-2c2NV8zX.js.map +1 -0
- package/dist/ensure-collection-tables-D4wJzM4y.js +820 -0
- package/dist/ensure-collection-tables-D4wJzM4y.js.map +1 -0
- package/dist/ensure-tables-CIojK-Rx.js +650 -0
- package/dist/ensure-tables-CIojK-Rx.js.map +1 -0
- package/dist/generate-drizzle-schema-CwEgVyjO.js +123 -0
- package/dist/generate-drizzle-schema-CwEgVyjO.js.map +1 -0
- package/dist/generate-drizzle-schema-logic-so2Z_lyC.js +528 -0
- package/dist/generate-drizzle-schema-logic-so2Z_lyC.js.map +1 -0
- package/dist/{ensure-collection-tables-DgVixhX3.js → generate-postgres-ddl-logic-CtnpIBVv.js} +126 -984
- package/dist/generate-postgres-ddl-logic-CtnpIBVv.js.map +1 -0
- package/dist/generated-schema-staleness-DQdfJl2A.js +157 -0
- package/dist/generated-schema-staleness-DQdfJl2A.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.es.js +7423 -1133
- package/dist/index.es.js.map +1 -1
- package/dist/introspect-db-logic-C6LQdTxj.js +1378 -0
- package/dist/introspect-db-logic-C6LQdTxj.js.map +1 -0
- package/dist/policy-drift-kNJ6X2Uj.js +313 -0
- package/dist/policy-drift-kNJ6X2Uj.js.map +1 -0
- package/dist/{backup-service-DCk7KhhL.js → rls-bootstrap-sql-DLDnUjtx.js} +737 -2689
- package/dist/rls-bootstrap-sql-DLDnUjtx.js.map +1 -0
- package/dist/{rls-enforcement-DvAbL9YJ.js → rls-enforcement-DeYEjYLt.js} +65 -190
- package/dist/rls-enforcement-DeYEjYLt.js.map +1 -0
- package/dist/schema/bin/generate-drizzle-schema.d.ts +1 -0
- package/dist/schema/bin/generate-postgres-ddl.d.ts +1 -0
- package/dist/schema/column-type-drift.d.ts +79 -0
- package/dist/schema/doctor-cli.js +129 -0
- package/dist/schema/doctor-cli.js.map +1 -0
- package/dist/schema/doctor.d.ts +42 -0
- package/dist/schema/ensure-collection-tables.d.ts +90 -0
- package/dist/schema/generate-drizzle-schema.d.ts +1 -1
- package/dist/schema/generate-drizzle-schema.js +24 -0
- package/dist/schema/generate-drizzle-schema.js.map +1 -0
- package/dist/schema/generate-next-step.d.ts +30 -0
- package/dist/schema/generate-postgres-ddl-logic.d.ts +28 -0
- package/dist/schema/generate-postgres-ddl.d.ts +1 -1
- package/dist/schema/generate-postgres-ddl.js +138 -0
- package/dist/schema/generate-postgres-ddl.js.map +1 -0
- package/dist/schema/generated-schema-staleness.d.ts +37 -0
- package/dist/schema/introspect-db-queries.d.ts +34 -0
- package/dist/schema/introspect-db.js +866 -0
- package/dist/schema/introspect-db.js.map +1 -0
- package/dist/security/rls-enforcement.d.ts +63 -2
- package/dist/services/BranchService.d.ts +86 -1
- package/dist/services/collection-helpers.d.ts +1 -1
- package/dist/services/realtimeService.d.ts +30 -2
- package/dist/services/row-pipeline.d.ts +12 -1
- package/dist/source-Br7L7GOI.js +410 -0
- package/dist/source-Br7L7GOI.js.map +1 -0
- package/dist/utils/pg-error-utils.d.ts +3 -1
- package/dist/utils/pg-numeric-number-patch.d.ts +31 -0
- package/dist/vector-index-DkWxm2de.js +309 -0
- package/dist/vector-index-DkWxm2de.js.map +1 -0
- package/package.json +32 -27
- package/dist/backup-service-DCk7KhhL.js.map +0 -1
- package/dist/collection-index-BRUg10H5.js +0 -5690
- package/dist/collection-index-BRUg10H5.js.map +0 -1
- package/dist/connection-GOKU3Hu5.js.map +0 -1
- package/dist/ensure-collection-policies-UCqgv_8c.js.map +0 -1
- package/dist/ensure-collection-tables-DgVixhX3.js.map +0 -1
- package/dist/rls-bootstrap-sql-B5C9LoJ6.js +0 -244
- package/dist/rls-bootstrap-sql-B5C9LoJ6.js.map +0 -1
- package/dist/rls-enforcement-DvAbL9YJ.js.map +0 -1
- package/dist/src-DiB5RP2Z.js +0 -541
- package/dist/src-DiB5RP2Z.js.map +0 -1
- package/dist/websocket-BZ4H5wUz.js +0 -8198
- package/dist/websocket-BZ4H5wUz.js.map +0 -1
- package/src/PostgresAdapter.ts +0 -91
- package/src/PostgresBackendDriver.ts +0 -1857
- package/src/PostgresBootstrapper.ts +0 -1157
- package/src/auth/ensure-tables.ts +0 -896
- package/src/auth/schema-version.ts +0 -260
- package/src/auth/services.ts +0 -1610
- package/src/backup/backup-cli.ts +0 -520
- package/src/backup/backup-cron.ts +0 -212
- package/src/backup/backup-logic.ts +0 -93
- package/src/backup/backup-service.ts +0 -485
- package/src/backup/index.ts +0 -12
- package/src/backup/pg-tools.ts +0 -403
- package/src/backup/retention.ts +0 -86
- package/src/cli-errors.ts +0 -265
- package/src/cli-helpers.ts +0 -569
- package/src/cli-output.ts +0 -43
- package/src/cli.ts +0 -1339
- package/src/collections/PostgresCollectionRegistry.ts +0 -108
- package/src/collections/buildRegistry.ts +0 -68
- package/src/collections/validate-relations.ts +0 -402
- package/src/connection.ts +0 -333
- package/src/data-transformer.ts +0 -845
- package/src/databasePoolManager.ts +0 -95
- package/src/history/HistoryService.ts +0 -254
- package/src/history/ensure-history-table.ts +0 -53
- package/src/index.ts +0 -19
- package/src/interfaces.ts +0 -60
- package/src/module-dir.ts +0 -7
- package/src/schema/atlas-argv.ts +0 -94
- package/src/schema/auth-schema.ts +0 -298
- package/src/schema/auth-users-columns.ts +0 -131
- package/src/schema/carved-out-migration.ts +0 -404
- package/src/schema/classify-change.ts +0 -436
- package/src/schema/collection-index.ts +0 -427
- package/src/schema/collections-schema-version.ts +0 -103
- package/src/schema/destructive-sql.ts +0 -94
- package/src/schema/doctor-cli.ts +0 -66
- package/src/schema/doctor-policy-checks.ts +0 -105
- package/src/schema/doctor.ts +0 -837
- package/src/schema/drizzle-ddl.ts +0 -33
- package/src/schema/dynamic-tables.test.ts +0 -302
- package/src/schema/dynamic-tables.ts +0 -293
- package/src/schema/ensure-collection-policies.ts +0 -198
- package/src/schema/ensure-collection-tables.test.ts +0 -518
- package/src/schema/ensure-collection-tables.ts +0 -1267
- package/src/schema/generate-drizzle-schema-logic.ts +0 -957
- package/src/schema/generate-drizzle-schema.ts +0 -143
- package/src/schema/generate-postgres-ddl-logic.ts +0 -1339
- package/src/schema/generate-postgres-ddl.ts +0 -180
- package/src/schema/generate-schema-commit.ts +0 -267
- package/src/schema/generated-schema-staleness.ts +0 -284
- package/src/schema/introspect-db-constraints.ts +0 -385
- package/src/schema/introspect-db-inference.ts +0 -255
- package/src/schema/introspect-db-logic.ts +0 -1387
- package/src/schema/introspect-db-naming.ts +0 -15
- package/src/schema/introspect-db-project.ts +0 -78
- package/src/schema/introspect-db-queries.ts +0 -326
- package/src/schema/introspect-db-structure.ts +0 -670
- package/src/schema/introspect-db-types.ts +0 -56
- package/src/schema/introspect-db.ts +0 -257
- package/src/schema/introspect-runtime.test.ts +0 -260
- package/src/schema/introspect-runtime.ts +0 -327
- package/src/schema/non-sql-collections.test.ts +0 -131
- package/src/schema/rls-bootstrap-sql.ts +0 -288
- package/src/schema/search-column.ts +0 -643
- package/src/schema/test-schema.ts +0 -11
- package/src/schema/vector-index.ts +0 -491
- package/src/security/anonymous-grants.test.ts +0 -73
- package/src/security/policy-drift.test.ts +0 -435
- package/src/security/policy-drift.ts +0 -473
- package/src/security/rls-enforcement.ts +0 -505
- package/src/services/BranchService.ts +0 -326
- package/src/services/FetchService.ts +0 -2115
- package/src/services/PersistService.ts +0 -557
- package/src/services/RelationService.ts +0 -978
- package/src/services/RelationWriteService.ts +0 -653
- package/src/services/cdc/CdcListener.ts +0 -103
- package/src/services/cdc/junction-tables.ts +0 -91
- package/src/services/cdc/trigger-cdc.ts +0 -173
- package/src/services/channel-bus/ChannelBus.ts +0 -44
- package/src/services/channel-bus/PostgresChannelBus.ts +0 -299
- package/src/services/channel-bus/index.ts +0 -114
- package/src/services/channel-history.ts +0 -424
- package/src/services/channel-presence.ts +0 -185
- package/src/services/collection-helpers.ts +0 -420
- package/src/services/dataService.ts +0 -256
- package/src/services/index.ts +0 -14
- package/src/services/junction-writes.ts +0 -295
- package/src/services/nested-path.ts +0 -145
- package/src/services/pg-notify-listener.ts +0 -151
- package/src/services/realtimeService.ts +0 -2545
- package/src/services/row-pipeline.ts +0 -240
- package/src/services/write-denial.ts +0 -55
- package/src/types.ts +0 -4
- package/src/utils/connection-string.ts +0 -58
- package/src/utils/drizzle-conditions.ts +0 -2743
- package/src/utils/pg-array-null-patch.ts +0 -42
- package/src/utils/pg-error-utils.ts +0 -391
- package/src/websocket.ts +0 -855
package/dist/{ensure-collection-tables-DgVixhX3.js → generate-postgres-ddl-logic-CtnpIBVv.js}
RENAMED
|
@@ -1,32 +1,13 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
|
-
import "process";
|
|
3
2
|
__createRequire(import.meta.url);
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
3
|
+
import { r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
|
|
4
|
+
import { i as isAuthCollection, n as authUsersColumnDefinition, r as authUsersColumnSql, t as AUTH_USERS_COLUMNS } from "./auth-users-columns-D2LBFrMH.js";
|
|
5
|
+
import { a as vectorDimensionGuard, c as vectorExtensionStatement, d as vectorObjectNames, i as vectorColumnDefinition, n as buildVectorColumnSpecs, o as vectorExtensionDeclared, r as buildVectorIndexPlan, t as VECTOR_EXTENSION_OPT_IN, u as vectorIndexStatements } from "./vector-index-DkWxm2de.js";
|
|
6
|
+
import { i as collectionIndexStatements, n as buildCollectionIndexSpecs } from "./collection-index-BHgT_TKd.js";
|
|
7
|
+
import { DEFAULT_FUZZY_THRESHOLD, DEFAULT_SEARCH_COLUMN, DEFAULT_SEARCH_LANGUAGE, DEFAULT_SEARCH_WEIGHT, REBASE_SCHEMA, isManyToMany, isPostgresCollectionConfig } from "@rebasepro/types";
|
|
8
|
+
import { findRelation, getEffectiveSecurityRules, getInjectedSecurityRules, getJunctionCollectionConfig, getJunctionSecurityRules, getTableName, policyToPostgres, relationalCollections, resolveCollectionRelations, resolveJunctionSpecs, resolveStringColumnLength, securityRuleToConditions } from "@rebasepro/common";
|
|
9
|
+
import { generateForeignKeyName, getPolicyNamesForRule, legacyForeignKeyName, toPostgresIdentifier, toSnakeCase } from "@rebasepro/utils";
|
|
8
10
|
import { createHash } from "node:crypto";
|
|
9
|
-
/**
|
|
10
|
-
* How wide a `varchar`/`char` column should be for a given property.
|
|
11
|
-
*
|
|
12
|
-
* One definition, three call sites, because they used to disagree. For the same
|
|
13
|
-
* `columnType: "varchar"` property the DDL generator emitted `VARCHAR(255)`
|
|
14
|
-
* while the Drizzle generator emitted a bare `varchar("col")` — which Postgres
|
|
15
|
-
* reads as *unbounded* — so which of the two you ran decided whether the column
|
|
16
|
-
* had a limit at all. Introspection then dropped the length entirely, so reading
|
|
17
|
-
* an existing `character varying(500)` column back and regenerating it produced
|
|
18
|
-
* a `VARCHAR(255)`: a silent narrowing of a column with data already in it.
|
|
19
|
-
*
|
|
20
|
-
* `validation.max` is the property's own statement about how long the value may
|
|
21
|
-
* be, so it is the only sensible source for the column's width — and it keeps
|
|
22
|
-
* the constraint the database enforces in step with the one the app enforces,
|
|
23
|
-
* rather than inventing a second, different limit underneath it.
|
|
24
|
-
*/
|
|
25
|
-
function resolveStringColumnLength(prop) {
|
|
26
|
-
const max = prop.validation?.max;
|
|
27
|
-
return typeof max === "number" && Number.isInteger(max) && max > 0 ? max : 255;
|
|
28
|
-
}
|
|
29
|
-
//#endregion
|
|
30
11
|
//#region src/schema/search-column.ts
|
|
31
12
|
/**
|
|
32
13
|
* The one place a collection's `search` block becomes SQL.
|
|
@@ -171,9 +152,9 @@ var rawTextSql = (field) => {
|
|
|
171
152
|
const col = `"${field.column}"`;
|
|
172
153
|
if (field.kind === "text") return `coalesce(${col}, '')`;
|
|
173
154
|
if (field.kind === "text_array") return `${SEARCH_TEXT_FN}(coalesce(${col}, '{}'::text[]))`;
|
|
174
|
-
return `${SEARCH_TEXT_FN}(coalesce(${field.jsonPath.length === 0 ? col : field.jsonPath.length === 1 ? `${col} -> ${quote
|
|
155
|
+
return `${SEARCH_TEXT_FN}(coalesce(${field.jsonPath.length === 0 ? col : field.jsonPath.length === 1 ? `${col} -> ${quote(field.jsonPath[0])}` : `${col} #> ${quote(`{${field.jsonPath.join(",")}}`)}`}, '{}'::jsonb))`;
|
|
175
156
|
};
|
|
176
|
-
var quote
|
|
157
|
+
var quote = (v) => `'${v.replace(/'/g, "''")}'`;
|
|
177
158
|
/**
|
|
178
159
|
* Resolve and validate one declared field path.
|
|
179
160
|
*
|
|
@@ -184,7 +165,7 @@ var quote$1 = (v) => `'${v.replace(/'/g, "''")}'`;
|
|
|
184
165
|
*/
|
|
185
166
|
var resolveField = (entry, collection, cfg) => {
|
|
186
167
|
const path = typeof entry === "string" ? entry : entry.path;
|
|
187
|
-
const weight = (typeof entry === "string" ? void 0 : entry.weight) ??
|
|
168
|
+
const weight = (typeof entry === "string" ? void 0 : entry.weight) ?? DEFAULT_SEARCH_WEIGHT;
|
|
188
169
|
const where = `${collection.slug}.search`;
|
|
189
170
|
if (!path || typeof path !== "string") throw new SearchConfigError(`${where}: every entry in \`fields\` needs a property path.`);
|
|
190
171
|
const [head, ...rest] = path.split(".");
|
|
@@ -203,14 +184,14 @@ var resolveField = (entry, collection, cfg) => {
|
|
|
203
184
|
jsonPath: rest,
|
|
204
185
|
kind: classified.kind
|
|
205
186
|
}), cfg.unaccent === true);
|
|
206
|
-
const language = cfg.language ??
|
|
187
|
+
const language = cfg.language ?? DEFAULT_SEARCH_LANGUAGE;
|
|
207
188
|
return {
|
|
208
189
|
path,
|
|
209
190
|
column,
|
|
210
191
|
jsonPath: rest,
|
|
211
192
|
kind: classified.kind,
|
|
212
193
|
weight,
|
|
213
|
-
sql: `setweight(to_tsvector(${quote
|
|
194
|
+
sql: `setweight(to_tsvector(${quote(language)}, ${textSql}), ${quote(weight)})`,
|
|
214
195
|
textSql
|
|
215
196
|
};
|
|
216
197
|
};
|
|
@@ -227,7 +208,7 @@ var buildSearchColumnSpec = (collection) => {
|
|
|
227
208
|
if (!Array.isArray(cfg.fields) || cfg.fields.length === 0) throw new SearchConfigError(`${collection.slug}.search: \`fields\` is empty. Name the properties to index, or remove the \`search\` block to keep the default ILIKE behaviour.`);
|
|
228
209
|
const table = getTableName(collection);
|
|
229
210
|
const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
230
|
-
const column = cfg.column ??
|
|
211
|
+
const column = cfg.column ?? DEFAULT_SEARCH_COLUMN;
|
|
231
212
|
if (collection.properties?.[column]) throw new SearchConfigError(`${collection.slug}.search: the generated column "${column}" collides with a declared property of the same name. Set \`search.column\` to something else.`);
|
|
232
213
|
const fields = cfg.fields.map((entry) => resolveField(entry, collection, cfg));
|
|
233
214
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -242,7 +223,7 @@ var buildSearchColumnSpec = (collection) => {
|
|
|
242
223
|
schema,
|
|
243
224
|
table,
|
|
244
225
|
column,
|
|
245
|
-
language: cfg.language ??
|
|
226
|
+
language: cfg.language ?? DEFAULT_SEARCH_LANGUAGE,
|
|
246
227
|
unaccent: cfg.unaccent === true,
|
|
247
228
|
fields,
|
|
248
229
|
expression: fields.map((f) => f.sql).join(" || "),
|
|
@@ -256,7 +237,7 @@ var buildSearchColumnSpec = (collection) => {
|
|
|
256
237
|
column: fuzzyColumn,
|
|
257
238
|
expression: fields.map((f) => f.textSql).join(" || ' ' || "),
|
|
258
239
|
indexName: toPostgresIdentifier(`${table}_${fuzzyColumn}_trgm`),
|
|
259
|
-
threshold: cfg.fuzzyThreshold ??
|
|
240
|
+
threshold: cfg.fuzzyThreshold ?? DEFAULT_FUZZY_THRESHOLD
|
|
260
241
|
};
|
|
261
242
|
}
|
|
262
243
|
return spec;
|
|
@@ -343,7 +324,7 @@ var searchColumnStamps = (spec) => {
|
|
|
343
324
|
column,
|
|
344
325
|
expression,
|
|
345
326
|
fingerprint,
|
|
346
|
-
sql: `COMMENT ON COLUMN "${spec.schema}"."${spec.table}"."${column}" IS ${quote
|
|
327
|
+
sql: `COMMENT ON COLUMN "${spec.schema}"."${spec.table}"."${column}" IS ${quote(fingerprint)};`
|
|
347
328
|
};
|
|
348
329
|
};
|
|
349
330
|
const stamps = [stamp(spec.column, spec.expression)];
|
|
@@ -361,20 +342,27 @@ var searchColumnStamps = (spec) => {
|
|
|
361
342
|
* and the operator reading the failure is the person who changed the block.
|
|
362
343
|
*/
|
|
363
344
|
var searchStampGuards = (spec) => searchColumnStamps(spec).map((stamp) => {
|
|
364
|
-
const relation = quote
|
|
345
|
+
const relation = quote(`"${spec.schema}"."${spec.table}"`);
|
|
365
346
|
return `DO $rebase_search$
|
|
366
347
|
DECLARE recorded text;
|
|
367
348
|
BEGIN
|
|
368
349
|
SELECT col_description(a.attrelid, a.attnum) INTO recorded
|
|
369
350
|
FROM pg_attribute a
|
|
370
|
-
WHERE a.attrelid = ${relation}::regclass AND a.attname = ${quote
|
|
371
|
-
IF recorded LIKE ${quote
|
|
351
|
+
WHERE a.attrelid = ${relation}::regclass AND a.attname = ${quote(stamp.column)} AND NOT a.attisdropped;
|
|
352
|
+
IF recorded LIKE ${quote(`${SEARCH_STAMP_PREFIX}%`)} AND recorded <> ${quote(stamp.fingerprint)} THEN
|
|
372
353
|
RAISE EXCEPTION 'Rebase: the search block for ${spec.schema}.${spec.table} changed after the generated column "${stamp.column}" was built (recorded %, expected ${stamp.fingerprint}). Postgres cannot alter a generated expression in place. Drop the column and re-apply this file — it rewrites the table and rebuilds the index: ALTER TABLE ${relation.slice(1, -1)} DROP COLUMN "${stamp.column}";', recorded;
|
|
373
354
|
END IF;
|
|
374
355
|
END
|
|
375
356
|
$rebase_search$;`;
|
|
376
357
|
});
|
|
377
358
|
/**
|
|
359
|
+
* The index names the spec creates.
|
|
360
|
+
*
|
|
361
|
+
* Needed by name, not just by statement, so Atlas can be told to exclude them
|
|
362
|
+
* from its diff — see `searchExcludePatterns`.
|
|
363
|
+
*/
|
|
364
|
+
var searchIndexNames = (spec) => spec.fuzzy ? [spec.indexName, spec.fuzzy.indexName] : [spec.indexName];
|
|
365
|
+
/**
|
|
378
366
|
* The generated column names a collection's search block adds, if any.
|
|
379
367
|
*
|
|
380
368
|
* These are physical columns on the table, so `SELECT *` returns them. They are
|
|
@@ -440,381 +428,27 @@ var excludedColumnNames = (tableColumns, collection) => {
|
|
|
440
428
|
return Object.keys(tableColumns).filter((name) => byName.has(name) || isSearchIndexColumn(tableColumns[name]));
|
|
441
429
|
};
|
|
442
430
|
//#endregion
|
|
443
|
-
//#region src/schema/auth-users-columns.ts
|
|
444
|
-
/**
|
|
445
|
-
* `email` is NOT NULL on purpose, and the anonymous sign-in route depends on it
|
|
446
|
-
* — it synthesizes `anon_<32 hex>@anonymous.local` rather than inserting NULL.
|
|
447
|
-
* The 320-char bound (RFC 5321) is a CHECK rather than a `VARCHAR(n)`, added
|
|
448
|
-
* separately by `ensureAuthTablesExist` so it can be `NOT VALID` on an adopted
|
|
449
|
-
* table that already holds a longer row.
|
|
450
|
-
*/
|
|
451
|
-
var AUTH_USERS_COLUMNS = [
|
|
452
|
-
{
|
|
453
|
-
column: "email",
|
|
454
|
-
type: "TEXT",
|
|
455
|
-
notNull: true
|
|
456
|
-
},
|
|
457
|
-
{
|
|
458
|
-
column: "display_name",
|
|
459
|
-
type: "TEXT"
|
|
460
|
-
},
|
|
461
|
-
{
|
|
462
|
-
column: "photo_url",
|
|
463
|
-
type: "TEXT"
|
|
464
|
-
},
|
|
465
|
-
{
|
|
466
|
-
column: "roles",
|
|
467
|
-
type: "TEXT[]",
|
|
468
|
-
default: "'{}'",
|
|
469
|
-
notNull: true
|
|
470
|
-
},
|
|
471
|
-
{
|
|
472
|
-
column: "password_hash",
|
|
473
|
-
type: "TEXT"
|
|
474
|
-
},
|
|
475
|
-
{
|
|
476
|
-
column: "email_verified",
|
|
477
|
-
type: "BOOLEAN",
|
|
478
|
-
default: "FALSE",
|
|
479
|
-
notNull: true
|
|
480
|
-
},
|
|
481
|
-
{
|
|
482
|
-
column: "email_verification_token",
|
|
483
|
-
type: "TEXT"
|
|
484
|
-
},
|
|
485
|
-
{
|
|
486
|
-
column: "email_verification_sent_at",
|
|
487
|
-
type: "TIMESTAMP WITH TIME ZONE"
|
|
488
|
-
},
|
|
489
|
-
{
|
|
490
|
-
column: "is_anonymous",
|
|
491
|
-
type: "BOOLEAN",
|
|
492
|
-
default: "FALSE",
|
|
493
|
-
notNull: true
|
|
494
|
-
},
|
|
495
|
-
{
|
|
496
|
-
column: "metadata",
|
|
497
|
-
type: "JSONB",
|
|
498
|
-
default: "'{}'",
|
|
499
|
-
notNull: true
|
|
500
|
-
},
|
|
501
|
-
{
|
|
502
|
-
column: "tokens_valid_after",
|
|
503
|
-
type: "TIMESTAMP WITH TIME ZONE"
|
|
504
|
-
},
|
|
505
|
-
{
|
|
506
|
-
column: "created_at",
|
|
507
|
-
type: "TIMESTAMP WITH TIME ZONE",
|
|
508
|
-
default: "NOW()",
|
|
509
|
-
notNull: true
|
|
510
|
-
},
|
|
511
|
-
{
|
|
512
|
-
column: "updated_at",
|
|
513
|
-
type: "TIMESTAMP WITH TIME ZONE",
|
|
514
|
-
default: "NOW()",
|
|
515
|
-
notNull: true
|
|
516
|
-
}
|
|
517
|
-
];
|
|
518
|
-
var BY_COLUMN = new Map(AUTH_USERS_COLUMNS.map((c) => [c.column, c]));
|
|
519
|
-
/** Type + inline constraints, as they appear after the column name. */
|
|
520
|
-
function authUsersColumnSql(spec) {
|
|
521
|
-
return [
|
|
522
|
-
spec.type,
|
|
523
|
-
spec.default !== void 0 ? `DEFAULT ${spec.default}` : "",
|
|
524
|
-
spec.notNull ? "NOT NULL" : ""
|
|
525
|
-
].filter(Boolean).join(" ");
|
|
526
|
-
}
|
|
527
|
-
/**
|
|
528
|
-
* The auth-owned definition for a physical column name, or `undefined` when
|
|
529
|
-
* auth does not own it.
|
|
530
|
-
*
|
|
531
|
-
* Callers pass the RESOLVED column name (after `columnName` mapping), because
|
|
532
|
-
* that is the only name the three creators agree on: the scaffold's users
|
|
533
|
-
* collection spells the property `displayName` and the column `display_name`.
|
|
534
|
-
*/
|
|
535
|
-
function authUsersColumnDefinition(column) {
|
|
536
|
-
const spec = BY_COLUMN.get(column);
|
|
537
|
-
return spec ? authUsersColumnSql(spec) : void 0;
|
|
538
|
-
}
|
|
539
|
-
/**
|
|
540
|
-
* Whether a collection is an auth collection, i.e. whether the definitions in
|
|
541
|
-
* this module apply to its table at all.
|
|
542
|
-
*
|
|
543
|
-
* Duplicated in shape from `@rebasepro/common`'s policy defaults on purpose:
|
|
544
|
-
* that one takes a `CollectionConfig`, this one is called from DDL code paths
|
|
545
|
-
* that hold looser objects, and both spellings must accept `auth: true` as well
|
|
546
|
-
* as `auth: { enabled: true }`.
|
|
547
|
-
*/
|
|
548
|
-
function isAuthCollection(collection) {
|
|
549
|
-
const auth = collection?.auth;
|
|
550
|
-
if (auth === true) return true;
|
|
551
|
-
return typeof auth === "object" && auth !== null && auth.enabled === true;
|
|
552
|
-
}
|
|
553
|
-
//#endregion
|
|
554
|
-
//#region src/schema/vector-index.ts
|
|
555
|
-
/**
|
|
556
|
-
* The widest `vector` pgvector will build an HNSW or IVFFlat index over.
|
|
557
|
-
* Storage and exact search are unaffected by this limit.
|
|
558
|
-
*/
|
|
559
|
-
var MAX_INDEXABLE_VECTOR_DIMENSIONS = 2e3;
|
|
560
|
-
/**
|
|
561
|
-
* Operator class per distance. These strings are part of the database contract:
|
|
562
|
-
* they appear in `CREATE INDEX`, so renaming one renames an index.
|
|
563
|
-
*/
|
|
564
|
-
var OPERATOR_CLASS = {
|
|
565
|
-
cosine: "vector_cosine_ops",
|
|
566
|
-
l2: "vector_l2_ops",
|
|
567
|
-
inner_product: "vector_ip_ops"
|
|
568
|
-
};
|
|
569
|
-
/** Short, stable tag per distance, used to name the index. */
|
|
570
|
-
var DISTANCE_TAG = {
|
|
571
|
-
cosine: "cosine",
|
|
572
|
-
l2: "l2",
|
|
573
|
-
inner_product: "ip"
|
|
574
|
-
};
|
|
575
|
-
var VectorIndexConfigError = class extends Error {
|
|
576
|
-
constructor(message) {
|
|
577
|
-
super(message);
|
|
578
|
-
this.name = "VectorIndexConfigError";
|
|
579
|
-
}
|
|
580
|
-
};
|
|
581
|
-
var isVectorProperty = (prop) => !!prop && typeof prop === "object" && prop.type === "vector";
|
|
582
|
-
var asDistances = (config, label) => {
|
|
583
|
-
const raw = config.distance ?? "cosine";
|
|
584
|
-
const list = Array.isArray(raw) ? raw : [raw];
|
|
585
|
-
if (list.length === 0) throw new VectorIndexConfigError(`${label}: \`index.distance\` is an empty array. Name at least one distance, or set \`index: false\` to create no index.`);
|
|
586
|
-
const seen = /* @__PURE__ */ new Set();
|
|
587
|
-
for (const distance of list) {
|
|
588
|
-
if (!(distance in OPERATOR_CLASS)) throw new VectorIndexConfigError(`${label}: \`index.distance\` is "${distance}", which is not a pgvector distance. Use ${Object.keys(OPERATOR_CLASS).map((d) => `"${d}"`).join(", ")}.`);
|
|
589
|
-
if (seen.has(distance)) throw new VectorIndexConfigError(`${label}: \`index.distance\` lists "${distance}" twice.`);
|
|
590
|
-
seen.add(distance);
|
|
591
|
-
}
|
|
592
|
-
return list;
|
|
593
|
-
};
|
|
594
|
-
var assertPositiveInteger = (value, key, label) => {
|
|
595
|
-
if (value === void 0) return;
|
|
596
|
-
if (!Number.isInteger(value) || value <= 0) throw new VectorIndexConfigError(`${label}: \`index.${key}\` is ${JSON.stringify(value)}. It must be a positive integer.`);
|
|
597
|
-
};
|
|
598
|
-
/**
|
|
599
|
-
* Index parameters for one method. Parameters belonging to the *other* method
|
|
600
|
-
* are rejected rather than ignored, because a silently dropped `lists` on an
|
|
601
|
-
* HNSW index reads, from the config, exactly like a tuned index.
|
|
602
|
-
*/
|
|
603
|
-
var parametersFor = (method, config, label) => {
|
|
604
|
-
assertPositiveInteger(config.m, "m", label);
|
|
605
|
-
assertPositiveInteger(config.efConstruction, "efConstruction", label);
|
|
606
|
-
assertPositiveInteger(config.lists, "lists", label);
|
|
607
|
-
if (method === "hnsw") {
|
|
608
|
-
if (config.lists !== void 0) throw new VectorIndexConfigError(`${label}: \`index.lists\` only applies to \`method: "ivfflat"\`. Remove it, or switch the method.`);
|
|
609
|
-
const params = [];
|
|
610
|
-
if (config.m !== void 0) params.push(["m", config.m]);
|
|
611
|
-
if (config.efConstruction !== void 0) params.push(["ef_construction", config.efConstruction]);
|
|
612
|
-
return params;
|
|
613
|
-
}
|
|
614
|
-
for (const key of ["m", "efConstruction"]) if (config[key] !== void 0) throw new VectorIndexConfigError(`${label}: \`index.${key}\` only applies to \`method: "hnsw"\`. Remove it, or switch the method.`);
|
|
615
|
-
return config.lists !== void 0 ? [["lists", config.lists]] : [];
|
|
616
|
-
};
|
|
617
|
-
/**
|
|
618
|
-
* Every ANN index a collection's vector properties call for.
|
|
619
|
-
*
|
|
620
|
-
* `resolveColumn` is passed in rather than imported so that this module stays
|
|
621
|
-
* free of the DDL generator, which imports *it*. Both callers hand it the same
|
|
622
|
-
* `resolveColumnName`, and a contract test asserts the names agree.
|
|
623
|
-
*/
|
|
624
|
-
var buildVectorIndexPlan = (collection, resolveColumn) => {
|
|
625
|
-
const specs = [];
|
|
626
|
-
const skipped = [];
|
|
627
|
-
const properties = collection.properties ?? {};
|
|
628
|
-
const table = getTableName(collection);
|
|
629
|
-
const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
630
|
-
for (const [propName, prop] of Object.entries(properties)) {
|
|
631
|
-
if (!isVectorProperty(prop)) continue;
|
|
632
|
-
if (prop.index === false) continue;
|
|
633
|
-
const label = `${collection.slug}.${propName}`;
|
|
634
|
-
const column = resolveColumn(propName, prop);
|
|
635
|
-
const config = prop.index ?? {};
|
|
636
|
-
const method = config.method ?? "hnsw";
|
|
637
|
-
if (method !== "hnsw" && method !== "ivfflat") throw new VectorIndexConfigError(`${label}: \`index.method\` is "${method}". Use "hnsw" or "ivfflat".`);
|
|
638
|
-
const distances = asDistances(config, label);
|
|
639
|
-
const parameters = parametersFor(method, config, label);
|
|
640
|
-
if (!Number.isInteger(prop.dimensions) || prop.dimensions <= 0) throw new VectorIndexConfigError(`${label}: \`dimensions\` is ${JSON.stringify(prop.dimensions)}. It must be a positive integer.`);
|
|
641
|
-
if (prop.dimensions > 2e3) {
|
|
642
|
-
skipped.push({
|
|
643
|
-
schema,
|
|
644
|
-
table,
|
|
645
|
-
column,
|
|
646
|
-
dimensions: prop.dimensions,
|
|
647
|
-
reason: `pgvector cannot index a vector wider than ${MAX_INDEXABLE_VECTOR_DIMENSIONS} dimensions, and ${label} declares ${prop.dimensions}. The column works and \`vectorSearch\` still answers, as an exact scan. To index it, reduce the dimensions (many embedding models support a shorter output) or set \`index: false\` to state that the scan is intended.`
|
|
648
|
-
});
|
|
649
|
-
continue;
|
|
650
|
-
}
|
|
651
|
-
for (const distance of distances) specs.push({
|
|
652
|
-
schema,
|
|
653
|
-
table,
|
|
654
|
-
column,
|
|
655
|
-
indexName: toPostgresIdentifier(`${table}_${column}_${method}_${DISTANCE_TAG[distance]}`),
|
|
656
|
-
method,
|
|
657
|
-
distance,
|
|
658
|
-
operatorClass: OPERATOR_CLASS[distance],
|
|
659
|
-
parameters
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
return {
|
|
663
|
-
specs,
|
|
664
|
-
skipped
|
|
665
|
-
};
|
|
666
|
-
};
|
|
667
|
-
/**
|
|
668
|
-
* The `CREATE INDEX` for one spec.
|
|
669
|
-
*
|
|
670
|
-
* `CONCURRENTLY` is deliberately absent, for the same reason it is absent from
|
|
671
|
-
* `searchIndexStatements`: this form is replayed as part of a migration, where
|
|
672
|
-
* a concurrent build is not allowed. The boot-time ensure rewrites it — see
|
|
673
|
-
* `ensureCollectionTables`.
|
|
674
|
-
*/
|
|
675
|
-
var vectorIndexStatement = (spec) => {
|
|
676
|
-
const params = spec.parameters.length ? ` WITH (${spec.parameters.map(([key, value]) => `${key} = ${value}`).join(", ")})` : "";
|
|
677
|
-
return `CREATE INDEX IF NOT EXISTS "${spec.indexName}" ON "${spec.schema}"."${spec.table}" USING ${spec.method} ("${spec.column}" ${spec.operatorClass})${params};`;
|
|
678
|
-
};
|
|
679
|
-
/** Every statement for a plan, in a stable order. */
|
|
680
|
-
var vectorIndexStatements = (plan) => plan.specs.map(vectorIndexStatement);
|
|
681
|
-
var quote = (v) => `'${v.replace(/'/g, "''")}'`;
|
|
682
|
-
/**
|
|
683
|
-
* The schema pgvector's types are installed into.
|
|
684
|
-
*
|
|
685
|
-
* `WITH SCHEMA public` for the same load-bearing reason `searchExtensionStatements`
|
|
686
|
-
* gives: an unqualified `CREATE EXTENSION` lands in the first schema on
|
|
687
|
-
* `search_path`, which is `"$user", public` — and the scaffold's role is named
|
|
688
|
-
* `rebase`, the same as a schema the generator creates. Left to itself the
|
|
689
|
-
* extension would install into `rebase`, and every unqualified `VECTOR(n)`
|
|
690
|
-
* below would fail to resolve.
|
|
691
|
-
*/
|
|
692
|
-
var VECTOR_EXTENSION_SCHEMA = "public";
|
|
693
|
-
/** The extension name a database has to name to let Rebase install pgvector. */
|
|
694
|
-
var VECTOR_EXTENSION = "vector";
|
|
695
|
-
/**
|
|
696
|
-
* How a project says Rebase may install pgvector, quoted into the messages that
|
|
697
|
-
* have to name it. Spelled once so the option and the advice cannot drift.
|
|
698
|
-
*/
|
|
699
|
-
var VECTOR_EXTENSION_OPT_IN = `database({ extensions: ["${VECTOR_EXTENSION}"] })`;
|
|
700
|
-
/** Did the project give Rebase leave to install pgvector? */
|
|
701
|
-
var vectorExtensionDeclared = (extensions) => (extensions ?? []).includes(VECTOR_EXTENSION);
|
|
702
|
-
/**
|
|
703
|
-
* Install pgvector — emitted only when the database asked for it.
|
|
704
|
-
*
|
|
705
|
-
* Opt-in because installing an extension is a decision with a deployment behind
|
|
706
|
-
* it: the image has to ship the library, the role has to be allowed to install
|
|
707
|
-
* it, and a managed provider has to have it on an allow-list. None of that is
|
|
708
|
-
* visible from inside the connection, so Rebase does not decide it. See
|
|
709
|
-
* `DatabaseOptions.extensions`.
|
|
710
|
-
*
|
|
711
|
-
* Withholding the statement is not withholding the *column*: the column is
|
|
712
|
-
* still created, and Postgres refuses it with `type "vector" does not exist`
|
|
713
|
-
* on a database where pgvector was never installed by hand. That error is the
|
|
714
|
-
* one this design accepts, and `vectorExtensionHint` is what makes it name
|
|
715
|
-
* {@link VECTOR_EXTENSION_OPT_IN} rather than nothing.
|
|
716
|
-
*/
|
|
717
|
-
var vectorExtensionStatement = () => `CREATE EXTENSION IF NOT EXISTS ${VECTOR_EXTENSION} WITH SCHEMA ${VECTOR_EXTENSION_SCHEMA};`;
|
|
718
|
-
/**
|
|
719
|
-
* The missing-pgvector explanation, appended to whichever error revealed it.
|
|
720
|
-
*
|
|
721
|
-
* Two readers, needing opposite things, which is why the branch is on the error
|
|
722
|
-
* text rather than on the configuration:
|
|
723
|
-
*
|
|
724
|
-
* - **`type "vector" does not exist`** — nobody opted in and the database has
|
|
725
|
-
* no pgvector. The fix is one line of config they cannot guess, so naming
|
|
726
|
-
* the option *is* the hint.
|
|
727
|
-
* - **the install itself failed** — the config is already right, and repeating
|
|
728
|
-
* the option would send them to edit a correct line. What is missing is the
|
|
729
|
-
* library on the server, or the grant.
|
|
730
|
-
*
|
|
731
|
-
* Lives here rather than beside either caller because both need it: the boot
|
|
732
|
-
* ensure raises the first through its action applier, and `rebase db push`
|
|
733
|
-
* raises it out of `applyVectorDdl`. A hint on only one path is how a bare
|
|
734
|
-
* `type "vector" does not exist` reaches somebody — which is the thing this
|
|
735
|
-
* exists to prevent.
|
|
736
|
-
*/
|
|
737
|
-
var vectorExtensionHint = (message) => {
|
|
738
|
-
if (/extension "vector" is not available/i.test(message) || /(permission denied to create extension|must be (superuser|owner).{0,40}extension)/i.test(message)) return "\n pgvector was declared and could not be installed. It is a server extension, so it needs an image that ships the library (the scaffold's `pgvector/pgvector:pg18` does; a stock `postgres:18` does not) and a role allowed to run `CREATE EXTENSION vector;`. Managed Postgres usually allows it once the extension is on the provider's allow-list.";
|
|
739
|
-
if (!/type "(vector|halfvec|sparsevec)" does not exist/i.test(message)) return "";
|
|
740
|
-
return `
|
|
741
|
-
pgvector is not installed on this database, and Rebase installs it only where a database says it may: add \`${VECTOR_EXTENSION_OPT_IN}\` in config/resources.ts, or install it once by hand with \`CREATE EXTENSION vector;\`. Either way the server needs an image that ships the library — the scaffold's \`pgvector/pgvector:pg18\` does, a stock \`postgres:18\` does not. Rebase then creates the column and its ANN index automatically — see the \`index\` option on the property.`;
|
|
742
|
-
};
|
|
743
|
-
/**
|
|
744
|
-
* Every vector column a collection declares.
|
|
745
|
-
*
|
|
746
|
-
* Wider than {@link buildVectorIndexPlan} on purpose: that one answers "what
|
|
747
|
-
* gets an ANN index", and skips both a property with `index: false` and one too
|
|
748
|
-
* wide to index. Either still needs its column.
|
|
749
|
-
*/
|
|
750
|
-
var buildVectorColumnSpecs = (collection, resolveColumn) => {
|
|
751
|
-
const table = getTableName(collection);
|
|
752
|
-
const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
753
|
-
const specs = [];
|
|
754
|
-
for (const [propName, prop] of Object.entries(collection.properties ?? {})) {
|
|
755
|
-
if (!isVectorProperty(prop)) continue;
|
|
756
|
-
const validation = prop.validation;
|
|
757
|
-
let modifiers = "";
|
|
758
|
-
if (validation?.unique) modifiers += " UNIQUE";
|
|
759
|
-
if (validation?.required) modifiers += " NOT NULL";
|
|
760
|
-
specs.push({
|
|
761
|
-
schema,
|
|
762
|
-
table,
|
|
763
|
-
column: resolveColumn(propName, prop),
|
|
764
|
-
dimensions: prop.dimensions,
|
|
765
|
-
modifiers
|
|
766
|
-
});
|
|
767
|
-
}
|
|
768
|
-
return specs;
|
|
769
|
-
};
|
|
770
|
-
/** The column type — the one place `VECTOR(n)` is spelled. */
|
|
771
|
-
var vectorColumnType = (spec) => `VECTOR(${spec.dimensions})`;
|
|
772
|
-
/** The column definition as it appears inside `CREATE TABLE`. */
|
|
773
|
-
var vectorColumnDefinition = (spec) => `"${spec.column}" ${vectorColumnType(spec)}${spec.modifiers}`;
|
|
774
|
-
/**
|
|
775
|
-
* Refuse — or perform — a `dimensions` change that `ADD COLUMN IF NOT EXISTS`
|
|
776
|
-
* would otherwise swallow.
|
|
777
|
-
*
|
|
778
|
-
* Without this the file *launders* the change: the ADD is a no-op against a
|
|
779
|
-
* column that exists, so a project that went from 384 to 768 dimensions would
|
|
780
|
-
* push clean, keep a 384-wide column, and fail on the next insert with a
|
|
781
|
-
* message about the row rather than about the config. Atlas used to catch this
|
|
782
|
-
* — it owned the column and planned the `ALTER … TYPE` — and taking the column
|
|
783
|
-
* out of its sight is exactly what makes the guard necessary.
|
|
784
|
-
*
|
|
785
|
-
* The widening is performed when the column holds no values, because that is
|
|
786
|
-
* the case Atlas handled and it is the common one: a developer changing
|
|
787
|
-
* embedding models before there are any embeddings. With values present the
|
|
788
|
-
* conversion is pgvector's to reject — every stored vector is the old width —
|
|
789
|
-
* so this refuses first and names the statement to run afterwards.
|
|
790
|
-
*
|
|
791
|
-
* `atttypmod` on a `vector` column *is* the dimension count: pgvector stores it
|
|
792
|
-
* directly rather than offsetting it the way `varchar` does. `-1` means the
|
|
793
|
-
* column was declared as a bare `vector`, which is drift in the same sense.
|
|
794
|
-
*/
|
|
795
|
-
var vectorDimensionGuard = (spec) => {
|
|
796
|
-
const relation = `"${spec.schema}"."${spec.table}"`;
|
|
797
|
-
return `DO $rebase_vector$
|
|
798
|
-
DECLARE actual int;
|
|
799
|
-
BEGIN
|
|
800
|
-
SELECT a.atttypmod INTO actual
|
|
801
|
-
FROM pg_attribute a
|
|
802
|
-
JOIN pg_type t ON t.oid = a.atttypid
|
|
803
|
-
WHERE a.attrelid = ${quote(relation)}::regclass
|
|
804
|
-
AND a.attname = ${quote(spec.column)}
|
|
805
|
-
AND NOT a.attisdropped
|
|
806
|
-
AND t.typname = 'vector';
|
|
807
|
-
IF actual IS NOT NULL AND actual <> ${spec.dimensions} THEN
|
|
808
|
-
IF EXISTS (SELECT 1 FROM ${relation} WHERE "${spec.column}" IS NOT NULL) THEN
|
|
809
|
-
RAISE EXCEPTION 'Rebase: ${spec.schema}.${spec.table}."${spec.column}" declares ${spec.dimensions} dimensions but the column is vector(%), and it already holds values of the old width. pgvector cannot convert them. Re-embed the rows at ${spec.dimensions} dimensions (or clear the column), then re-apply: ALTER TABLE ${relation} ALTER COLUMN "${spec.column}" TYPE ${vectorColumnType(spec)};', actual;
|
|
810
|
-
END IF;
|
|
811
|
-
ALTER TABLE ${relation} ALTER COLUMN "${spec.column}" TYPE ${vectorColumnType(spec)};
|
|
812
|
-
END IF;
|
|
813
|
-
END
|
|
814
|
-
$rebase_vector$;`;
|
|
815
|
-
};
|
|
816
|
-
//#endregion
|
|
817
431
|
//#region src/schema/generate-postgres-ddl-logic.ts
|
|
432
|
+
var generate_postgres_ddl_logic_exports = /* @__PURE__ */ __exportAll({
|
|
433
|
+
defaultBelongsToOnDelete: () => defaultBelongsToOnDelete,
|
|
434
|
+
generatePolicyStatements: () => generatePolicyStatements,
|
|
435
|
+
generatePostgresDdl: () => generatePostgresDdl,
|
|
436
|
+
generatePostgresPoliciesDdl: () => generatePostgresPoliciesDdl,
|
|
437
|
+
generatePostgresSearchDdl: () => generatePostgresSearchDdl,
|
|
438
|
+
generatePostgresVectorDdl: () => generatePostgresVectorDdl,
|
|
439
|
+
getPrimaryKeyName: () => getPrimaryKeyName,
|
|
440
|
+
getPrimaryKeyProp: () => getPrimaryKeyProp,
|
|
441
|
+
getSqlColumnType: () => getSqlColumnType,
|
|
442
|
+
isIdProperty: () => isIdProperty,
|
|
443
|
+
isNumericId: () => isNumericId,
|
|
444
|
+
planCollectionPolicies: () => planCollectionPolicies,
|
|
445
|
+
planJunctionTables: () => planJunctionTables,
|
|
446
|
+
planRelationalColumns: () => planRelationalColumns,
|
|
447
|
+
quoteSqlLiteral: () => quoteSqlLiteral,
|
|
448
|
+
resolveColumnName: () => resolveColumnName,
|
|
449
|
+
searchExcludePatterns: () => searchExcludePatterns,
|
|
450
|
+
vectorExcludePatterns: () => vectorExcludePatterns
|
|
451
|
+
});
|
|
818
452
|
var resolveColumnName = (propName, prop) => {
|
|
819
453
|
if (prop && "columnName" in prop && typeof prop.columnName === "string") return prop.columnName;
|
|
820
454
|
return toSnakeCase(propName);
|
|
@@ -864,6 +498,34 @@ var isIdProperty = (propName, prop, collection) => {
|
|
|
864
498
|
* the generated `policies.sql` emit the same SQL, from the same source.
|
|
865
499
|
*/
|
|
866
500
|
var statementsToDdl = (statements) => statements.map((s) => `${s}\n`).join("");
|
|
501
|
+
/**
|
|
502
|
+
* The `ON DELETE` a foreign key gets when the author did not say.
|
|
503
|
+
*
|
|
504
|
+
* One rule for every property that emits one — `belongsTo` relations and
|
|
505
|
+
* `reference` properties alike. `reference` kept its own `CASCADE` default for
|
|
506
|
+
* a release after this rule was written, which is exactly the split this
|
|
507
|
+
* function exists to prevent: two spellings of the same link, two data-retention
|
|
508
|
+
* behaviours, and only one of them documented.
|
|
509
|
+
*
|
|
510
|
+
* An optional link is `SET NULL`: the column can hold NULL, so dropping the
|
|
511
|
+
* parent leaves the child row with an empty pointer, which is what "optional"
|
|
512
|
+
* already means.
|
|
513
|
+
*
|
|
514
|
+
* A required link is **`RESTRICT`**, not `CASCADE`. `NOT NULL` says the child
|
|
515
|
+
* cannot exist without a parent; it does not say deleting the parent should
|
|
516
|
+
* take the child with it. That second claim is a data-retention decision, and
|
|
517
|
+
* defaulting to it meant `onDelete` — a field nobody has to write — silently
|
|
518
|
+
* turned every `DELETE FROM authors` into a cascade through posts, comments and
|
|
519
|
+
* anything else that hung off them. `RESTRICT` fails the delete instead and
|
|
520
|
+
* names the constraint, so the author chooses `cascade` on purpose or clears
|
|
521
|
+
* the children first.
|
|
522
|
+
*
|
|
523
|
+
* Both DDL paths — the `CREATE TABLE` generator and {@link planRelationalColumns},
|
|
524
|
+
* which boot-ensure compares the live database against — call this, so a
|
|
525
|
+
* default that appears in one and not the other cannot make `db push` plan a
|
|
526
|
+
* constraint rewrite on every run.
|
|
527
|
+
*/
|
|
528
|
+
var defaultBelongsToOnDelete = (required) => required ? "RESTRICT" : "SET NULL";
|
|
867
529
|
var generatePolicyDdl = (collection, rule, resolveCollection) => statementsToDdl(generatePolicyStatements(collection, rule, resolveCollection));
|
|
868
530
|
/**
|
|
869
531
|
* The individual SQL statements a single security rule compiles to: a
|
|
@@ -1058,6 +720,31 @@ var generatePostgresSearchDdl = (allCollections) => {
|
|
|
1058
720
|
return ddl;
|
|
1059
721
|
};
|
|
1060
722
|
/**
|
|
723
|
+
* Glob patterns telling Atlas to leave the search column and its index alone.
|
|
724
|
+
*
|
|
725
|
+
* Without these, a desired state that omits search reads to Atlas as an
|
|
726
|
+
* instruction to drop the column — taking the index and the whole search
|
|
727
|
+
* feature with it on the next push.
|
|
728
|
+
*
|
|
729
|
+
* Fully qualified, `schema.table.object`, matching the include list. The
|
|
730
|
+
* two-part form is what Atlas wants when the connection URL scopes it to one
|
|
731
|
+
* schema and is *silently ignored* otherwise: it reads `posts.search_vector`
|
|
732
|
+
* as a table named `search_vector` in a schema named `posts`, matches nothing,
|
|
733
|
+
* and reports no error for the pattern that never fired.
|
|
734
|
+
*/
|
|
735
|
+
var searchExcludePatterns = (allCollections) => {
|
|
736
|
+
const patterns = [];
|
|
737
|
+
for (const collection of relationalCollections(allCollections)) {
|
|
738
|
+
const spec = buildSearchColumnSpec(collection);
|
|
739
|
+
if (!spec) continue;
|
|
740
|
+
const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
741
|
+
const table = getTableName(collection);
|
|
742
|
+
for (const name of searchColumnNames(collection)) patterns.push(`${schema}.${table}.${name}`);
|
|
743
|
+
for (const index of searchIndexNames(spec)) patterns.push(`${schema}.${table}.${index}`);
|
|
744
|
+
}
|
|
745
|
+
return patterns;
|
|
746
|
+
};
|
|
747
|
+
/**
|
|
1061
748
|
* Everything a `{ type: "vector" }` property needs, as a file Rebase applies
|
|
1062
749
|
* itself.
|
|
1063
750
|
*
|
|
@@ -1113,6 +800,27 @@ var generatePostgresVectorDdl = (allCollections, options = {}) => {
|
|
|
1113
800
|
}
|
|
1114
801
|
return ddl;
|
|
1115
802
|
};
|
|
803
|
+
/**
|
|
804
|
+
* Glob patterns telling Atlas to leave the vector column and its ANN indexes
|
|
805
|
+
* alone.
|
|
806
|
+
*
|
|
807
|
+
* Without these, a desired state that omits the column reads to Atlas as an
|
|
808
|
+
* instruction to drop it, taking the embeddings and the indexes with it on the
|
|
809
|
+
* next push.
|
|
810
|
+
*
|
|
811
|
+
* Fully qualified, `schema.table.object`, for the reason spelled out on
|
|
812
|
+
* {@link searchExcludePatterns}: the two-part form matches nothing and reports
|
|
813
|
+
* no error, which turns a protection into a `DROP COLUMN`.
|
|
814
|
+
*/
|
|
815
|
+
var vectorExcludePatterns = (allCollections) => {
|
|
816
|
+
const patterns = [];
|
|
817
|
+
for (const collection of relationalCollections(allCollections)) {
|
|
818
|
+
const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
819
|
+
const table = getTableName(collection);
|
|
820
|
+
for (const name of vectorObjectNames(collection, resolveColumnName)) patterns.push(`${schema}.${table}.${name}`);
|
|
821
|
+
}
|
|
822
|
+
return patterns;
|
|
823
|
+
};
|
|
1116
824
|
var generatePostgresDdl = async (allCollections, options = {
|
|
1117
825
|
includePolicies: true,
|
|
1118
826
|
includeSearch: true,
|
|
@@ -1234,7 +942,7 @@ var generatePostgresDdl = async (allCollections, options = {
|
|
|
1234
942
|
const fkColType = getSqlColumnType(propName, prop, collection, collections);
|
|
1235
943
|
const onUpdate = relInfo.onUpdate ? ` ON UPDATE ${relInfo.onUpdate.toUpperCase()}` : "";
|
|
1236
944
|
const required = prop.validation?.required;
|
|
1237
|
-
const onDeleteVal = relInfo.onDelete ?? (required
|
|
945
|
+
const onDeleteVal = relInfo.onDelete ?? defaultBelongsToOnDelete(required);
|
|
1238
946
|
let colDef = ` "${relInfo.localKey}" ${fkColType}`;
|
|
1239
947
|
if (required) colDef += " NOT NULL";
|
|
1240
948
|
columns.push(colDef);
|
|
@@ -1253,7 +961,7 @@ var generatePostgresDdl = async (allCollections, options = {
|
|
|
1253
961
|
const targetTable = getTableName(targetCollection);
|
|
1254
962
|
const targetSchema = isPostgresCollectionConfig(targetCollection) && targetCollection.schema ? targetCollection.schema : "public";
|
|
1255
963
|
const targetId = getPrimaryKeyName(targetCollection);
|
|
1256
|
-
const onDelete = required
|
|
964
|
+
const onDelete = defaultBelongsToOnDelete(required);
|
|
1257
965
|
let colDef = ` "${colName}" ${colType}`;
|
|
1258
966
|
if (required) colDef += " NOT NULL";
|
|
1259
967
|
columns.push(colDef);
|
|
@@ -1420,7 +1128,7 @@ var planRelationalColumns = (allCollections) => {
|
|
|
1420
1128
|
targetSchema: schemaOfCollection(targetCollection),
|
|
1421
1129
|
targetTable: bareTableName(getTableName(targetCollection)),
|
|
1422
1130
|
targetColumn: getPrimaryKeyName(targetCollection),
|
|
1423
|
-
onDelete: relInfo.onDelete ?? (required
|
|
1131
|
+
onDelete: relInfo.onDelete ?? defaultBelongsToOnDelete(required),
|
|
1424
1132
|
onUpdate: relInfo.onUpdate
|
|
1425
1133
|
})
|
|
1426
1134
|
});
|
|
@@ -1442,7 +1150,7 @@ var planRelationalColumns = (allCollections) => {
|
|
|
1442
1150
|
targetSchema: schemaOfCollection(targetCollection),
|
|
1443
1151
|
targetTable: bareTableName(getTableName(targetCollection)),
|
|
1444
1152
|
targetColumn: getPrimaryKeyName(targetCollection),
|
|
1445
|
-
onDelete: required
|
|
1153
|
+
onDelete: defaultBelongsToOnDelete(required)
|
|
1446
1154
|
}) : void 0
|
|
1447
1155
|
});
|
|
1448
1156
|
}
|
|
@@ -1599,572 +1307,6 @@ var generatePostgresPoliciesDdl = (allCollections) => {
|
|
|
1599
1307
|
return ddl;
|
|
1600
1308
|
};
|
|
1601
1309
|
//#endregion
|
|
1602
|
-
|
|
1603
|
-
/**
|
|
1604
|
-
* Bringing a database up to date with a bundle's collections, additively.
|
|
1605
|
-
*
|
|
1606
|
-
* ## Why this exists
|
|
1607
|
-
*
|
|
1608
|
-
* A managed runtime boots someone else's compiled project against a database it
|
|
1609
|
-
* has never seen. Auth tables are ensured at boot already, but collection tables
|
|
1610
|
-
* were not created by anything: the platform ran the app and every `/api/data/*`
|
|
1611
|
-
* request answered 500 on a missing relation. `rebase db push` cannot help — it
|
|
1612
|
-
* is an Atlas-driven CLI command, and the runtime image ships no CLI.
|
|
1613
|
-
*
|
|
1614
|
-
* ## Why additive-only, forever
|
|
1615
|
-
*
|
|
1616
|
-
* This runs unattended, against a database with customers' data in it, with no
|
|
1617
|
-
* human reading a diff. So it may only ever do things that cannot lose data:
|
|
1618
|
-
* create a missing table, add a missing column, create a missing enum type.
|
|
1619
|
-
*
|
|
1620
|
-
* It will **never** drop a table or a column, narrow a type, or alter a
|
|
1621
|
-
* constraint. A removed field leaves its column behind; a renamed field looks
|
|
1622
|
-
* like an addition and the old column stays. That is the correct trade for an
|
|
1623
|
-
* automated path — the alternative is an unattended process that can silently
|
|
1624
|
-
* destroy a column, which is precisely the failure `db push` was hardened
|
|
1625
|
-
* against. Destructive changes stay a deliberate, human-reviewed migration.
|
|
1626
|
-
*
|
|
1627
|
-
* Because of that, this is safe to run on every boot, and re-running it is a
|
|
1628
|
-
* no-op.
|
|
1629
|
-
*/
|
|
1630
|
-
var ensure_collection_tables_exports = /* @__PURE__ */ __exportAll({
|
|
1631
|
-
ensureCollectionTables: () => ensureCollectionTables,
|
|
1632
|
-
planCollectionSchemaEnsure: () => planCollectionSchemaEnsure,
|
|
1633
|
-
readExistingSchema: () => readExistingSchema,
|
|
1634
|
-
readSchemaFactsFor: () => readSchemaFactsFor
|
|
1635
|
-
});
|
|
1636
|
-
/** Postgres identifiers this module is willing to interpolate. */
|
|
1637
|
-
var SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;
|
|
1638
|
-
function assertSafeIdentifier(value, what) {
|
|
1639
|
-
if (!SAFE_IDENTIFIER.test(value)) throw new Error(`Refusing to build SQL with an unsafe ${what}: ${JSON.stringify(value)}`);
|
|
1640
|
-
return value;
|
|
1641
|
-
}
|
|
1642
|
-
function schemaOf(collection) {
|
|
1643
|
-
return isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
1644
|
-
}
|
|
1645
|
-
function qualified(collection) {
|
|
1646
|
-
return `${schemaOf(collection)}.${getTableName(collection)}`;
|
|
1647
|
-
}
|
|
1648
|
-
/**
|
|
1649
|
-
* Enum types a collection's properties require, as `schema.typename`.
|
|
1650
|
-
*
|
|
1651
|
-
* Named exactly as the DDL generator names them (`<table>_<column>`), because
|
|
1652
|
-
* a column added here has to reference the same type the generator would have
|
|
1653
|
-
* created — a second, differently-named type for the same field would be a
|
|
1654
|
-
* silent schema fork.
|
|
1655
|
-
*/
|
|
1656
|
-
function requiredEnums(collection) {
|
|
1657
|
-
const table = getTableName(collection);
|
|
1658
|
-
const schema = schemaOf(collection);
|
|
1659
|
-
const out = [];
|
|
1660
|
-
for (const [propName, prop] of Object.entries(collection.properties ?? {})) {
|
|
1661
|
-
const p = prop;
|
|
1662
|
-
if (!("enum" in p) || !p.enum) continue;
|
|
1663
|
-
if (p.type !== "string" && p.type !== "number") continue;
|
|
1664
|
-
const values = p.enum.map((entry) => entry && typeof entry === "object" && "id" in entry ? String(entry.id) : String(entry)).filter((v) => v.length > 0);
|
|
1665
|
-
if (values.length === 0) continue;
|
|
1666
|
-
out.push({
|
|
1667
|
-
name: `${schema}.${table}_${resolveColumnName(propName, p)}`,
|
|
1668
|
-
values
|
|
1669
|
-
});
|
|
1670
|
-
}
|
|
1671
|
-
return out;
|
|
1672
|
-
}
|
|
1673
|
-
/**
|
|
1674
|
-
* Decide what to add. Pure — the caller supplies what exists and runs the result.
|
|
1675
|
-
*
|
|
1676
|
-
* Ordering matters and is deliberate: enum types before the tables and columns
|
|
1677
|
-
* that reference them, tables before the columns added to other tables (a new
|
|
1678
|
-
* table may be the target of a relation), and nothing is emitted twice.
|
|
1679
|
-
*/
|
|
1680
|
-
function planCollectionSchemaEnsure(allCollections, existing, options = {}) {
|
|
1681
|
-
const constraintPolicy = options.constraints ?? "additive";
|
|
1682
|
-
const withheldConstraints = [];
|
|
1683
|
-
assertSearchIsPostgresOnly(allCollections);
|
|
1684
|
-
const collections = relationalCollections(allCollections);
|
|
1685
|
-
const actions = [];
|
|
1686
|
-
const plannedEnums = /* @__PURE__ */ new Set();
|
|
1687
|
-
for (const collection of collections) for (const { name, values } of requiredEnums(collection)) {
|
|
1688
|
-
if (existing.enums.has(name) || plannedEnums.has(name)) {
|
|
1689
|
-
const current = existing.enumValues?.get(name);
|
|
1690
|
-
if (!current || plannedEnums.has(name)) continue;
|
|
1691
|
-
const [schema, typeName] = name.split(".");
|
|
1692
|
-
for (const value of values) {
|
|
1693
|
-
if (current.includes(value)) continue;
|
|
1694
|
-
actions.push({
|
|
1695
|
-
kind: "add-enum-value",
|
|
1696
|
-
target: `${name}.${value}`,
|
|
1697
|
-
sql: `ALTER TYPE "${schema}"."${typeName}" ADD VALUE IF NOT EXISTS ${quoteSqlLiteral(value)};`
|
|
1698
|
-
});
|
|
1699
|
-
}
|
|
1700
|
-
continue;
|
|
1701
|
-
}
|
|
1702
|
-
plannedEnums.add(name);
|
|
1703
|
-
const [schema, typeName] = name.split(".");
|
|
1704
|
-
actions.push({
|
|
1705
|
-
kind: "create-enum",
|
|
1706
|
-
target: name,
|
|
1707
|
-
sql: `CREATE TYPE "${schema}"."${typeName}" AS ENUM (${values.map(quoteSqlLiteral).join(", ")});`
|
|
1708
|
-
});
|
|
1709
|
-
}
|
|
1710
|
-
const searchSpecs = collections.map((c) => buildSearchColumnSpec(c)).filter((spec) => spec !== void 0);
|
|
1711
|
-
const plannedExtensions = /* @__PURE__ */ new Set();
|
|
1712
|
-
const planExtension = (statement) => {
|
|
1713
|
-
if (plannedExtensions.has(statement)) return;
|
|
1714
|
-
plannedExtensions.add(statement);
|
|
1715
|
-
actions.push({
|
|
1716
|
-
kind: "create-extension",
|
|
1717
|
-
target: statement.replace(/^CREATE EXTENSION IF NOT EXISTS |;$/g, ""),
|
|
1718
|
-
sql: statement
|
|
1719
|
-
});
|
|
1720
|
-
};
|
|
1721
|
-
for (const spec of searchSpecs) for (const statement of searchExtensionStatements(spec)) planExtension(statement);
|
|
1722
|
-
if (vectorExtensionDeclared(options.databaseExtensions) && collections.some((c) => buildVectorColumnSpecs(c, resolveColumnName).length > 0)) planExtension(vectorExtensionStatement());
|
|
1723
|
-
const plannedFunctions = /* @__PURE__ */ new Set();
|
|
1724
|
-
for (const spec of searchSpecs) for (const statement of searchHelperFunctions(spec)) {
|
|
1725
|
-
if (plannedFunctions.has(statement)) continue;
|
|
1726
|
-
plannedFunctions.add(statement);
|
|
1727
|
-
actions.push({
|
|
1728
|
-
kind: "create-function",
|
|
1729
|
-
target: statement.includes("unaccent") ? SEARCH_UNACCENT_FN : SEARCH_TEXT_FN,
|
|
1730
|
-
sql: statement
|
|
1731
|
-
});
|
|
1732
|
-
}
|
|
1733
|
-
const created = /* @__PURE__ */ new Set();
|
|
1734
|
-
for (const collection of collections) {
|
|
1735
|
-
const key = qualified(collection);
|
|
1736
|
-
if (existing.tables.has(key) || created.has(key)) continue;
|
|
1737
|
-
created.add(key);
|
|
1738
|
-
const schema = schemaOf(collection);
|
|
1739
|
-
const table = getTableName(collection);
|
|
1740
|
-
const idEntry = Object.entries(collection.properties ?? {}).find(([n, p]) => isIdProperty(n, p, collection));
|
|
1741
|
-
const idName = idEntry ? resolveColumnName(idEntry[0], idEntry[1]) : "id";
|
|
1742
|
-
const idProp = idEntry?.[1];
|
|
1743
|
-
let idDef = `"${idName}" ${idProp ? getSqlColumnType(idEntry[0], idProp, collection, collections) : "TEXT"} PRIMARY KEY`;
|
|
1744
|
-
if (idProp?.type === "string" && idProp.isId === "uuid") idDef += " DEFAULT gen_random_uuid()";
|
|
1745
|
-
actions.push({
|
|
1746
|
-
kind: "create-table",
|
|
1747
|
-
target: key,
|
|
1748
|
-
sql: `CREATE TABLE IF NOT EXISTS "${schema}"."${table}" (${idDef});`
|
|
1749
|
-
});
|
|
1750
|
-
}
|
|
1751
|
-
const junctions = planJunctionTables(collections);
|
|
1752
|
-
for (const junction of junctions) {
|
|
1753
|
-
const key = `${junction.schema}.${junction.table}`;
|
|
1754
|
-
if (existing.tables.has(key) || created.has(key)) continue;
|
|
1755
|
-
created.add(key);
|
|
1756
|
-
actions.push({
|
|
1757
|
-
kind: "create-table",
|
|
1758
|
-
target: key,
|
|
1759
|
-
sql: junction.createTable
|
|
1760
|
-
});
|
|
1761
|
-
}
|
|
1762
|
-
const legacyForeignKeys = [];
|
|
1763
|
-
/**
|
|
1764
|
-
* Move a relation column that is only missing because it was renamed.
|
|
1765
|
-
*
|
|
1766
|
-
* Returns true when it handled the column, so the caller skips the ordinary
|
|
1767
|
-
* ADD. Adding here would be the wrong move and a quiet one: the data is in
|
|
1768
|
-
* the old column, `ADD COLUMN` creates the new one empty beside it, every
|
|
1769
|
-
* statement succeeds, and the relation reads the empty one. A rename is
|
|
1770
|
-
* metadata-only in Postgres, keeps the values, and carries the column's
|
|
1771
|
-
* indexes and constraints with it.
|
|
1772
|
-
*
|
|
1773
|
-
* Only ever reached when the new name is absent and the old name is
|
|
1774
|
-
* present, so there is nothing to overwrite and nothing to choose between.
|
|
1775
|
-
*/
|
|
1776
|
-
const renameLegacyColumn = (key, schema, table, column, legacyName) => {
|
|
1777
|
-
const present = existing.tables.get(key);
|
|
1778
|
-
if (!legacyName || !present) return false;
|
|
1779
|
-
if (present.has(column) || !present.has(legacyName)) return false;
|
|
1780
|
-
legacyForeignKeys.push({
|
|
1781
|
-
table: key,
|
|
1782
|
-
expected: column,
|
|
1783
|
-
legacy: legacyName
|
|
1784
|
-
});
|
|
1785
|
-
actions.push({
|
|
1786
|
-
kind: "rename-column",
|
|
1787
|
-
target: `${key}.${column}`,
|
|
1788
|
-
sql: `ALTER TABLE "${schema}"."${table}" RENAME COLUMN "${legacyName}" TO "${column}";`
|
|
1789
|
-
});
|
|
1790
|
-
return true;
|
|
1791
|
-
};
|
|
1792
|
-
const addColumn = (key, schema, table, column, definition) => {
|
|
1793
|
-
if (existing.tables.get(key)?.has(column)) return;
|
|
1794
|
-
actions.push({
|
|
1795
|
-
kind: "add-column",
|
|
1796
|
-
target: `${key}.${column}`,
|
|
1797
|
-
sql: `ALTER TABLE "${schema}"."${table}" ADD COLUMN IF NOT EXISTS "${column}" ${definition};`
|
|
1798
|
-
});
|
|
1799
|
-
};
|
|
1800
|
-
for (const collection of collections) {
|
|
1801
|
-
const key = qualified(collection);
|
|
1802
|
-
const schema = schemaOf(collection);
|
|
1803
|
-
const table = getTableName(collection);
|
|
1804
|
-
const fresh = created.has(key);
|
|
1805
|
-
const auth = isAuthCollection(collection);
|
|
1806
|
-
for (const [propName, prop] of Object.entries(collection.properties ?? {})) {
|
|
1807
|
-
const p = prop;
|
|
1808
|
-
if (isIdProperty(propName, p, collection)) continue;
|
|
1809
|
-
if (p.type === "reference" || p.type === "relation") continue;
|
|
1810
|
-
const column = resolveColumnName(propName, p);
|
|
1811
|
-
const authDefinition = auth ? authUsersColumnDefinition(column) : void 0;
|
|
1812
|
-
if (authDefinition) {
|
|
1813
|
-
addColumn(key, schema, table, column, authDefinition);
|
|
1814
|
-
continue;
|
|
1815
|
-
}
|
|
1816
|
-
let definition = getSqlColumnType(propName, p, collection, collections);
|
|
1817
|
-
if (fresh && p.validation?.unique) definition += " UNIQUE";
|
|
1818
|
-
const autoValue = p.autoValue;
|
|
1819
|
-
const hasDefault = p.type === "date" && (autoValue === "on_create" || autoValue === "on_update");
|
|
1820
|
-
if (hasDefault) definition += " DEFAULT now()";
|
|
1821
|
-
const required = p.validation?.required === true;
|
|
1822
|
-
const columnKey = `${key}.${column}`;
|
|
1823
|
-
const columnExists = existing.tables.get(key)?.has(column) === true;
|
|
1824
|
-
const tableIsEmpty = existing.populatedTables !== void 0 && existing.tables.has(key) && !existing.populatedTables.has(key);
|
|
1825
|
-
const notNullIsSafe = fresh || tableIsEmpty || hasDefault;
|
|
1826
|
-
if (required && !columnExists) if (notNullIsSafe) definition += " NOT NULL";
|
|
1827
|
-
else withheldConstraints.push({
|
|
1828
|
-
target: columnKey,
|
|
1829
|
-
kind: "not-null",
|
|
1830
|
-
reason: `"${column}" is required, but "${key}" already holds rows and the column has no default to backfill them with, so NOT NULL would be checked against data that does not have a value yet.`,
|
|
1831
|
-
remedy: "Backfill the column, then add the constraint — or give the property a default so every existing row gets one."
|
|
1832
|
-
});
|
|
1833
|
-
addColumn(key, schema, table, column, definition);
|
|
1834
|
-
if (columnExists && constraintPolicy === "converge") {
|
|
1835
|
-
const isNotNull = existing.notNullColumns?.has(columnKey) === true;
|
|
1836
|
-
if (required && !isNotNull) if (tableIsEmpty) actions.push({
|
|
1837
|
-
kind: "set-not-null",
|
|
1838
|
-
target: columnKey,
|
|
1839
|
-
sql: `ALTER TABLE "${schema}"."${table}" ALTER COLUMN "${column}" SET NOT NULL;`
|
|
1840
|
-
});
|
|
1841
|
-
else withheldConstraints.push({
|
|
1842
|
-
target: columnKey,
|
|
1843
|
-
kind: "not-null",
|
|
1844
|
-
reason: `"${column}" became required, but "${key}" holds rows and any of them with no value would make SET NOT NULL fail.`,
|
|
1845
|
-
remedy: "Backfill the column first — `UPDATE … SET \"" + column + "\" = … WHERE \"" + column + "\" IS NULL` — then apply this again."
|
|
1846
|
-
});
|
|
1847
|
-
if (!required && isNotNull) actions.push({
|
|
1848
|
-
kind: "drop-not-null",
|
|
1849
|
-
target: columnKey,
|
|
1850
|
-
sql: `ALTER TABLE "${schema}"."${table}" ALTER COLUMN "${column}" DROP NOT NULL;`
|
|
1851
|
-
});
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
1854
|
-
if (auth) {
|
|
1855
|
-
const declared = new Set(Object.entries(collection.properties ?? {}).map(([name, prop]) => resolveColumnName(name, prop)));
|
|
1856
|
-
for (const spec of AUTH_USERS_COLUMNS) {
|
|
1857
|
-
if (declared.has(spec.column)) continue;
|
|
1858
|
-
addColumn(key, schema, table, spec.column, authUsersColumnSql(spec));
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
}
|
|
1862
|
-
const searchDrift = [];
|
|
1863
|
-
const searchAdopted = [];
|
|
1864
|
-
for (const spec of searchSpecs) {
|
|
1865
|
-
const key = `${spec.schema}.${spec.table}`;
|
|
1866
|
-
const definitions = { [spec.column]: `tsvector GENERATED ALWAYS AS (${spec.expression}) STORED` };
|
|
1867
|
-
if (spec.fuzzy) definitions[spec.fuzzy.column] = `text GENERATED ALWAYS AS (${spec.fuzzy.expression}) STORED`;
|
|
1868
|
-
for (const stamp of searchColumnStamps(spec)) {
|
|
1869
|
-
const definition = definitions[stamp.column];
|
|
1870
|
-
const exists = existing.tables.get(key)?.has(stamp.column) === true;
|
|
1871
|
-
const recorded = existing.columnComments?.get(`${key}.${stamp.column}`);
|
|
1872
|
-
if (exists && recorded?.startsWith("rebase:search:v1:") && recorded !== stamp.fingerprint) {
|
|
1873
|
-
searchDrift.push({
|
|
1874
|
-
table: key,
|
|
1875
|
-
column: stamp.column,
|
|
1876
|
-
found: recorded,
|
|
1877
|
-
expected: stamp.fingerprint,
|
|
1878
|
-
rebuild: [
|
|
1879
|
-
`ALTER TABLE "${spec.schema}"."${spec.table}" DROP COLUMN "${stamp.column}";`,
|
|
1880
|
-
`ALTER TABLE "${spec.schema}"."${spec.table}" ADD COLUMN "${stamp.column}" ${definition};`,
|
|
1881
|
-
stamp.sql
|
|
1882
|
-
]
|
|
1883
|
-
});
|
|
1884
|
-
continue;
|
|
1885
|
-
}
|
|
1886
|
-
addColumn(key, spec.schema, spec.table, stamp.column, definition);
|
|
1887
|
-
if (exists && recorded === void 0) searchAdopted.push({
|
|
1888
|
-
table: key,
|
|
1889
|
-
column: stamp.column
|
|
1890
|
-
});
|
|
1891
|
-
if (recorded !== stamp.fingerprint) actions.push({
|
|
1892
|
-
kind: "comment-column",
|
|
1893
|
-
target: `${key}.${stamp.column}`,
|
|
1894
|
-
sql: stamp.sql
|
|
1895
|
-
});
|
|
1896
|
-
}
|
|
1897
|
-
}
|
|
1898
|
-
for (const junction of junctions) {
|
|
1899
|
-
const key = `${junction.schema}.${junction.table}`;
|
|
1900
|
-
if (created.has(key)) continue;
|
|
1901
|
-
for (const column of junction.columns) {
|
|
1902
|
-
if (renameLegacyColumn(key, junction.schema, junction.table, column.name, column.legacyName)) continue;
|
|
1903
|
-
addColumn(key, junction.schema, junction.table, column.name, column.type);
|
|
1904
|
-
}
|
|
1905
|
-
}
|
|
1906
|
-
for (const relational of planRelationalColumns(collections)) {
|
|
1907
|
-
const relKey = `${relational.schema}.${relational.table}`;
|
|
1908
|
-
if (renameLegacyColumn(relKey, relational.schema, relational.table, relational.column, relational.legacyColumn)) continue;
|
|
1909
|
-
addColumn(relKey, relational.schema, relational.table, relational.column, relational.type);
|
|
1910
|
-
}
|
|
1911
|
-
const knownConstraints = existing.constraints ?? /* @__PURE__ */ new Set();
|
|
1912
|
-
const plannedConstraints = /* @__PURE__ */ new Set();
|
|
1913
|
-
const foreignKeys = [...planRelationalColumns(collections).map((r) => r.foreignKey), ...junctions.flatMap((j) => j.foreignKeys)];
|
|
1914
|
-
for (const fk of foreignKeys) {
|
|
1915
|
-
if (!fk) continue;
|
|
1916
|
-
const name = `${fk.schema}.${fk.table}.${fk.constraintName}`;
|
|
1917
|
-
if (knownConstraints.has(name) || plannedConstraints.has(name)) continue;
|
|
1918
|
-
plannedConstraints.add(name);
|
|
1919
|
-
actions.push({
|
|
1920
|
-
kind: "add-constraint",
|
|
1921
|
-
target: `${fk.schema}.${fk.table}.${fk.constraintName}`,
|
|
1922
|
-
sql: fk.sql
|
|
1923
|
-
});
|
|
1924
|
-
}
|
|
1925
|
-
for (const spec of searchSpecs) for (const statement of searchIndexStatements(spec)) actions.push({
|
|
1926
|
-
kind: "create-index",
|
|
1927
|
-
target: `${spec.schema}.${spec.table}`,
|
|
1928
|
-
sql: statement.replace("CREATE INDEX IF NOT EXISTS", "CREATE INDEX CONCURRENTLY IF NOT EXISTS")
|
|
1929
|
-
});
|
|
1930
|
-
const vectorIndexSkipped = [];
|
|
1931
|
-
for (const collection of collections) {
|
|
1932
|
-
const plan = buildVectorIndexPlan(collection, resolveColumnName);
|
|
1933
|
-
for (const spec of plan.specs) actions.push({
|
|
1934
|
-
kind: "create-index",
|
|
1935
|
-
target: `${spec.schema}.${spec.table}`,
|
|
1936
|
-
sql: vectorIndexStatement(spec).replace("CREATE INDEX IF NOT EXISTS", "CREATE INDEX CONCURRENTLY IF NOT EXISTS")
|
|
1937
|
-
});
|
|
1938
|
-
vectorIndexSkipped.push(...plan.skipped);
|
|
1939
|
-
}
|
|
1940
|
-
for (const spec of buildCollectionIndexPlan(collections, resolveColumnName)) actions.push({
|
|
1941
|
-
kind: "create-index",
|
|
1942
|
-
target: `${spec.schema}.${spec.table}`,
|
|
1943
|
-
sql: collectionIndexStatement(spec, {
|
|
1944
|
-
concurrently: true,
|
|
1945
|
-
ifNotExists: true
|
|
1946
|
-
})
|
|
1947
|
-
});
|
|
1948
|
-
return {
|
|
1949
|
-
actions,
|
|
1950
|
-
statements: actions.map((a) => a.sql),
|
|
1951
|
-
legacyForeignKeys,
|
|
1952
|
-
searchDrift,
|
|
1953
|
-
searchAdopted,
|
|
1954
|
-
vectorIndexSkipped,
|
|
1955
|
-
withheldConstraints
|
|
1956
|
-
};
|
|
1957
|
-
}
|
|
1958
|
-
/** Read what the database has, for the schemas the collections live in. */
|
|
1959
|
-
async function readExistingSchema(client, schemas) {
|
|
1960
|
-
const tables = /* @__PURE__ */ new Map();
|
|
1961
|
-
const enums = /* @__PURE__ */ new Set();
|
|
1962
|
-
if (schemas.length === 0) return {
|
|
1963
|
-
tables,
|
|
1964
|
-
enums
|
|
1965
|
-
};
|
|
1966
|
-
const inList = schemas.map((schema) => `'${assertSafeIdentifier(schema, "schema name")}'`).join(", ");
|
|
1967
|
-
const notNullColumns = /* @__PURE__ */ new Set();
|
|
1968
|
-
const { rows: columns } = await client.query(`SELECT table_schema, table_name, column_name, is_nullable
|
|
1969
|
-
FROM information_schema.columns
|
|
1970
|
-
WHERE table_schema IN (${inList})`);
|
|
1971
|
-
for (const row of columns) {
|
|
1972
|
-
const key = `${row.table_schema}.${row.table_name}`;
|
|
1973
|
-
if (!tables.has(key)) tables.set(key, /* @__PURE__ */ new Set());
|
|
1974
|
-
tables.get(key).add(row.column_name);
|
|
1975
|
-
if (row.is_nullable === "NO") notNullColumns.add(`${key}.${row.column_name}`);
|
|
1976
|
-
}
|
|
1977
|
-
const populatedTables = /* @__PURE__ */ new Set();
|
|
1978
|
-
const { rows: realTables } = await client.query(`SELECT n.nspname AS schema, c.relname AS name
|
|
1979
|
-
FROM pg_class c
|
|
1980
|
-
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
1981
|
-
WHERE c.relkind IN ('r', 'p') AND n.nspname IN (${inList})`);
|
|
1982
|
-
if (realTables.length > 0) {
|
|
1983
|
-
const probes = realTables.map((row) => {
|
|
1984
|
-
const schema = assertSafeIdentifier(row.schema, "schema name");
|
|
1985
|
-
const table = assertSafeIdentifier(row.name, "table name");
|
|
1986
|
-
return `SELECT ${quoteSqlLiteral(`${schema}.${table}`)} AS key, EXISTS(SELECT 1 FROM "${schema}"."${table}" LIMIT 1) AS populated`;
|
|
1987
|
-
});
|
|
1988
|
-
const { rows: populationRows } = await client.query(probes.join(" UNION ALL "));
|
|
1989
|
-
for (const row of populationRows) if (row.populated) populatedTables.add(row.key);
|
|
1990
|
-
}
|
|
1991
|
-
const enumValues = /* @__PURE__ */ new Map();
|
|
1992
|
-
const { rows: enumValueRows } = await client.query(`SELECT n.nspname AS schema, t.typname AS name, e.enumlabel AS value
|
|
1993
|
-
FROM pg_enum e
|
|
1994
|
-
JOIN pg_type t ON e.enumtypid = t.oid
|
|
1995
|
-
JOIN pg_namespace n ON t.typnamespace = n.oid
|
|
1996
|
-
WHERE n.nspname IN (${inList})
|
|
1997
|
-
ORDER BY t.typname, e.enumsortorder`);
|
|
1998
|
-
for (const row of enumValueRows) {
|
|
1999
|
-
const key = `${row.schema}.${row.name}`;
|
|
2000
|
-
if (!enumValues.has(key)) enumValues.set(key, []);
|
|
2001
|
-
enumValues.get(key).push(row.value);
|
|
2002
|
-
}
|
|
2003
|
-
const { rows: enumRows } = await client.query(`SELECT n.nspname AS schema, t.typname AS name
|
|
2004
|
-
FROM pg_type t
|
|
2005
|
-
JOIN pg_namespace n ON t.typnamespace = n.oid
|
|
2006
|
-
WHERE t.typtype = 'e' AND n.nspname IN (${inList})`);
|
|
2007
|
-
for (const row of enumRows) enums.add(`${row.schema}.${row.name}`);
|
|
2008
|
-
const constraints = /* @__PURE__ */ new Set();
|
|
2009
|
-
const { rows: constraintRows } = await client.query(`SELECT n.nspname AS schema, c.relname AS table, con.conname AS name
|
|
2010
|
-
FROM pg_constraint con
|
|
2011
|
-
JOIN pg_class c ON con.conrelid = c.oid
|
|
2012
|
-
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
2013
|
-
WHERE n.nspname IN (${inList})`);
|
|
2014
|
-
for (const row of constraintRows) constraints.add(`${row.schema}.${row.table}.${row.name}`);
|
|
2015
|
-
const columnComments = /* @__PURE__ */ new Map();
|
|
2016
|
-
const { rows: commentRows } = await client.query(`SELECT n.nspname AS schema, c.relname AS table, a.attname AS column, d.description AS comment
|
|
2017
|
-
FROM pg_description d
|
|
2018
|
-
JOIN pg_class c ON d.objoid = c.oid
|
|
2019
|
-
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
2020
|
-
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = d.objsubid
|
|
2021
|
-
WHERE d.objsubid > 0 AND n.nspname IN (${inList})`);
|
|
2022
|
-
for (const row of commentRows) {
|
|
2023
|
-
if (row.comment == null) continue;
|
|
2024
|
-
columnComments.set(`${row.schema}.${row.table}.${row.column}`, row.comment);
|
|
2025
|
-
}
|
|
2026
|
-
return {
|
|
2027
|
-
tables,
|
|
2028
|
-
enums,
|
|
2029
|
-
constraints,
|
|
2030
|
-
columnComments,
|
|
2031
|
-
enumValues,
|
|
2032
|
-
notNullColumns,
|
|
2033
|
-
populatedTables
|
|
2034
|
-
};
|
|
2035
|
-
}
|
|
2036
|
-
/**
|
|
2037
|
-
* What to tell an operator whose `search` block no longer matches its column.
|
|
2038
|
-
*
|
|
2039
|
-
* Every line here is doing work: naming the collection is not enough, because
|
|
2040
|
-
* the symptom (a search that finds nothing) points at the data, not the schema;
|
|
2041
|
-
* and the remediation has to be exact, because it is a table rewrite the
|
|
2042
|
-
* operator is being asked to schedule rather than discover.
|
|
2043
|
-
*/
|
|
2044
|
-
function searchDriftMessage(drift) {
|
|
2045
|
-
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.";
|
|
2046
|
-
}
|
|
2047
|
-
/**
|
|
2048
|
-
* Read what the database looks like, for the schemas a set of collections
|
|
2049
|
-
* lives in.
|
|
2050
|
-
*
|
|
2051
|
-
* The same read `ensureCollectionTables` does at boot, exposed on its own for
|
|
2052
|
-
* the callers that want to *plan* against a real database without changing it —
|
|
2053
|
-
* the live schema editor, which has to tell somebody what a change would do
|
|
2054
|
-
* before they agree to it.
|
|
2055
|
-
*/
|
|
2056
|
-
async function readSchemaFactsFor(client, collections) {
|
|
2057
|
-
const relational = relationalCollections(collections);
|
|
2058
|
-
return readExistingSchema(client, Array.from(/* @__PURE__ */ new Set([...relational.map(schemaOf), ...planJunctionTables(relational).map((junction) => junction.schema)])));
|
|
2059
|
-
}
|
|
2060
|
-
/**
|
|
2061
|
-
* Bring the database up to date. Returns what it did.
|
|
2062
|
-
*
|
|
2063
|
-
* Each statement runs on its own rather than in one transaction: they are all
|
|
2064
|
-
* independently safe and idempotent, and a single failure (an enum label that
|
|
2065
|
-
* cannot be added, say) should not roll back the tables that were created fine.
|
|
2066
|
-
* The error is surfaced with the statement that caused it.
|
|
2067
|
-
*/
|
|
2068
|
-
async function ensureCollectionTables(client, collections, log, options = {}) {
|
|
2069
|
-
const schemas = Array.from(/* @__PURE__ */ new Set([...collections.map(schemaOf), ...planJunctionTables(collections).map((j) => j.schema)]));
|
|
2070
|
-
for (const schema of schemas) {
|
|
2071
|
-
assertSafeIdentifier(schema, "schema name");
|
|
2072
|
-
if (schema !== "public") await client.query(`CREATE SCHEMA IF NOT EXISTS "${schema}";`);
|
|
2073
|
-
}
|
|
2074
|
-
const plan = planCollectionSchemaEnsure(collections, await readExistingSchema(client, schemas), {
|
|
2075
|
-
...options,
|
|
2076
|
-
databaseExtensions: options.databaseExtensions ?? declaredDatabaseExtensions()
|
|
2077
|
-
});
|
|
2078
|
-
const failures = [];
|
|
2079
|
-
for (const legacy of plan.legacyForeignKeys) {
|
|
2080
|
-
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.`;
|
|
2081
|
-
logger.info(`[schema] ${message}`);
|
|
2082
|
-
log?.(message);
|
|
2083
|
-
}
|
|
2084
|
-
if (plan.searchDrift.length > 0) throw new Error(searchDriftMessage(plan.searchDrift));
|
|
2085
|
-
for (const adopted of plan.searchAdopted) {
|
|
2086
|
-
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.`;
|
|
2087
|
-
logger.info(`[schema] ${message}`);
|
|
2088
|
-
log?.(message);
|
|
2089
|
-
}
|
|
2090
|
-
for (const skip of plan.vectorIndexSkipped) {
|
|
2091
|
-
const message = `No ANN index on "${skip.table}"."${skip.column}": ${skip.reason}`;
|
|
2092
|
-
logger.warn(`[schema] ${message}`);
|
|
2093
|
-
log?.(message);
|
|
2094
|
-
}
|
|
2095
|
-
for (const withheld of plan.withheldConstraints) {
|
|
2096
|
-
const message = `No NOT NULL on "${withheld.target}": ${withheld.reason} ${withheld.remedy}`;
|
|
2097
|
-
logger.warn(`[schema] ${message}`);
|
|
2098
|
-
log?.(message);
|
|
2099
|
-
}
|
|
2100
|
-
if (plan.actions.length === 0) {
|
|
2101
|
-
log?.("Schema is up to date; nothing to create.");
|
|
2102
|
-
return {
|
|
2103
|
-
...plan,
|
|
2104
|
-
failures
|
|
2105
|
-
};
|
|
2106
|
-
}
|
|
2107
|
-
for (const action of plan.actions) try {
|
|
2108
|
-
if (await applyAction(client, action)) log?.(`${action.kind}: ${action.target}`);
|
|
2109
|
-
else log?.(`${action.kind}: ${action.target} (already created by a peer)`);
|
|
2110
|
-
} catch (err) {
|
|
2111
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2112
|
-
if (action.kind === "add-constraint" || action.kind === "comment-column") {
|
|
2113
|
-
failures.push({
|
|
2114
|
-
kind: action.kind,
|
|
2115
|
-
target: action.target,
|
|
2116
|
-
error: message
|
|
2117
|
-
});
|
|
2118
|
-
continue;
|
|
2119
|
-
}
|
|
2120
|
-
throw new Error(`Failed to ${action.kind} ${action.target}: ${message}${vectorExtensionHint(message)}\n ${action.sql}`);
|
|
2121
|
-
}
|
|
2122
|
-
return {
|
|
2123
|
-
...plan,
|
|
2124
|
-
failures
|
|
2125
|
-
};
|
|
2126
|
-
}
|
|
2127
|
-
/** Attempts per action, including the first. Matches the server's bootstraps. */
|
|
2128
|
-
var DDL_ATTEMPTS = 4;
|
|
2129
|
-
/**
|
|
2130
|
-
* Run one planned statement, surviving a simultaneous boot.
|
|
2131
|
-
*
|
|
2132
|
-
* Every statement in a plan is written to be idempotent, and that is not the
|
|
2133
|
-
* same as being safe to run concurrently: `CREATE … IF NOT EXISTS` reads the
|
|
2134
|
-
* catalog and then writes to it as two steps, so peers starting together both
|
|
2135
|
-
* see "absent" and the loser gets a duplicate key on a *catalog* index. Measured
|
|
2136
|
-
* against Postgres 18: five instances, 8 of 10 calls lost. `CREATE TYPE` is
|
|
2137
|
-
* worse, because Postgres has no `IF NOT EXISTS` for it at all.
|
|
2138
|
-
*
|
|
2139
|
-
* What made that fatal here rather than merely noisy is the loop this sits in.
|
|
2140
|
-
* A losing statement threw, and the throw abandoned **every remaining action in
|
|
2141
|
-
* the plan** — so a replica that lost one race came up missing tables it never
|
|
2142
|
-
* attempted, and the boot log blamed the one statement that failed.
|
|
2143
|
-
*
|
|
2144
|
-
* @returns `true` if this process applied the statement, `false` if a peer had
|
|
2145
|
-
* already created the object. The distinction is only for the log; both mean
|
|
2146
|
-
* the object is now there.
|
|
2147
|
-
* @throws the original error for anything that is not a race — a syntax error, a
|
|
2148
|
-
* permission failure, a unique constraint the customer's own rows violate.
|
|
2149
|
-
*/
|
|
2150
|
-
async function applyAction(client, action) {
|
|
2151
|
-
for (let attempt = 1;; attempt++) try {
|
|
2152
|
-
await client.query(action.sql);
|
|
2153
|
-
return true;
|
|
2154
|
-
} catch (err) {
|
|
2155
|
-
if (isDuplicateObjectRace(err)) {
|
|
2156
|
-
logger.debug(`[schema] ${action.kind} ${action.target}: already created by another instance`);
|
|
2157
|
-
return false;
|
|
2158
|
-
}
|
|
2159
|
-
if (isConcurrentDdlRace(err) && attempt < DDL_ATTEMPTS) {
|
|
2160
|
-
logger.debug(`[schema] ${action.kind} ${action.target}: lost a race with another instance (attempt ${attempt}/${DDL_ATTEMPTS}) — retrying`);
|
|
2161
|
-
await new Promise((resolve) => setTimeout(resolve, 40 * attempt * (1 + Math.random())));
|
|
2162
|
-
continue;
|
|
2163
|
-
}
|
|
2164
|
-
throw err;
|
|
2165
|
-
}
|
|
2166
|
-
}
|
|
2167
|
-
//#endregion
|
|
2168
|
-
export { resolveStringColumnLength as _, generatePostgresDdl as a, generatePostgresVectorDdl as c, AUTH_USERS_COLUMNS as d, authUsersColumnSql as f, visibleColumnProjection as g, hiddenColumnsOption as h, readSchemaFactsFor as i, planCollectionPolicies as l, buildSearchColumnSpec as m, planCollectionSchemaEnsure as n, generatePostgresPoliciesDdl as o, SEARCH_UNACCENT_FN as p, readExistingSchema as r, generatePostgresSearchDdl as s, ensure_collection_tables_exports as t, resolveColumnName as u };
|
|
1310
|
+
export { searchIndexStatements as C, searchHelperFunctions as S, assertSearchIsPostgresOnly as _, generatePostgresVectorDdl as a, searchColumnStamps as b, isIdProperty as c, planRelationalColumns as d, quoteSqlLiteral as f, SEARCH_UNACCENT_FN as g, SEARCH_TEXT_FN as h, generatePostgresSearchDdl as i, planCollectionPolicies as l, SEARCH_STAMP_PREFIX as m, generatePostgresDdl as n, generate_postgres_ddl_logic_exports as o, resolveColumnName as p, generatePostgresPoliciesDdl as r, getSqlColumnType as s, defaultBelongsToOnDelete as t, planJunctionTables as u, buildSearchColumnSpec as v, visibleColumnProjection as w, searchExtensionStatements as x, hiddenColumnsOption as y };
|
|
2169
1311
|
|
|
2170
|
-
//# sourceMappingURL=
|
|
1312
|
+
//# sourceMappingURL=generate-postgres-ddl-logic-CtnpIBVv.js.map
|