@sequenceholdings/orm 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -1
- package/dist/authoring.cjs +1619 -121
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +252 -8
- package/dist/authoring.d.ts +252 -8
- package/dist/authoring.js +1607 -123
- package/dist/authoring.js.map +1 -1
- package/dist/canonical.cjs +713 -41
- package/dist/canonical.cjs.map +1 -1
- package/dist/canonical.d.cts +1520 -51
- package/dist/canonical.d.ts +1520 -51
- package/dist/canonical.js +693 -42
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +7103 -3901
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +7104 -3902
- package/dist/cli.js.map +1 -1
- package/dist/client.cjs +122 -40
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +85 -25
- package/dist/client.d.ts +85 -25
- package/dist/client.js +119 -41
- package/dist/client.js.map +1 -1
- package/dist/compiler.cjs +2464 -450
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +79 -4
- package/dist/compiler.d.ts +79 -4
- package/dist/compiler.js +2457 -451
- package/dist/compiler.js.map +1 -1
- package/dist/constants-C6EG6Wq7.d.cts +99 -0
- package/dist/constants-C6EG6Wq7.d.ts +99 -0
- package/dist/definition-DjfC7Lll.d.cts +91 -0
- package/dist/definition-DjfC7Lll.d.ts +91 -0
- package/dist/dynamic-input.cjs +2 -1
- package/dist/dynamic-input.cjs.map +1 -1
- package/dist/dynamic-input.d.cts +9 -1
- package/dist/dynamic-input.d.ts +9 -1
- package/dist/dynamic-input.js +2 -1
- package/dist/dynamic-input.js.map +1 -1
- package/dist/index.cjs +2656 -418
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +693 -116
- package/dist/index.d.ts +693 -116
- package/dist/index.js +2622 -420
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +811 -48
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +670 -27
- package/dist/ir.d.ts +670 -27
- package/dist/ir.js +802 -49
- package/dist/ir.js.map +1 -1
- package/dist/{naming-DCn94LJ4.d.ts → naming-CPfx7EfF.d.ts} +14 -1
- package/dist/{naming-DPglwzkB.d.cts → naming-CU9ThP_E.d.cts} +14 -1
- package/dist/policy-1iZbGnZZ.d.cts +24 -0
- package/dist/policy-1iZbGnZZ.d.ts +24 -0
- package/dist/predicate-v2.cjs +804 -51
- package/dist/predicate-v2.cjs.map +1 -1
- package/dist/predicate-v2.d.cts +2 -1
- package/dist/predicate-v2.d.ts +2 -1
- package/dist/predicate-v2.js +804 -51
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +1014 -75
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.d.cts +14 -3
- package/dist/sdl.d.ts +14 -3
- package/dist/sdl.js +1012 -76
- package/dist/sdl.js.map +1 -1
- package/package.json +13 -7
- package/src/codegen-gql/server-fns.cjs +2 -2
- package/templates/orm-namespace/AGENTS.md +10 -0
- package/templates/orm-namespace/CLAUDE.md +1 -82
- package/templates/orm-namespace/codegen.preset.d.cts +7 -0
- package/templates/orm-namespace/codegen.ts +2 -1
- package/templates/orm-namespace/graphql/example/queries.ts +4 -3
- package/templates/orm-namespace/package.json +2 -1
- package/templates/orm-namespace/schema/tables.ts +3 -2
- package/templates/orm-namespace/sequence.config.ts +7 -11
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { sql, eq, notExists, is, SQL } from 'drizzle-orm';
|
|
2
|
-
import { pgSchema, uniqueIndex, index, foreignKey, primaryKey, PgDialect, QueryBuilder, getViewConfig, extractUsedTable, jsonb, date, timestamp, boolean, doublePrecision, numeric, bigint, integer, text, uuid } from 'drizzle-orm/pg-core';
|
|
3
|
-
import { createHash } from 'crypto';
|
|
2
|
+
import { pgSchema, uniqueIndex, index, foreignKey, check, primaryKey, getTableConfig, PgDialect, QueryBuilder, getViewConfig, extractUsedTable, jsonb, date, timestamp, boolean, doublePrecision, numeric, bigint, integer, text, uuid } from 'drizzle-orm/pg-core';
|
|
4
3
|
import { z } from 'zod';
|
|
4
|
+
import { createHash } from 'crypto';
|
|
5
5
|
import { createJiti } from 'jiti';
|
|
6
6
|
import { realpath, stat, readdir, readFile, mkdir, writeFile } from 'fs/promises';
|
|
7
7
|
import { createRequire } from 'module';
|
|
@@ -38,7 +38,8 @@ var ColumnBuilder = class {
|
|
|
38
38
|
description: void 0,
|
|
39
39
|
renamedFrom: void 0,
|
|
40
40
|
cast: void 0,
|
|
41
|
-
deprecated: false
|
|
41
|
+
deprecated: false,
|
|
42
|
+
cell: void 0
|
|
42
43
|
};
|
|
43
44
|
}
|
|
44
45
|
/** Marks this column as (part of) the primary key. Implies NOT NULL. */
|
|
@@ -98,6 +99,11 @@ var ColumnBuilder = class {
|
|
|
98
99
|
this.state.deprecated = true;
|
|
99
100
|
return this;
|
|
100
101
|
}
|
|
102
|
+
/** @internal System cell-metadata part of logical column `of` (generated resolved views only). */
|
|
103
|
+
cellPart(of, part) {
|
|
104
|
+
this.state.cell = { of, part };
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
101
107
|
/** Whether `.primaryKey()` was called — collected into the table-level PK. */
|
|
102
108
|
get isPrimaryKey() {
|
|
103
109
|
return this.state.primaryKey;
|
|
@@ -126,6 +132,7 @@ var ColumnBuilder = class {
|
|
|
126
132
|
if (this.state.description !== void 0) column.description = this.state.description;
|
|
127
133
|
if (this.state.renamedFrom !== void 0) column.renamedFrom = this.state.renamedFrom;
|
|
128
134
|
if (this.state.cast !== void 0) column.cast = this.state.cast;
|
|
135
|
+
if (this.state.cell !== void 0) column.cell = { ...this.state.cell };
|
|
129
136
|
return column;
|
|
130
137
|
}
|
|
131
138
|
};
|
|
@@ -149,15 +156,15 @@ var t = {
|
|
|
149
156
|
|
|
150
157
|
// src/constants.ts
|
|
151
158
|
var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
|
|
159
|
+
var ORM_DATA_TOKEN_HEADER = "x-sequence-data-token";
|
|
160
|
+
var ORM_SERVICE_DATA_TOKEN_HEADER = "x-sequence-data-token-service";
|
|
152
161
|
var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
153
162
|
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
154
163
|
var MAX_IDENTIFIER_LENGTH = 63;
|
|
155
|
-
var
|
|
156
|
-
var ORM_SERVICE_DATA_TOKEN_HEADER = "x-sequence-data-token-service";
|
|
157
|
-
var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
|
|
164
|
+
var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
|
|
158
165
|
var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
159
|
-
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]
|
|
160
|
-
var SYNCED_SOURCE_PATTERN =
|
|
166
|
+
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]{0,62}\.[a-z][a-z0-9_]{0,62}$/;
|
|
167
|
+
var SYNCED_SOURCE_PATTERN = QUALIFIED_TABLE_PATTERN;
|
|
161
168
|
var ORM_SCHEMA_PREFIX = "orm_";
|
|
162
169
|
var CONSUMER_ROLE_SUFFIX = "_consumer";
|
|
163
170
|
var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
|
|
@@ -170,6 +177,27 @@ var MAX_GENERATED_SDL_BYTES = 16 * 1024 * 1024;
|
|
|
170
177
|
var ORM_REGISTRATION_MAX_BYTES = 64 * 1024 * 1024;
|
|
171
178
|
var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
|
|
172
179
|
var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
|
|
180
|
+
var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
|
|
181
|
+
var CELL_PART_SUFFIX = {
|
|
182
|
+
at: "__at",
|
|
183
|
+
by: "__by",
|
|
184
|
+
rev: "__rev",
|
|
185
|
+
source: "__src",
|
|
186
|
+
conflicts: "__lost"
|
|
187
|
+
};
|
|
188
|
+
var MAX_TRACKED_COLUMN_NAME = MAX_IDENTIFIER_LENGTH - 6;
|
|
189
|
+
var CELLS_FIELD = "cells";
|
|
190
|
+
function cellColumnName(column, part) {
|
|
191
|
+
return `${column}${CELL_PART_SUFFIX[part]}`;
|
|
192
|
+
}
|
|
193
|
+
var MATERIALIZE_ACTOR = "orm:materialize";
|
|
194
|
+
var MATERIALIZE_CLAIMS_JSON = JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
|
|
195
|
+
function materializedRefreshFunctionName(table) {
|
|
196
|
+
return `${table}__refresh`;
|
|
197
|
+
}
|
|
198
|
+
function materializeLockKey(schema, table) {
|
|
199
|
+
return `orm:materialize:${schema}.${table}`;
|
|
200
|
+
}
|
|
173
201
|
|
|
174
202
|
// src/errors.ts
|
|
175
203
|
var OrmDefinitionError = class extends Error {
|
|
@@ -237,6 +265,9 @@ function registerNamespaceConfig(value) {
|
|
|
237
265
|
// src/authoring/table.ts
|
|
238
266
|
var TABLE_BRAND = "seq-table";
|
|
239
267
|
var LINK_BRAND = "seq-link";
|
|
268
|
+
function objectRef(namespace, table) {
|
|
269
|
+
return { kind: "seq-external-object", namespace, table };
|
|
270
|
+
}
|
|
240
271
|
function toOne(target, options) {
|
|
241
272
|
return {
|
|
242
273
|
kind: LINK_BRAND,
|
|
@@ -245,6 +276,8 @@ function toOne(target, options) {
|
|
|
245
276
|
on: options.on,
|
|
246
277
|
references: options.references,
|
|
247
278
|
enforce: options.enforce ?? false,
|
|
279
|
+
onDelete: options.onDelete,
|
|
280
|
+
onUpdate: options.onUpdate,
|
|
248
281
|
description: options.description
|
|
249
282
|
};
|
|
250
283
|
}
|
|
@@ -256,6 +289,8 @@ function toMany(target, options) {
|
|
|
256
289
|
on: options.on,
|
|
257
290
|
references: options.references,
|
|
258
291
|
enforce: false,
|
|
292
|
+
onDelete: void 0,
|
|
293
|
+
onUpdate: void 0,
|
|
259
294
|
description: options.description
|
|
260
295
|
};
|
|
261
296
|
}
|
|
@@ -267,6 +302,8 @@ function toPlatformUser(options) {
|
|
|
267
302
|
on: options.on,
|
|
268
303
|
references: options.references,
|
|
269
304
|
enforce: false,
|
|
305
|
+
onDelete: void 0,
|
|
306
|
+
onUpdate: void 0,
|
|
270
307
|
description: options.description
|
|
271
308
|
};
|
|
272
309
|
}
|
|
@@ -284,45 +321,51 @@ function assertIdentifier({ value, what, file, pattern = IDENTIFIER_PATTERN }) {
|
|
|
284
321
|
);
|
|
285
322
|
}
|
|
286
323
|
}
|
|
287
|
-
function defineTable(
|
|
324
|
+
function defineTable(name2, columns, config = {}) {
|
|
288
325
|
const file = activeAuthoringContext()?.currentFile;
|
|
289
|
-
assertIdentifier({ value:
|
|
326
|
+
assertIdentifier({ value: name2, what: "table name", file });
|
|
290
327
|
const entries = Object.entries(columns);
|
|
291
328
|
if (entries.length === 0) {
|
|
292
|
-
throw new OrmDefinitionError(`table "${
|
|
329
|
+
throw new OrmDefinitionError(`table "${name2}" declares no columns`, file);
|
|
293
330
|
}
|
|
294
331
|
const seenColumnNames = /* @__PURE__ */ new Map();
|
|
295
332
|
for (const [propertyName, builder] of entries) {
|
|
296
333
|
if (!isColumnBuilder(builder)) {
|
|
297
334
|
throw new OrmDefinitionError(
|
|
298
|
-
`table "${
|
|
335
|
+
`table "${name2}": column "${propertyName}" is not a column builder \u2014 use the t.* factories (t.uuid(), t.text(), \u2026)`,
|
|
299
336
|
file
|
|
300
337
|
);
|
|
301
338
|
}
|
|
302
339
|
const columnName = builder.columnNameFor(propertyName);
|
|
303
|
-
assertIdentifier({ value: columnName, what: `table "${
|
|
340
|
+
assertIdentifier({ value: columnName, what: `table "${name2}": column "${propertyName}" column name`, file, pattern: COLUMN_IDENTIFIER_PATTERN });
|
|
341
|
+
if (config.materializes === void 0 && (propertyName === CELLS_FIELD || Object.values(CELL_PART_SUFFIX).some((suffix) => columnName.endsWith(suffix) || propertyName.endsWith(suffix)))) {
|
|
342
|
+
throw new OrmDefinitionError(
|
|
343
|
+
`table "${name2}": "${propertyName}" collides with the reserved cell-metadata names ('${CELLS_FIELD}', or a ${Object.values(CELL_PART_SUFFIX).join("/")} suffix)`,
|
|
344
|
+
file
|
|
345
|
+
);
|
|
346
|
+
}
|
|
304
347
|
const existing = seenColumnNames.get(columnName);
|
|
305
348
|
if (existing !== void 0) {
|
|
306
349
|
throw new OrmDefinitionError(
|
|
307
|
-
`table "${
|
|
350
|
+
`table "${name2}": columns "${existing}" and "${propertyName}" both map to Postgres column "${columnName}"`,
|
|
308
351
|
file
|
|
309
352
|
);
|
|
310
353
|
}
|
|
311
354
|
seenColumnNames.set(columnName, propertyName);
|
|
312
355
|
}
|
|
313
|
-
const table = { kind: TABLE_BRAND, tableName:
|
|
356
|
+
const table = { kind: TABLE_BRAND, tableName: name2, columns, config, source: void 0, file };
|
|
314
357
|
registerTable(table);
|
|
315
358
|
return table;
|
|
316
359
|
}
|
|
317
|
-
function defineSyncedTable(
|
|
360
|
+
function defineSyncedTable(name2, columnsOrConfig, maybeConfig) {
|
|
318
361
|
const file = activeAuthoringContext()?.currentFile;
|
|
319
|
-
assertIdentifier({ value:
|
|
362
|
+
assertIdentifier({ value: name2, what: "table name", file });
|
|
320
363
|
const isTyped = maybeConfig !== void 0;
|
|
321
364
|
const columns = isTyped ? columnsOrConfig : {};
|
|
322
365
|
const config = isTyped ? maybeConfig : columnsOrConfig;
|
|
323
366
|
if (!SYNCED_SOURCE_PATTERN.test(config.source)) {
|
|
324
367
|
throw new OrmDefinitionError(
|
|
325
|
-
`synced table "${
|
|
368
|
+
`synced table "${name2}": source "${config.source}" must be a qualified lowercase schema.table identifier`,
|
|
326
369
|
file
|
|
327
370
|
);
|
|
328
371
|
}
|
|
@@ -332,21 +375,21 @@ function defineSyncedTable(name, columnsOrConfig, maybeConfig) {
|
|
|
332
375
|
for (const [propertyName, builder] of entries) {
|
|
333
376
|
if (!isColumnBuilder(builder)) {
|
|
334
377
|
throw new OrmDefinitionError(
|
|
335
|
-
`synced table "${
|
|
378
|
+
`synced table "${name2}": column "${propertyName}" is not a column builder \u2014 use the t.* factories (t.uuid(), t.text(), \u2026)`,
|
|
336
379
|
file
|
|
337
380
|
);
|
|
338
381
|
}
|
|
339
382
|
const columnName = builder.columnNameFor(propertyName);
|
|
340
383
|
assertIdentifier({
|
|
341
384
|
value: columnName,
|
|
342
|
-
what: `synced table "${
|
|
385
|
+
what: `synced table "${name2}": column "${propertyName}" column name`,
|
|
343
386
|
file,
|
|
344
387
|
pattern: COLUMN_IDENTIFIER_PATTERN
|
|
345
388
|
});
|
|
346
389
|
const existing = seenColumnNames.get(columnName);
|
|
347
390
|
if (existing !== void 0) {
|
|
348
391
|
throw new OrmDefinitionError(
|
|
349
|
-
`synced table "${
|
|
392
|
+
`synced table "${name2}": columns "${existing}" and "${propertyName}" both map to Postgres column "${columnName}"`,
|
|
350
393
|
file
|
|
351
394
|
);
|
|
352
395
|
}
|
|
@@ -357,13 +400,13 @@ function defineSyncedTable(name, columnsOrConfig, maybeConfig) {
|
|
|
357
400
|
const hasConfigPk = config.primaryKey !== void 0 && config.primaryKey.length > 0;
|
|
358
401
|
if (columnLevelPk.length === 0 && !hasConfigPk) {
|
|
359
402
|
throw new OrmDefinitionError(
|
|
360
|
-
`synced table "${
|
|
403
|
+
`synced table "${name2}" declares columns but no primary key \u2014 use .primaryKey() or config.primaryKey`,
|
|
361
404
|
file
|
|
362
405
|
);
|
|
363
406
|
}
|
|
364
407
|
if (columnLevelPk.length > 0 && hasConfigPk) {
|
|
365
408
|
throw new OrmDefinitionError(
|
|
366
|
-
`synced table "${
|
|
409
|
+
`synced table "${name2}": declare the primary key either with .primaryKey() on columns or with config.primaryKey \u2014 not both`,
|
|
367
410
|
file
|
|
368
411
|
);
|
|
369
412
|
}
|
|
@@ -375,7 +418,7 @@ function defineSyncedTable(name, columnsOrConfig, maybeConfig) {
|
|
|
375
418
|
if (config.policies !== void 0) tableConfig.policies = { read: config.policies.read };
|
|
376
419
|
const table = {
|
|
377
420
|
kind: TABLE_BRAND,
|
|
378
|
-
tableName:
|
|
421
|
+
tableName: name2,
|
|
379
422
|
columns,
|
|
380
423
|
config: tableConfig,
|
|
381
424
|
source: config.source,
|
|
@@ -384,26 +427,26 @@ function defineSyncedTable(name, columnsOrConfig, maybeConfig) {
|
|
|
384
427
|
registerTable(table);
|
|
385
428
|
return table;
|
|
386
429
|
}
|
|
387
|
-
function defineEnum(
|
|
430
|
+
function defineEnum(name2, values) {
|
|
388
431
|
const file = activeAuthoringContext()?.currentFile;
|
|
389
|
-
assertIdentifier({ value:
|
|
432
|
+
assertIdentifier({ value: name2, what: "enum name", file });
|
|
390
433
|
if (values.length === 0 || values.length > 100) {
|
|
391
|
-
throw new OrmDefinitionError(`enum "${
|
|
434
|
+
throw new OrmDefinitionError(`enum "${name2}" must declare between 1 and 100 values`, file);
|
|
392
435
|
}
|
|
393
436
|
const seen = /* @__PURE__ */ new Set();
|
|
394
437
|
for (const value of values) {
|
|
395
438
|
if (value.length === 0 || value.length > MAX_IDENTIFIER_LENGTH) {
|
|
396
439
|
throw new OrmDefinitionError(
|
|
397
|
-
`enum "${
|
|
440
|
+
`enum "${name2}": value ${JSON.stringify(value)} must be 1-${MAX_IDENTIFIER_LENGTH} characters`,
|
|
398
441
|
file
|
|
399
442
|
);
|
|
400
443
|
}
|
|
401
444
|
if (seen.has(value)) {
|
|
402
|
-
throw new OrmDefinitionError(`enum "${
|
|
445
|
+
throw new OrmDefinitionError(`enum "${name2}" declares duplicate value "${value}"`, file);
|
|
403
446
|
}
|
|
404
447
|
seen.add(value);
|
|
405
448
|
}
|
|
406
|
-
const ref = { kind: ENUM_BRAND, name, values: [...values] };
|
|
449
|
+
const ref = { kind: ENUM_BRAND, name: name2, values: [...values] };
|
|
407
450
|
registerEnum(ref);
|
|
408
451
|
return ref;
|
|
409
452
|
}
|
|
@@ -411,52 +454,52 @@ var VIEW_BRAND = "seq-view";
|
|
|
411
454
|
function isSeqView(value) {
|
|
412
455
|
return typeof value === "object" && value !== null && value.kind === VIEW_BRAND;
|
|
413
456
|
}
|
|
414
|
-
function defineView(
|
|
457
|
+
function defineView(name2, columns, config) {
|
|
415
458
|
const file = activeAuthoringContext()?.currentFile;
|
|
416
|
-
if (!IDENTIFIER_PATTERN.test(
|
|
459
|
+
if (!IDENTIFIER_PATTERN.test(name2) || name2.length > MAX_IDENTIFIER_LENGTH) {
|
|
417
460
|
throw new OrmDefinitionError(
|
|
418
|
-
`view name "${
|
|
461
|
+
`view name "${name2}" must match ${IDENTIFIER_PATTERN} and be at most ${MAX_IDENTIFIER_LENGTH} characters`,
|
|
419
462
|
file
|
|
420
463
|
);
|
|
421
464
|
}
|
|
422
465
|
const entries = Object.entries(columns);
|
|
423
|
-
if (entries.length === 0) throw new OrmDefinitionError(`view "${
|
|
466
|
+
if (entries.length === 0) throw new OrmDefinitionError(`view "${name2}" declares no columns`, file);
|
|
424
467
|
const seenColumnNames = /* @__PURE__ */ new Map();
|
|
425
468
|
for (const [propertyName, builder] of entries) {
|
|
426
469
|
if (!isColumnBuilder(builder)) {
|
|
427
470
|
throw new OrmDefinitionError(
|
|
428
|
-
`view "${
|
|
471
|
+
`view "${name2}": column "${propertyName}" is not a column builder \u2014 use the t.* factories`,
|
|
429
472
|
file
|
|
430
473
|
);
|
|
431
474
|
}
|
|
432
475
|
const columnName = builder.columnNameFor(propertyName);
|
|
433
476
|
if (!COLUMN_IDENTIFIER_PATTERN.test(columnName) || columnName.length > MAX_IDENTIFIER_LENGTH) {
|
|
434
|
-
throw new OrmDefinitionError(`view "${
|
|
477
|
+
throw new OrmDefinitionError(`view "${name2}": column "${propertyName}" has invalid column name "${columnName}"`, file);
|
|
435
478
|
}
|
|
436
479
|
const existing = seenColumnNames.get(columnName);
|
|
437
480
|
if (existing !== void 0) {
|
|
438
481
|
throw new OrmDefinitionError(
|
|
439
|
-
`view "${
|
|
482
|
+
`view "${name2}": columns "${existing}" and "${propertyName}" both map to Postgres column "${columnName}"`,
|
|
440
483
|
file
|
|
441
484
|
);
|
|
442
485
|
}
|
|
443
486
|
seenColumnNames.set(columnName, propertyName);
|
|
444
487
|
}
|
|
445
|
-
const view = { kind: VIEW_BRAND, viewName:
|
|
488
|
+
const view = { kind: VIEW_BRAND, viewName: name2, columns, config, file };
|
|
446
489
|
registerView(view);
|
|
447
490
|
return view;
|
|
448
491
|
}
|
|
449
492
|
var QUERY_COLUMN_FACTORIES = {
|
|
450
|
-
uuid: (
|
|
451
|
-
text: (
|
|
452
|
-
integer: (
|
|
453
|
-
bigint: (
|
|
454
|
-
numeric: (
|
|
455
|
-
double: (
|
|
456
|
-
boolean: (
|
|
457
|
-
timestamptz: (
|
|
458
|
-
date: (
|
|
459
|
-
jsonb: (
|
|
493
|
+
uuid: (name2) => uuid(name2),
|
|
494
|
+
text: (name2) => text(name2),
|
|
495
|
+
integer: (name2) => integer(name2),
|
|
496
|
+
bigint: (name2) => bigint(name2, { mode: "bigint" }),
|
|
497
|
+
numeric: (name2) => numeric(name2),
|
|
498
|
+
double: (name2) => doublePrecision(name2),
|
|
499
|
+
boolean: (name2) => boolean(name2),
|
|
500
|
+
timestamptz: (name2) => timestamp(name2, { withTimezone: true }),
|
|
501
|
+
date: (name2) => date(name2),
|
|
502
|
+
jsonb: (name2) => jsonb(name2)
|
|
460
503
|
};
|
|
461
504
|
function compileViewQueries(input) {
|
|
462
505
|
const schemaName = `orm_${input.namespace}`;
|
|
@@ -512,9 +555,9 @@ function compileViewQueries(input) {
|
|
|
512
555
|
}
|
|
513
556
|
const handle = handles.get(resolved);
|
|
514
557
|
if (!handle) {
|
|
515
|
-
const
|
|
558
|
+
const name2 = isSeqTable(resolved) ? resolved.tableName : resolved.viewName;
|
|
516
559
|
throw new OrmDefinitionError(
|
|
517
|
-
`view "${authored.viewName}" references relation "${
|
|
560
|
+
`view "${authored.viewName}" references relation "${name2}" which is not part of this namespace`,
|
|
518
561
|
authored.file
|
|
519
562
|
);
|
|
520
563
|
}
|
|
@@ -548,11 +591,11 @@ function compileViewQueries(input) {
|
|
|
548
591
|
);
|
|
549
592
|
}
|
|
550
593
|
if (!config.query) throw new OrmDefinitionError(`view "${authored.viewName}" produced no query`, authored.file);
|
|
551
|
-
const tableDependencies = extractUsedTable(config.query).map((
|
|
552
|
-
const logical = physicalToLogical.get(
|
|
594
|
+
const tableDependencies = extractUsedTable(config.query).map((physical2) => {
|
|
595
|
+
const logical = physicalToLogical.get(physical2);
|
|
553
596
|
if (!logical) {
|
|
554
597
|
throw new OrmDefinitionError(
|
|
555
|
-
`view "${authored.viewName}" query references unmanaged relation "${
|
|
598
|
+
`view "${authored.viewName}" query references unmanaged relation "${physical2}" \u2014 use relation(...) with a namespace table or view`,
|
|
556
599
|
authored.file
|
|
557
600
|
);
|
|
558
601
|
}
|
|
@@ -705,8 +748,8 @@ function overlayWinsUnion(input) {
|
|
|
705
748
|
// src/authoring/cow-mutations.ts
|
|
706
749
|
function overrideRowMutation(input) {
|
|
707
750
|
const { overlay, synced, pk } = input;
|
|
708
|
-
const
|
|
709
|
-
assertMutationName(
|
|
751
|
+
const name2 = input.name ?? `override_${overlay.tableName}`;
|
|
752
|
+
assertMutationName(name2, "overrideRowMutation");
|
|
710
753
|
assertOverlayPairPrimaryKey({ overlay, synced, pk, helper: "overrideRowMutation" });
|
|
711
754
|
const sharedProperties = sharedNonPkProperties({ overlay, synced, pk });
|
|
712
755
|
const setProperties = resolveSetProperties({
|
|
@@ -725,7 +768,7 @@ function overrideRowMutation(input) {
|
|
|
725
768
|
});
|
|
726
769
|
return {
|
|
727
770
|
kind: "override",
|
|
728
|
-
name,
|
|
771
|
+
name: name2,
|
|
729
772
|
synced: synced.tableName,
|
|
730
773
|
pk,
|
|
731
774
|
copy: [pk, ...sharedProperties],
|
|
@@ -734,23 +777,23 @@ function overrideRowMutation(input) {
|
|
|
734
777
|
}
|
|
735
778
|
function revertOverlayMutation(input) {
|
|
736
779
|
const { overlay, synced, pk } = input;
|
|
737
|
-
const
|
|
738
|
-
assertMutationName(
|
|
780
|
+
const name2 = input.name ?? `revert_${overlay.tableName}`;
|
|
781
|
+
assertMutationName(name2, "revertOverlayMutation");
|
|
739
782
|
assertOverlayPairPrimaryKey({ overlay, synced, pk, helper: "revertOverlayMutation" });
|
|
740
783
|
assertMatchingColumns({ overlay, synced, properties: [pk], helper: "revertOverlayMutation" });
|
|
741
784
|
return {
|
|
742
785
|
kind: "revert",
|
|
743
|
-
name,
|
|
786
|
+
name: name2,
|
|
744
787
|
synced: synced.tableName,
|
|
745
788
|
pk,
|
|
746
789
|
copy: [],
|
|
747
790
|
set: []
|
|
748
791
|
};
|
|
749
792
|
}
|
|
750
|
-
function assertMutationName(
|
|
751
|
-
if (!IDENTIFIER_PATTERN.test(
|
|
793
|
+
function assertMutationName(name2, helper) {
|
|
794
|
+
if (!IDENTIFIER_PATTERN.test(name2) || name2.length > MAX_IDENTIFIER_LENGTH) {
|
|
752
795
|
throw new OrmDefinitionError(
|
|
753
|
-
`${helper}: mutation name "${
|
|
796
|
+
`${helper}: mutation name "${name2}" must match ${IDENTIFIER_PATTERN} and be at most ${MAX_IDENTIFIER_LENGTH} characters`
|
|
754
797
|
);
|
|
755
798
|
}
|
|
756
799
|
}
|
|
@@ -876,9 +919,495 @@ function scalarColumnType(table, property, helper) {
|
|
|
876
919
|
}
|
|
877
920
|
return type;
|
|
878
921
|
}
|
|
922
|
+
var objectRef2 = z.string().max(255).regex(/^[a-z][a-z0-9_]{0,40}\/[A-Za-z_][A-Za-z0-9_]*$/, "Expected an explicit namespace/object reference (no wildcards)");
|
|
923
|
+
var functionRef = z.string().max(255).regex(/^[a-z][a-z0-9-]*$/, "Expected a function slug");
|
|
924
|
+
var refs = (schema) => z.array(schema).max(256).refine(
|
|
925
|
+
(values) => new Set(values).size === values.length,
|
|
926
|
+
"Duplicate dependency reference"
|
|
927
|
+
).default([]);
|
|
928
|
+
var ontologyContractSchema = z.strictObject({
|
|
929
|
+
version: z.literal(1),
|
|
930
|
+
read: refs(objectRef2),
|
|
931
|
+
write: refs(objectRef2),
|
|
932
|
+
produce: refs(objectRef2),
|
|
933
|
+
invoke: refs(functionRef)
|
|
934
|
+
}).refine((contract) => JSON.stringify(contract).length <= 4096, "Ontology contract exceeds the 4 KiB signed-context budget");
|
|
935
|
+
function readOntologyContract(manifest) {
|
|
936
|
+
if (!manifest || typeof manifest !== "object" || !("ontology" in manifest) || manifest.ontology === void 0) return void 0;
|
|
937
|
+
return ontologyContractSchema.parse(manifest.ontology);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// src/compiler/sql.ts
|
|
941
|
+
var CLAIMS_SETTING = "request.jwt.claims";
|
|
942
|
+
var CAPABILITIES_CLAIM_KEY = "capabilities";
|
|
943
|
+
var ROLES_CLAIM_KEY = "roles";
|
|
944
|
+
var quoteIdentifier = (name2) => `"${name2.replaceAll('"', '""')}"`;
|
|
945
|
+
var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
946
|
+
var CLAIMS_SOURCE = `nullif(current_setting('${CLAIMS_SETTING}', true), '')::jsonb`;
|
|
947
|
+
var claimExpression = (key) => `(${CLAIMS_SOURCE} ->> ${quoteLiteral(key)})`;
|
|
948
|
+
var scopedClaimsSource = (namespace) => namespace ? `(CASE WHEN ${CLAIMS_SOURCE} ? 'namespaces' THEN ${CLAIMS_SOURCE} -> 'namespaces' -> ${quoteLiteral(namespace)} ELSE ${CLAIMS_SOURCE} END)` : CLAIMS_SOURCE;
|
|
949
|
+
var capabilityExpression = (key, namespace) => `coalesce(${scopedClaimsSource(namespace)} -> '${CAPABILITIES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
|
|
950
|
+
var roleExpression = (key, namespace) => `coalesce(${scopedClaimsSource(namespace)} -> '${ROLES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
|
|
951
|
+
|
|
952
|
+
// src/checks/compile.ts
|
|
953
|
+
function tokenize(source) {
|
|
954
|
+
const tokens = [];
|
|
955
|
+
for (let position = 0; position < source.length; ) {
|
|
956
|
+
const char = source[position];
|
|
957
|
+
if (/\s/.test(char)) {
|
|
958
|
+
position++;
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
if (char === "(" || char === ")" || char === ",") {
|
|
962
|
+
tokens.push({ kind: char === "(" ? "lparen" : char === ")" ? "rparen" : "comma", value: char, position });
|
|
963
|
+
position++;
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
if (char === "'") {
|
|
967
|
+
const start = position++;
|
|
968
|
+
let value = "";
|
|
969
|
+
let closed = false;
|
|
970
|
+
while (position < source.length) {
|
|
971
|
+
if (source[position] !== "'") {
|
|
972
|
+
value += source[position++];
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
if (source[position + 1] === "'") {
|
|
976
|
+
value += "'";
|
|
977
|
+
position += 2;
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
position++;
|
|
981
|
+
closed = true;
|
|
982
|
+
break;
|
|
983
|
+
}
|
|
984
|
+
if (!closed) throw new Error(`unterminated string at position ${start}`);
|
|
985
|
+
if (/[\x00-\x1f\x7f]/.test(value)) throw new Error(`control character in string at position ${start}`);
|
|
986
|
+
tokens.push({ kind: "string", value, position: start });
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
const operator = /^(<=|>=|<>|!=|!~|=|<|>|~)/.exec(source.slice(position));
|
|
990
|
+
if (operator) {
|
|
991
|
+
tokens.push({ kind: "operator", value: operator[0] === "<>" ? "!=" : operator[0], position });
|
|
992
|
+
position += operator[0].length;
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
const number = /^-?\d+(?:\.\d+)?/.exec(source.slice(position));
|
|
996
|
+
if (number) {
|
|
997
|
+
tokens.push({ kind: "number", value: number[0], position });
|
|
998
|
+
position += number[0].length;
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
const identifier3 = /^[a-zA-Z_][a-zA-Z_0-9]*/.exec(source.slice(position));
|
|
1002
|
+
if (identifier3) {
|
|
1003
|
+
tokens.push({ kind: "identifier", value: identifier3[0], position });
|
|
1004
|
+
position += identifier3[0].length;
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
throw new Error(`unexpected character '${char}' at position ${position}`);
|
|
1008
|
+
}
|
|
1009
|
+
tokens.push({ kind: "end", value: "", position: source.length });
|
|
1010
|
+
if (tokens.length > 256) throw new Error("check expression exceeds 256 tokens");
|
|
1011
|
+
return tokens;
|
|
1012
|
+
}
|
|
1013
|
+
var Parser = class {
|
|
1014
|
+
constructor(tokens, columns) {
|
|
1015
|
+
this.tokens = tokens;
|
|
1016
|
+
this.columns = /* @__PURE__ */ new Map();
|
|
1017
|
+
for (const column of columns) {
|
|
1018
|
+
for (const alias of [column.name, column.columnName]) {
|
|
1019
|
+
const previous = this.columns.get(alias);
|
|
1020
|
+
if (previous && previous.columnName !== column.columnName) {
|
|
1021
|
+
throw new Error(`ambiguous check column '${alias}'`);
|
|
1022
|
+
}
|
|
1023
|
+
this.columns.set(alias, column);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
index = 0;
|
|
1028
|
+
depth = 0;
|
|
1029
|
+
columns;
|
|
1030
|
+
parse() {
|
|
1031
|
+
const expression = this.parseOr();
|
|
1032
|
+
this.expect("end", "end of expression");
|
|
1033
|
+
return expression;
|
|
1034
|
+
}
|
|
1035
|
+
parseOr() {
|
|
1036
|
+
let left = this.parseAnd();
|
|
1037
|
+
while (this.matchKeyword("or")) left = `(${left} OR ${this.parseAnd()})`;
|
|
1038
|
+
return left;
|
|
1039
|
+
}
|
|
1040
|
+
parseAnd() {
|
|
1041
|
+
let left = this.parseNot();
|
|
1042
|
+
while (this.matchKeyword("and")) left = `(${left} AND ${this.parseNot()})`;
|
|
1043
|
+
return left;
|
|
1044
|
+
}
|
|
1045
|
+
parseNot() {
|
|
1046
|
+
if (this.matchKeyword("not")) return `(NOT ${this.parseNot()})`;
|
|
1047
|
+
return this.parsePrimary();
|
|
1048
|
+
}
|
|
1049
|
+
parsePrimary() {
|
|
1050
|
+
if (this.peek().kind === "lparen") {
|
|
1051
|
+
if (++this.depth > 32) throw new Error("check expression nesting exceeds 32 levels");
|
|
1052
|
+
this.advance();
|
|
1053
|
+
const expression = this.parseOr();
|
|
1054
|
+
this.expect("rparen", "')'");
|
|
1055
|
+
this.depth--;
|
|
1056
|
+
return `(${expression})`;
|
|
1057
|
+
}
|
|
1058
|
+
return this.parseComparison();
|
|
1059
|
+
}
|
|
1060
|
+
parseComparison() {
|
|
1061
|
+
const left = this.parseValue();
|
|
1062
|
+
if (this.matchKeyword("is")) {
|
|
1063
|
+
const negate = this.matchKeyword("not");
|
|
1064
|
+
this.expectKeyword("null");
|
|
1065
|
+
return `${left.sql} IS ${negate ? "NOT " : ""}NULL`;
|
|
1066
|
+
}
|
|
1067
|
+
if (this.matchKeyword("between")) {
|
|
1068
|
+
const lower = this.parseValue();
|
|
1069
|
+
this.expectKeyword("and");
|
|
1070
|
+
const upper = this.parseValue();
|
|
1071
|
+
return `${left.sql} BETWEEN ${lower.sql} AND ${upper.sql}`;
|
|
1072
|
+
}
|
|
1073
|
+
if (this.matchKeyword("in")) {
|
|
1074
|
+
this.expect("lparen", "'(' after IN");
|
|
1075
|
+
const values = [this.parseValue()];
|
|
1076
|
+
while (this.peek().kind === "comma") {
|
|
1077
|
+
this.advance();
|
|
1078
|
+
values.push(this.parseValue());
|
|
1079
|
+
}
|
|
1080
|
+
this.expect("rparen", "')' after IN values");
|
|
1081
|
+
if (values.some((value) => !value.literal)) throw new Error("IN values must be literals");
|
|
1082
|
+
return `${left.sql} IN (${values.map((value) => value.sql).join(", ")})`;
|
|
1083
|
+
}
|
|
1084
|
+
const operator = this.expect("operator", "a comparison operator");
|
|
1085
|
+
const right = this.parseValue();
|
|
1086
|
+
if (operator.value === "~" || operator.value === "!~") {
|
|
1087
|
+
if (left.column && left.column.type !== "text") throw new Error("regex checks require a text column");
|
|
1088
|
+
if (!right.literal) throw new Error("regex pattern must be a literal");
|
|
1089
|
+
}
|
|
1090
|
+
return `${left.sql} ${operator.value} ${right.sql}`;
|
|
1091
|
+
}
|
|
1092
|
+
parseValue() {
|
|
1093
|
+
const token = this.advance();
|
|
1094
|
+
if (token.kind === "string") return { sql: quoteLiteral(token.value), literal: true };
|
|
1095
|
+
if (token.kind === "number") {
|
|
1096
|
+
const number = Number(token.value);
|
|
1097
|
+
if (!Number.isFinite(number) || Number.isInteger(number) && !Number.isSafeInteger(number)) {
|
|
1098
|
+
throw new Error(`unsafe numeric literal at position ${token.position}`);
|
|
1099
|
+
}
|
|
1100
|
+
return { sql: token.value, literal: true };
|
|
1101
|
+
}
|
|
1102
|
+
if (token.kind !== "identifier") throw new Error(`expected a column or literal at position ${token.position}`);
|
|
1103
|
+
const keyword = token.value.toLowerCase();
|
|
1104
|
+
if (keyword === "true" || keyword === "false" || keyword === "null") return { sql: keyword.toUpperCase(), literal: true };
|
|
1105
|
+
if (keyword === "btrim") {
|
|
1106
|
+
this.expect("lparen", "'(' after btrim");
|
|
1107
|
+
const column2 = this.parseColumn();
|
|
1108
|
+
this.expect("rparen", "')' after btrim column");
|
|
1109
|
+
if (column2.type !== "text") throw new Error("btrim requires a text column");
|
|
1110
|
+
return { sql: `btrim(${quoteIdentifier(column2.columnName)})`, column: column2 };
|
|
1111
|
+
}
|
|
1112
|
+
const column = this.columns.get(token.value);
|
|
1113
|
+
if (!column) throw new Error(`unknown check column '${token.value}'`);
|
|
1114
|
+
return { sql: quoteIdentifier(column.columnName), column };
|
|
1115
|
+
}
|
|
1116
|
+
parseColumn() {
|
|
1117
|
+
const token = this.expect("identifier", "a column name");
|
|
1118
|
+
const column = this.columns.get(token.value);
|
|
1119
|
+
if (!column) throw new Error(`unknown check column '${token.value}'`);
|
|
1120
|
+
return column;
|
|
1121
|
+
}
|
|
1122
|
+
peek() {
|
|
1123
|
+
return this.tokens[this.index];
|
|
1124
|
+
}
|
|
1125
|
+
advance() {
|
|
1126
|
+
const token = this.peek();
|
|
1127
|
+
if (token.kind !== "end") this.index++;
|
|
1128
|
+
return token;
|
|
1129
|
+
}
|
|
1130
|
+
matchKeyword(keyword) {
|
|
1131
|
+
if (this.peek().kind !== "identifier" || this.peek().value.toLowerCase() !== keyword) return false;
|
|
1132
|
+
this.advance();
|
|
1133
|
+
return true;
|
|
1134
|
+
}
|
|
1135
|
+
expectKeyword(keyword) {
|
|
1136
|
+
if (!this.matchKeyword(keyword)) throw new Error(`expected '${keyword}' at position ${this.peek().position}`);
|
|
1137
|
+
}
|
|
1138
|
+
expect(kind, description) {
|
|
1139
|
+
const token = this.peek();
|
|
1140
|
+
if (token.kind !== kind) throw new Error(`expected ${description} at position ${token.position}`);
|
|
1141
|
+
return this.advance();
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
function compileCheckExpression(source, columns) {
|
|
1145
|
+
return new Parser(tokenize(source), columns).parse();
|
|
1146
|
+
}
|
|
1147
|
+
var policyVariantSchema = z.object({
|
|
1148
|
+
/** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
|
|
1149
|
+
when: z.string().min(1).max(1e3),
|
|
1150
|
+
/** Exactly one of: allow-all bypass, or a row predicate. */
|
|
1151
|
+
allow: z.literal(true).optional(),
|
|
1152
|
+
policy: z.string().min(1).max(2e3).optional()
|
|
1153
|
+
}).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
|
|
1154
|
+
message: "A variant declares exactly one of allow: true or policy"
|
|
1155
|
+
});
|
|
1156
|
+
var policyExpressionSchema = z.union([
|
|
1157
|
+
z.string().min(1).max(2e3),
|
|
1158
|
+
z.object({
|
|
1159
|
+
default: z.string().min(1).max(2e3),
|
|
1160
|
+
variants: z.array(policyVariantSchema).max(10).default([])
|
|
1161
|
+
})
|
|
1162
|
+
]);
|
|
1163
|
+
function parseDurationMs(input) {
|
|
1164
|
+
if (typeof input === "number") return Number.isInteger(input) && input > 0 ? input : void 0;
|
|
1165
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s|m)$/.exec(input);
|
|
1166
|
+
if (!match) return void 0;
|
|
1167
|
+
const factor = match[2] === "ms" ? 1 : match[2] === "s" ? 1e3 : 6e4;
|
|
1168
|
+
const value = Math.round(Number(match[1]) * factor);
|
|
1169
|
+
return value > 0 ? value : void 0;
|
|
1170
|
+
}
|
|
1171
|
+
var identifier = z.string().min(1).max(63).regex(/^[a-z][a-z0-9_]*$/);
|
|
1172
|
+
var durationMs = z.number().int().positive().max(24 * 60 * 60 * 1e3);
|
|
1173
|
+
var RESOLVED_VIEW_SYSTEM_COLUMNS = {
|
|
1174
|
+
writebackStatus: { name: "writebackStatus", columnName: "_writeback_status", type: "text" },
|
|
1175
|
+
writebackError: { name: "writebackError", columnName: "_writeback_error", type: "text" }
|
|
1176
|
+
};
|
|
1177
|
+
var RESOLVED_SOURCE_SYSTEM_COLUMNS = {
|
|
1178
|
+
tombstoned: { name: "sysTombstoned", columnName: "_tombstoned" },
|
|
1179
|
+
fetchedAt: { name: "sysFetchedAt", columnName: "_fetched_at" },
|
|
1180
|
+
writebackStatus: { name: "sysWritebackStatus", columnName: "_writeback_status" },
|
|
1181
|
+
writebackError: { name: "sysWritebackError", columnName: "_writeback_error" },
|
|
1182
|
+
writebackAt: { name: "sysWritebackAt", columnName: "_writeback_at" }
|
|
1183
|
+
};
|
|
1184
|
+
var RESOLVED_VIEW_CELL_PARTS = ["source", "at", "by", "conflicts"];
|
|
1185
|
+
var RESERVED_RESOLVED_COLUMN_NAMES = /* @__PURE__ */ new Set([
|
|
1186
|
+
...Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName]),
|
|
1187
|
+
...Object.values(RESOLVED_SOURCE_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName])
|
|
1188
|
+
]);
|
|
1189
|
+
var COMPARATORS = ["latest", "highest", "lowest", "priority", "nonNullFirst", "custom"];
|
|
1190
|
+
var canonicalFunctionRefSchema = z.object({
|
|
1191
|
+
/** `managed` = a Managed Function id/slug; `native` = an in-process Atlas function (reserved). */
|
|
1192
|
+
kind: z.enum(["managed", "native"]),
|
|
1193
|
+
id: z.string().min(1).max(200)
|
|
1194
|
+
}).strict();
|
|
1195
|
+
var canonicalFetchShapeSchema = z.union([
|
|
1196
|
+
z.object({ shape: z.literal("single") }).strict(),
|
|
1197
|
+
z.object({ shape: z.literal("batch"), maxKeys: z.number().int().min(2).max(500) }).strict()
|
|
1198
|
+
]);
|
|
1199
|
+
var canonicalResolvedSourceSchema = z.object({
|
|
1200
|
+
name: identifier,
|
|
1201
|
+
kind: z.enum(["manual", "etl", "prefetch"]),
|
|
1202
|
+
/** Backing relation in this namespace: an internal table (manual/prefetch) or a synced table (etl). */
|
|
1203
|
+
table: identifier,
|
|
1204
|
+
/** Resolved columns (logical names) this source supplies — the primary key is implicit. */
|
|
1205
|
+
columns: z.array(z.string().min(1).max(63)).min(1).max(500),
|
|
1206
|
+
/** etl: logical timestamptz/date column on the synced table used as every cell's updated_at. */
|
|
1207
|
+
updatedAt: z.string().min(1).max(63).optional(),
|
|
1208
|
+
/** prefetch: resolved column holding the source system's key (may be the primary key). */
|
|
1209
|
+
key: z.string().min(1).max(63).optional(),
|
|
1210
|
+
fetch: canonicalFetchShapeSchema.optional(),
|
|
1211
|
+
function: canonicalFunctionRefSchema.optional(),
|
|
1212
|
+
prefetchDefaults: z.object({
|
|
1213
|
+
maxStalenessMs: durationMs,
|
|
1214
|
+
timeoutMs: durationMs,
|
|
1215
|
+
onError: z.enum(["fallback", "fail"])
|
|
1216
|
+
}).strict().optional(),
|
|
1217
|
+
writeback: z.object({
|
|
1218
|
+
columns: z.array(z.string().min(1).max(63)).min(1).max(500),
|
|
1219
|
+
defaultMode: z.enum(["sync", "async"]),
|
|
1220
|
+
timeoutMs: durationMs,
|
|
1221
|
+
/** Remove confirmed cells from the manual source so a stale edit cannot shadow the source. */
|
|
1222
|
+
clearManualOnConfirm: z.boolean()
|
|
1223
|
+
}).strict().optional()
|
|
1224
|
+
}).strict();
|
|
1225
|
+
var canonicalResolveRuleSchema = z.object({
|
|
1226
|
+
column: z.string().min(1).max(63),
|
|
1227
|
+
comparator: z.enum(COMPARATORS),
|
|
1228
|
+
/** Source names, in tie-break order. */
|
|
1229
|
+
candidates: z.array(identifier).min(1).max(16),
|
|
1230
|
+
/** custom only: SQL ORDER BY key over value / updated_at / source / actor / ord. */
|
|
1231
|
+
orderBy: z.string().min(1).max(2e3).optional()
|
|
1232
|
+
}).strict();
|
|
1233
|
+
var canonicalResolvedSchema = z.object({
|
|
1234
|
+
sources: z.array(canonicalResolvedSourceSchema).min(1).max(16),
|
|
1235
|
+
/** One rule per resolved (non-pk) column; documents the generated view for tooling. */
|
|
1236
|
+
resolve: z.array(canonicalResolveRuleSchema).max(500),
|
|
1237
|
+
/** Sources whose tombstone hides the resolved row (v1: the manual source only). */
|
|
1238
|
+
deletedBy: z.array(identifier).max(1).default([]),
|
|
1239
|
+
/**
|
|
1240
|
+
* Materialized: the view reads an indexed internal table (`table`) that the
|
|
1241
|
+
* data plane keeps in step with `compute`, the internal view holding the
|
|
1242
|
+
* resolution query. Omitted when the view computes on read.
|
|
1243
|
+
*/
|
|
1244
|
+
materialized: z.object({ table: identifier, compute: identifier }).strict().optional(),
|
|
1245
|
+
/** Write policies evaluated against the RESOLVED row (reads use the view's read policy). */
|
|
1246
|
+
policies: z.object({
|
|
1247
|
+
write: policyExpressionSchema.optional(),
|
|
1248
|
+
insert: policyExpressionSchema.optional(),
|
|
1249
|
+
update: policyExpressionSchema.optional()
|
|
1250
|
+
}).strict().default({})
|
|
1251
|
+
}).strict();
|
|
1252
|
+
function validateResolvedView({ view, tables, views = [] }) {
|
|
1253
|
+
const resolved = view.resolved;
|
|
1254
|
+
if (!resolved) return [];
|
|
1255
|
+
const errors = [];
|
|
1256
|
+
const fail = (message) => errors.push(`resolved table '${view.view}': ${message}`);
|
|
1257
|
+
const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
|
|
1258
|
+
if (!pkColumn) {
|
|
1259
|
+
fail("requires exactly one primary-key column");
|
|
1260
|
+
return errors;
|
|
1261
|
+
}
|
|
1262
|
+
const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
|
|
1263
|
+
const scope = {
|
|
1264
|
+
view,
|
|
1265
|
+
resolved,
|
|
1266
|
+
pkColumn,
|
|
1267
|
+
dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
|
|
1268
|
+
sourceByName: /* @__PURE__ */ new Map(),
|
|
1269
|
+
fail
|
|
1270
|
+
};
|
|
1271
|
+
if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail("requires exactly one manual source");
|
|
1272
|
+
const tableByName = new Map(tables.map((table) => [table.table, table]));
|
|
1273
|
+
for (const source of resolved.sources) {
|
|
1274
|
+
if (scope.sourceByName.has(source.name)) fail(`duplicate source '${source.name}'`);
|
|
1275
|
+
scope.sourceByName.set(source.name, source);
|
|
1276
|
+
const table = tableByName.get(source.table);
|
|
1277
|
+
if (!table) fail(`source '${source.name}' references unknown table '${source.table}'`);
|
|
1278
|
+
else validateSource({ scope, source, table });
|
|
1279
|
+
}
|
|
1280
|
+
validateRules(scope);
|
|
1281
|
+
validateViewCells(scope);
|
|
1282
|
+
if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
|
|
1283
|
+
for (const name2 of resolved.deletedBy) {
|
|
1284
|
+
if (scope.sourceByName.get(name2)?.kind !== "manual") fail(`deletedBy '${name2}' must be the manual source`);
|
|
1285
|
+
}
|
|
1286
|
+
return errors;
|
|
1287
|
+
}
|
|
1288
|
+
function validateSource({ scope, source, table }) {
|
|
1289
|
+
const { fail } = scope;
|
|
1290
|
+
if (source.kind === "etl" && table.source === void 0) fail(`etl source '${source.name}' must reference a synced table`);
|
|
1291
|
+
if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
|
|
1292
|
+
fail(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
|
|
1293
|
+
}
|
|
1294
|
+
const hasRev = table.columns.some((column) => column.cell?.part === "rev");
|
|
1295
|
+
if (source.kind === "manual" && !hasRev) fail(`manual source '${source.name}' must carry '__rev' cell columns`);
|
|
1296
|
+
if (source.kind === "prefetch" && hasRev) fail(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
|
|
1297
|
+
const byName = new Map(table.columns.map((c) => [c.name, c]));
|
|
1298
|
+
validateSourceColumns({ scope, source, byName });
|
|
1299
|
+
if (source.kind === "etl") {
|
|
1300
|
+
const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
|
|
1301
|
+
if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
|
|
1302
|
+
fail(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
validatePrefetchFields({ scope, source });
|
|
1306
|
+
}
|
|
1307
|
+
function validatePrefetchFields({ scope, source }) {
|
|
1308
|
+
const { fail, pkColumn } = scope;
|
|
1309
|
+
if (source.kind !== "prefetch") {
|
|
1310
|
+
if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
|
|
1311
|
+
fail(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
|
|
1312
|
+
}
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
|
|
1316
|
+
fail(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
|
|
1317
|
+
} else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
|
|
1318
|
+
fail(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
|
|
1319
|
+
}
|
|
1320
|
+
for (const column of source.writeback?.columns ?? []) {
|
|
1321
|
+
if (!source.columns.includes(column)) fail(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
function validateSourceColumns({ scope, source, byName }) {
|
|
1325
|
+
const { fail, pkColumn, dataColumns } = scope;
|
|
1326
|
+
for (const column of [pkColumn.name, ...source.columns]) {
|
|
1327
|
+
const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
|
|
1328
|
+
const theirs = byName.get(column);
|
|
1329
|
+
if (!own) fail(`source '${source.name}' supplies unknown column '${column}'`);
|
|
1330
|
+
else if (!theirs) fail(`source '${source.name}' table '${source.table}' has no column '${column}'`);
|
|
1331
|
+
else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail(`column '${column}' has a different type on source '${source.name}'`);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail }) {
|
|
1335
|
+
const ruled = /* @__PURE__ */ new Set();
|
|
1336
|
+
for (const rule of resolved.resolve) {
|
|
1337
|
+
if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
|
|
1338
|
+
fail(`resolve rule targets unknown column '${rule.column}'`);
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1341
|
+
if (ruled.has(rule.column)) fail(`column '${rule.column}' has more than one resolve rule`);
|
|
1342
|
+
ruled.add(rule.column);
|
|
1343
|
+
if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
|
|
1344
|
+
fail(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
|
|
1345
|
+
}
|
|
1346
|
+
if (new Set(rule.candidates).size !== rule.candidates.length) fail(`column '${rule.column}' repeats a candidate`);
|
|
1347
|
+
for (const candidate of rule.candidates) {
|
|
1348
|
+
const source = sourceByName.get(candidate);
|
|
1349
|
+
if (!source) fail(`column '${rule.column}' names unknown candidate source '${candidate}'`);
|
|
1350
|
+
else if (!source.columns.includes(rule.column)) fail(`column '${rule.column}' candidate '${candidate}' does not supply it`);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
for (const column of dataColumns.keys()) {
|
|
1354
|
+
if (column !== pkColumn.name && !ruled.has(column)) fail(`column '${column}' has no resolve rule`);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
var VIEW_CELL_TYPES = {
|
|
1358
|
+
source: "text",
|
|
1359
|
+
at: "timestamptz",
|
|
1360
|
+
by: "text",
|
|
1361
|
+
conflicts: "jsonb"
|
|
1362
|
+
};
|
|
1363
|
+
function validateViewCells({ view, dataColumns, pkColumn, fail }) {
|
|
1364
|
+
const expected = /* @__PURE__ */ new Map();
|
|
1365
|
+
for (const column of dataColumns.values()) {
|
|
1366
|
+
if (column.name === pkColumn.name) continue;
|
|
1367
|
+
for (const part of RESOLVED_VIEW_CELL_PARTS) {
|
|
1368
|
+
expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
for (const column of view.columns) {
|
|
1372
|
+
if (!column.cell) continue;
|
|
1373
|
+
const want = expected.get(column.name);
|
|
1374
|
+
if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
|
|
1375
|
+
fail(`unexpected cell column '${column.name}'`);
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
|
|
1379
|
+
expected.delete(column.name);
|
|
1380
|
+
}
|
|
1381
|
+
for (const name2 of expected.keys()) fail(`missing cell column '${name2}'`);
|
|
1382
|
+
}
|
|
1383
|
+
var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
|
|
1384
|
+
function validateMaterialized({ scope, tableByName, views }) {
|
|
1385
|
+
const { view, resolved, fail } = scope;
|
|
1386
|
+
const { table: tableName, compute: computeName } = resolved.materialized;
|
|
1387
|
+
const table = tableByName.get(tableName);
|
|
1388
|
+
if (!table || table.internal !== true || table.materializes !== view.view) {
|
|
1389
|
+
fail(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
|
|
1390
|
+
} else {
|
|
1391
|
+
if (columnSignature(table.columns) !== columnSignature(view.columns)) fail(`materialized table '${tableName}' must have exactly the view's columns`);
|
|
1392
|
+
if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail(`materialized table '${tableName}' must share the view's primary key`);
|
|
1393
|
+
}
|
|
1394
|
+
const compute = views.find((candidate) => candidate.view === computeName);
|
|
1395
|
+
if (!compute || compute.internal !== true) fail(`compute view '${computeName}' must be an internal view`);
|
|
1396
|
+
else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail(`compute view '${computeName}' must have exactly the view's columns`);
|
|
1397
|
+
for (const source of resolved.sources) {
|
|
1398
|
+
if (source.kind !== "etl") continue;
|
|
1399
|
+
const read = tableByName.get(source.table)?.policies?.read;
|
|
1400
|
+
const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
|
|
1401
|
+
if (!unconditional) {
|
|
1402
|
+
fail(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// src/canonical/v2.ts
|
|
879
1408
|
var CANONICAL_FORMAT = 2;
|
|
880
1409
|
var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
|
|
881
|
-
var
|
|
1410
|
+
var identifier2 = z.string().min(1).max(63).regex(IDENTIFIER_RE);
|
|
882
1411
|
var columnIdentifier = z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
|
|
883
1412
|
var COLUMN_TYPES = [
|
|
884
1413
|
"uuid",
|
|
@@ -894,14 +1423,14 @@ var COLUMN_TYPES = [
|
|
|
894
1423
|
];
|
|
895
1424
|
var columnTypeSchema = z.enum(COLUMN_TYPES);
|
|
896
1425
|
var canonicalEnumSchema = z.object({
|
|
897
|
-
name:
|
|
1426
|
+
name: identifier2,
|
|
898
1427
|
values: z.array(z.string().min(1).max(63)).min(1).max(100)
|
|
899
1428
|
});
|
|
900
1429
|
var canonicalColumnSchema = z.object({
|
|
901
1430
|
/** Property name in TS (camelCase); columnName is the Postgres name. */
|
|
902
1431
|
name: z.string().min(1).max(63),
|
|
903
1432
|
columnName: columnIdentifier,
|
|
904
|
-
type: z.union([columnTypeSchema, z.object({ enum:
|
|
1433
|
+
type: z.union([columnTypeSchema, z.object({ enum: identifier2 })]),
|
|
905
1434
|
nullable: z.boolean().default(true),
|
|
906
1435
|
unique: z.boolean().default(false),
|
|
907
1436
|
/** Literal default, or one of the allowlisted SQL functions. */
|
|
@@ -917,7 +1446,14 @@ var canonicalColumnSchema = z.object({
|
|
|
917
1446
|
/** Migration hints — same semantics as v1. */
|
|
918
1447
|
renamedFrom: columnIdentifier.optional(),
|
|
919
1448
|
cast: z.string().max(200).optional(),
|
|
920
|
-
deprecated: z.boolean().default(false)
|
|
1449
|
+
deprecated: z.boolean().default(false),
|
|
1450
|
+
/**
|
|
1451
|
+
* Marks a system cell-metadata column (see CELL_PARTS in constants): part
|
|
1452
|
+
* `part` of the cell of logical column `of`. Synthesized by authoring,
|
|
1453
|
+
* never client-writable, absent from GraphQL scalar fields. Omitted on
|
|
1454
|
+
* ordinary columns so their hashes are unchanged.
|
|
1455
|
+
*/
|
|
1456
|
+
cell: z.object({ of: z.string().min(1).max(63), part: z.enum(CELL_PARTS) }).strict().optional()
|
|
921
1457
|
}).superRefine((column, ctx) => {
|
|
922
1458
|
if (column.onUpdateFn === void 0) return;
|
|
923
1459
|
if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
|
|
@@ -932,36 +1468,28 @@ var canonicalLinkSchema = z.object({
|
|
|
932
1468
|
name: z.string().min(1).max(63),
|
|
933
1469
|
kind: z.enum(["toOne", "toMany", "toPlatformUser"]),
|
|
934
1470
|
/** Target table name (namespace-local). Absent for toPlatformUser. */
|
|
935
|
-
target:
|
|
1471
|
+
target: identifier2.optional(),
|
|
1472
|
+
/** Owning namespace of an imported target; omitted for local links. */
|
|
1473
|
+
namespace: identifier2.max(41).optional(),
|
|
936
1474
|
/** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
|
|
937
1475
|
on: columnIdentifier,
|
|
938
1476
|
/** Referenced column on the target (default: target's PK / platform user `sub`). */
|
|
939
1477
|
references: columnIdentifier.optional(),
|
|
940
1478
|
/** enforce: true emits a real FK constraint (same-namespace toOne only). */
|
|
941
1479
|
enforce: z.boolean().default(false),
|
|
1480
|
+
onDelete: z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
1481
|
+
onUpdate: z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
942
1482
|
description: z.string().max(500).optional()
|
|
943
1483
|
});
|
|
944
1484
|
var canonicalIndexSchema = z.object({
|
|
945
1485
|
columns: z.array(columnIdentifier).min(1),
|
|
946
1486
|
unique: z.boolean().default(false),
|
|
947
|
-
name:
|
|
948
|
-
});
|
|
949
|
-
var policyVariantSchema = z.object({
|
|
950
|
-
/** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
|
|
951
|
-
when: z.string().min(1).max(1e3),
|
|
952
|
-
/** Exactly one of: allow-all bypass, or a row predicate. */
|
|
953
|
-
allow: z.literal(true).optional(),
|
|
954
|
-
policy: z.string().min(1).max(2e3).optional()
|
|
955
|
-
}).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
|
|
956
|
-
message: "A variant declares exactly one of allow: true or policy"
|
|
1487
|
+
name: identifier2.optional()
|
|
957
1488
|
});
|
|
958
|
-
var
|
|
959
|
-
|
|
960
|
-
z.
|
|
961
|
-
|
|
962
|
-
variants: z.array(policyVariantSchema).max(10).default([])
|
|
963
|
-
})
|
|
964
|
-
]);
|
|
1489
|
+
var canonicalCheckSchema = z.object({
|
|
1490
|
+
name: identifier2,
|
|
1491
|
+
expression: z.string().min(1).max(2e3)
|
|
1492
|
+
}).strict();
|
|
965
1493
|
var canonicalPoliciesSchema = z.object({
|
|
966
1494
|
read: policyExpressionSchema.optional(),
|
|
967
1495
|
write: policyExpressionSchema.optional(),
|
|
@@ -1149,7 +1677,7 @@ var VIEW_QUERY_FUNCTION_ALLOWLIST = [
|
|
|
1149
1677
|
"width_bucket"
|
|
1150
1678
|
];
|
|
1151
1679
|
var canonicalActionSchema = z.object({
|
|
1152
|
-
name:
|
|
1680
|
+
name: identifier2,
|
|
1153
1681
|
requires: z.string().max(1e3).optional(),
|
|
1154
1682
|
params: z.record(z.string(), z.object({
|
|
1155
1683
|
type: columnTypeSchema,
|
|
@@ -1161,9 +1689,9 @@ var canonicalActionSchema = z.object({
|
|
|
1161
1689
|
});
|
|
1162
1690
|
var canonicalCowMutationSchema = z.object({
|
|
1163
1691
|
kind: z.enum(["override", "revert"]),
|
|
1164
|
-
name:
|
|
1692
|
+
name: identifier2,
|
|
1165
1693
|
/** Synced table (same namespace) the overlay copies from / falls back to. */
|
|
1166
|
-
synced:
|
|
1694
|
+
synced: identifier2,
|
|
1167
1695
|
/** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
|
|
1168
1696
|
pk: z.string().min(1).max(63),
|
|
1169
1697
|
/** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
|
|
@@ -1191,23 +1719,28 @@ var canonicalCowMutationSchema = z.object({
|
|
|
1191
1719
|
}
|
|
1192
1720
|
});
|
|
1193
1721
|
var canonicalForeignKeySchema = z.object({
|
|
1194
|
-
name:
|
|
1722
|
+
name: identifier2,
|
|
1195
1723
|
columns: z.array(columnIdentifier).min(1).max(8),
|
|
1196
|
-
target:
|
|
1724
|
+
target: identifier2,
|
|
1197
1725
|
references: z.array(columnIdentifier).min(1).max(8),
|
|
1198
|
-
deferrable: z.enum(["initially-immediate", "initially-deferred"]).optional()
|
|
1726
|
+
deferrable: z.enum(["initially-immediate", "initially-deferred"]).optional(),
|
|
1727
|
+
onDelete: z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
1728
|
+
onUpdate: z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional()
|
|
1199
1729
|
}).strict();
|
|
1200
1730
|
var canonicalTableSchema = z.object({
|
|
1201
|
-
table:
|
|
1731
|
+
table: identifier2,
|
|
1732
|
+
displayName: z.string().trim().min(1).max(120).optional(),
|
|
1202
1733
|
description: z.string().max(2e3).optional(),
|
|
1203
1734
|
/** Table-level rename hint (new in v2). */
|
|
1204
|
-
renamedFrom:
|
|
1205
|
-
/** Synced/reflected read-only tables (`source:
|
|
1206
|
-
source: z.string().regex(
|
|
1735
|
+
renamedFrom: identifier2.optional(),
|
|
1736
|
+
/** Synced/reflected read-only tables (`source: schema.table`) — v1 semantics. */
|
|
1737
|
+
source: z.string().regex(QUALIFIED_TABLE_PATTERN).optional(),
|
|
1207
1738
|
columns: z.array(canonicalColumnSchema).default([]),
|
|
1208
1739
|
primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
|
|
1209
1740
|
links: z.array(canonicalLinkSchema).default([]),
|
|
1210
1741
|
indexes: z.array(canonicalIndexSchema).default([]),
|
|
1742
|
+
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
1743
|
+
checks: z.array(canonicalCheckSchema).max(100).optional(),
|
|
1211
1744
|
policies: canonicalPoliciesSchema.default({}),
|
|
1212
1745
|
/**
|
|
1213
1746
|
* v1-conversion carry-over only. v2 has no action surface — the runtime
|
|
@@ -1218,22 +1751,55 @@ var canonicalTableSchema = z.object({
|
|
|
1218
1751
|
/** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
|
|
1219
1752
|
cowMutations: z.array(canonicalCowMutationSchema).optional(),
|
|
1220
1753
|
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
1221
|
-
foreignKeys: z.array(canonicalForeignKeySchema).max(100).optional()
|
|
1754
|
+
foreignKeys: z.array(canonicalForeignKeySchema).max(100).optional(),
|
|
1755
|
+
/**
|
|
1756
|
+
* Per-cell edit metadata (`cellMetadata: true` in authoring): every tracked
|
|
1757
|
+
* column `c` carries typed `c__at` / `c__by` system columns (plus `c__rev`
|
|
1758
|
+
* on a resolved table's internal sources), listed explicitly in `columns`
|
|
1759
|
+
* with a `cell` marker so DDL and migrations treat them as ordinary columns.
|
|
1760
|
+
* Omitted when off so existing definitions hash unchanged.
|
|
1761
|
+
*/
|
|
1762
|
+
cellMetadata: z.literal(true).optional(),
|
|
1763
|
+
/**
|
|
1764
|
+
* System-owned backing table of a resolved table (its manual or prefetch
|
|
1765
|
+
* source). Internal tables get DDL and IR metadata but no GraphQL surface;
|
|
1766
|
+
* only the data plane writes them. Omitted when false.
|
|
1767
|
+
*/
|
|
1768
|
+
internal: z.literal(true).optional(),
|
|
1769
|
+
/**
|
|
1770
|
+
* The materialized copy of resolved table `materializes` (internal): the
|
|
1771
|
+
* same columns as that view, including its cell parts, maintained by the
|
|
1772
|
+
* data plane. Omitted otherwise.
|
|
1773
|
+
*/
|
|
1774
|
+
materializes: identifier2.optional()
|
|
1222
1775
|
});
|
|
1776
|
+
var MAX_VIEW_QUERY = 1e5;
|
|
1777
|
+
var MAX_RESOLVED_VIEW_QUERY = 5e6;
|
|
1223
1778
|
var canonicalViewSchema = z.object({
|
|
1224
|
-
view:
|
|
1779
|
+
view: identifier2,
|
|
1225
1780
|
description: z.string().max(2e3).optional(),
|
|
1226
1781
|
columns: z.array(canonicalColumnSchema).min(1),
|
|
1227
1782
|
/** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
|
|
1228
1783
|
primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
|
|
1229
1784
|
links: z.array(canonicalLinkSchema).default([]),
|
|
1230
1785
|
policies: z.object({ read: policyExpressionSchema }).strict(),
|
|
1231
|
-
query: z.string().min(1).max(
|
|
1786
|
+
query: z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
|
|
1232
1787
|
message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
|
|
1233
1788
|
}),
|
|
1234
1789
|
/** Namespace-local logical relation names, sorted for deterministic hashing. */
|
|
1235
|
-
dependencies: z.array(
|
|
1790
|
+
dependencies: z.array(identifier2).max(500),
|
|
1791
|
+
/**
|
|
1792
|
+
* Present when this view is a resolved table (defineResolvedTable): the
|
|
1793
|
+
* multi-source metadata behind its generated query. Omitted otherwise so
|
|
1794
|
+
* existing views hash unchanged.
|
|
1795
|
+
*/
|
|
1796
|
+
resolved: canonicalResolvedSchema.optional(),
|
|
1797
|
+
/** System-owned view (a materialized resolved table's compute view): no GraphQL surface. */
|
|
1798
|
+
internal: z.literal(true).optional()
|
|
1236
1799
|
}).superRefine((view, ctx) => {
|
|
1800
|
+
if (view.resolved === void 0 && view.query.length > MAX_VIEW_QUERY) {
|
|
1801
|
+
ctx.addIssue({ code: "custom", path: ["query"], message: `view query must be at most ${MAX_VIEW_QUERY} characters` });
|
|
1802
|
+
}
|
|
1237
1803
|
for (const [index3, column] of view.columns.entries()) {
|
|
1238
1804
|
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
1239
1805
|
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare a default` });
|
|
@@ -1248,20 +1814,91 @@ var canonicalViewSchema = z.object({
|
|
|
1248
1814
|
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare migration hints` });
|
|
1249
1815
|
}
|
|
1250
1816
|
}
|
|
1817
|
+
if (view.resolved === void 0 && view.internal !== true) {
|
|
1818
|
+
for (const [index3, column] of view.columns.entries()) {
|
|
1819
|
+
if (column.cell) ctx.addIssue({ code: "custom", path: ["columns", index3], message: `only resolved tables carry cell columns ('${column.name}')` });
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1251
1822
|
for (const [index3, link] of view.links.entries()) {
|
|
1252
1823
|
if (link.enforce) {
|
|
1253
1824
|
ctx.addIssue({ code: "custom", path: ["links", index3, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
|
|
1254
1825
|
}
|
|
1826
|
+
if (link.onDelete !== void 0 || link.onUpdate !== void 0) {
|
|
1827
|
+
ctx.addIssue({ code: "custom", path: ["links", index3], message: `view link '${link.name}' cannot declare referential actions` });
|
|
1828
|
+
}
|
|
1255
1829
|
}
|
|
1256
1830
|
});
|
|
1257
|
-
var
|
|
1831
|
+
var CELL_PART_TYPES = {
|
|
1832
|
+
at: "timestamptz",
|
|
1833
|
+
by: "text",
|
|
1834
|
+
rev: "uuid",
|
|
1835
|
+
source: "text",
|
|
1836
|
+
conflicts: "jsonb"
|
|
1837
|
+
};
|
|
1838
|
+
function trackedCellColumns(table) {
|
|
1839
|
+
const pk = new Set(table.primaryKey ?? []);
|
|
1840
|
+
return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
|
|
1841
|
+
}
|
|
1842
|
+
var SUFFIXES = Object.values(CELL_PART_SUFFIX);
|
|
1843
|
+
function validateCellMetadata(table, index3, ctx) {
|
|
1844
|
+
const fail = (message, path = ["tables", index3]) => ctx.addIssue({ code: "custom", path, message });
|
|
1845
|
+
const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
|
|
1846
|
+
if (table.materializes !== void 0) return;
|
|
1847
|
+
if (table.cellMetadata !== true) {
|
|
1848
|
+
for (const { position } of cells) fail(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index3, "columns", position]);
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
if (table.source !== void 0) {
|
|
1852
|
+
fail(`synced table '${table.table}' cannot track cell metadata`);
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
const tracked = trackedCellColumns(table);
|
|
1856
|
+
const byName = new Map(tracked.map((column) => [column.name, column]));
|
|
1857
|
+
const withRev = cells.some(({ cell }) => cell.part === "rev");
|
|
1858
|
+
if (withRev && table.internal !== true) fail(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
|
|
1859
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1860
|
+
for (const { column, position, cell } of cells) {
|
|
1861
|
+
const path = ["tables", index3, "columns", position];
|
|
1862
|
+
const owner = byName.get(cell.of);
|
|
1863
|
+
if (!owner) {
|
|
1864
|
+
fail(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
|
|
1865
|
+
continue;
|
|
1866
|
+
}
|
|
1867
|
+
if (cell.part !== "at" && cell.part !== "by" && cell.part !== "rev") fail(`a table cell column must be part at/by/rev, not '${cell.part}'`, path);
|
|
1868
|
+
if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
|
|
1869
|
+
fail(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
|
|
1870
|
+
}
|
|
1871
|
+
if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
|
|
1872
|
+
fail(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
|
|
1873
|
+
}
|
|
1874
|
+
const key = `${cell.of}:${cell.part}`;
|
|
1875
|
+
if (seen.has(key)) fail(`duplicate cell column '${column.name}'`, path);
|
|
1876
|
+
seen.add(key);
|
|
1877
|
+
}
|
|
1878
|
+
for (const column of tracked) {
|
|
1879
|
+
if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
|
|
1880
|
+
fail(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
|
|
1881
|
+
}
|
|
1882
|
+
if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
|
|
1883
|
+
fail(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
|
|
1884
|
+
}
|
|
1885
|
+
for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
|
|
1886
|
+
if (!seen.has(`${column.name}:${part}`)) fail(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
var exportGateSchema = z.union([z.literal(true), z.object({ capability: z.string().min(1) }).strict(), z.object({ role: z.string().min(1) }).strict()]);
|
|
1891
|
+
var relationExportSchema = z.object({ read: exportGateSchema, write: exportGateSchema.optional() }).strict();
|
|
1892
|
+
var localDefinitionSchema = z.object({
|
|
1258
1893
|
definition_format: z.literal(CANONICAL_FORMAT),
|
|
1259
|
-
namespace:
|
|
1894
|
+
namespace: identifier2.max(41),
|
|
1260
1895
|
description: z.string().max(2e3).optional(),
|
|
1261
1896
|
enums: z.array(canonicalEnumSchema).default([]),
|
|
1262
1897
|
tables: z.array(canonicalTableSchema).min(0).max(500),
|
|
1263
1898
|
/** Optional to preserve byte-identical hashes for pre-view canonical v2 documents. */
|
|
1264
1899
|
views: z.array(canonicalViewSchema).max(500).optional(),
|
|
1900
|
+
/** Target-owned cross-namespace access contract, using existing namespace grants. */
|
|
1901
|
+
exports: z.record(identifier2, relationExportSchema).optional(),
|
|
1265
1902
|
/** Verbatim custom-roles grammar (@sequenceholdings/custom-roles-manifest). */
|
|
1266
1903
|
capabilities: z.object({
|
|
1267
1904
|
roles: z.unknown().optional(),
|
|
@@ -1274,6 +1911,7 @@ var canonicalDefinitionSchema = z.object({
|
|
|
1274
1911
|
ctx.addIssue({ code: "custom", path: ["tables", index3, "table"], message: `duplicate relation '${table.table}'` });
|
|
1275
1912
|
}
|
|
1276
1913
|
seen.add(table.table);
|
|
1914
|
+
validateCellMetadata(table, index3, ctx);
|
|
1277
1915
|
if (table.source !== void 0) {
|
|
1278
1916
|
for (const [columnIndex, column] of table.columns.entries()) {
|
|
1279
1917
|
if (column.onUpdateFn !== void 0) {
|
|
@@ -1287,12 +1925,41 @@ var canonicalDefinitionSchema = z.object({
|
|
|
1287
1925
|
}
|
|
1288
1926
|
}
|
|
1289
1927
|
const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
|
|
1928
|
+
const relations = /* @__PURE__ */ new Set([...fkTables.keys(), ...(definition.views ?? []).map((view) => view.view)]);
|
|
1929
|
+
for (const name2 of Object.keys(definition.exports ?? {})) {
|
|
1930
|
+
if (!relations.has(name2)) ctx.addIssue({ code: "custom", path: ["exports", name2], message: `export '${name2}' is not a local object` });
|
|
1931
|
+
}
|
|
1290
1932
|
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
1933
|
+
for (const [linkIndex, link] of table.links.entries()) {
|
|
1934
|
+
if (link.enforce && link.namespace !== void 0 && link.namespace !== definition.namespace) {
|
|
1935
|
+
ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", linkIndex], message: "physical foreign keys require a local target; use an ORM link without enforce" });
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1291
1938
|
const names = /* @__PURE__ */ new Set([
|
|
1292
1939
|
...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
|
|
1293
1940
|
...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
|
|
1294
1941
|
...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
|
|
1295
1942
|
]);
|
|
1943
|
+
for (const [index3, link] of table.links.entries()) {
|
|
1944
|
+
if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && (!link.enforce || link.kind !== "toOne")) {
|
|
1945
|
+
ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index3], message: "referential actions require an enforced toOne link" });
|
|
1946
|
+
}
|
|
1947
|
+
if (link.onDelete === "set null" || link.onUpdate === "set null") {
|
|
1948
|
+
const source = table.columns.find((column) => column.columnName === link.on);
|
|
1949
|
+
if (source?.nullable === false || table.primaryKey?.includes(link.on)) ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index3], message: "SET NULL requires a nullable source column" });
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
for (const [index3, check] of (table.checks ?? []).entries()) {
|
|
1953
|
+
const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index3], message });
|
|
1954
|
+
if (table.source) fail("synced tables cannot declare checks");
|
|
1955
|
+
if (names.has(check.name)) fail(`duplicate constraint '${check.name}'`);
|
|
1956
|
+
names.add(check.name);
|
|
1957
|
+
try {
|
|
1958
|
+
compileCheckExpression(check.expression, table.columns);
|
|
1959
|
+
} catch (error) {
|
|
1960
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1296
1963
|
for (const [index3, fk] of (table.foreignKeys ?? []).entries()) {
|
|
1297
1964
|
const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index3], message });
|
|
1298
1965
|
if (names.has(fk.name)) fail(`duplicate foreign key '${fk.name}'`);
|
|
@@ -1304,16 +1971,24 @@ var canonicalDefinitionSchema = z.object({
|
|
|
1304
1971
|
}
|
|
1305
1972
|
if (fk.columns.length !== fk.references.length) fail("foreign key columns and references must have the same number of columns");
|
|
1306
1973
|
if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail("foreign key columns must not repeat");
|
|
1307
|
-
for (const [position,
|
|
1308
|
-
const sourceColumn = table.columns.find((column) => column.columnName ===
|
|
1974
|
+
for (const [position, name2] of fk.columns.entries()) {
|
|
1975
|
+
const sourceColumn = table.columns.find((column) => column.columnName === name2);
|
|
1309
1976
|
const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
|
|
1310
1977
|
if (!sourceColumn || !targetColumn) fail("foreign key references an unknown column");
|
|
1311
1978
|
else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail("foreign key column types must match");
|
|
1312
1979
|
}
|
|
1980
|
+
if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
|
|
1981
|
+
if (fk.columns.some((name2) => table.columns.find((column) => column.columnName === name2)?.nullable === false || table.primaryKey?.includes(name2))) fail("SET NULL requires nullable source columns");
|
|
1982
|
+
}
|
|
1313
1983
|
const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index4) => index4.unique).map((index4) => index4.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
|
|
1314
1984
|
if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column)))) fail("foreign key target columns must form a unique key");
|
|
1315
1985
|
}
|
|
1316
1986
|
}
|
|
1987
|
+
for (const [viewIndex, view] of (definition.views ?? []).entries()) {
|
|
1988
|
+
for (const message of validateResolvedView({ view, tables: definition.tables, views: definition.views ?? [] })) {
|
|
1989
|
+
ctx.addIssue({ code: "custom", path: ["views", viewIndex, "resolved"], message });
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1317
1992
|
const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
1318
1993
|
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
1319
1994
|
const cowMutations = table.cowMutations ?? [];
|
|
@@ -1408,27 +2083,31 @@ var canonicalDefinitionSchema = z.object({
|
|
|
1408
2083
|
const visiting = /* @__PURE__ */ new Set();
|
|
1409
2084
|
const visited = /* @__PURE__ */ new Set();
|
|
1410
2085
|
let cycleReported = false;
|
|
1411
|
-
const visit = (
|
|
1412
|
-
if (visited.has(
|
|
1413
|
-
if (visiting.has(
|
|
2086
|
+
const visit = (name2, path) => {
|
|
2087
|
+
if (visited.has(name2) || cycleReported) return;
|
|
2088
|
+
if (visiting.has(name2)) {
|
|
1414
2089
|
cycleReported = true;
|
|
1415
2090
|
ctx.addIssue({
|
|
1416
2091
|
code: "custom",
|
|
1417
2092
|
path: ["views"],
|
|
1418
|
-
message: `view dependencies form a cycle: ${[...path,
|
|
2093
|
+
message: `view dependencies form a cycle: ${[...path, name2].join(" -> ")}`
|
|
1419
2094
|
});
|
|
1420
2095
|
return;
|
|
1421
2096
|
}
|
|
1422
|
-
visiting.add(
|
|
1423
|
-
const view = viewByName.get(
|
|
2097
|
+
visiting.add(name2);
|
|
2098
|
+
const view = viewByName.get(name2);
|
|
1424
2099
|
for (const dependency of view?.dependencies ?? []) {
|
|
1425
|
-
if (viewByName.has(dependency)) visit(dependency, [...path,
|
|
2100
|
+
if (viewByName.has(dependency)) visit(dependency, [...path, name2]);
|
|
1426
2101
|
}
|
|
1427
|
-
visiting.delete(
|
|
1428
|
-
visited.add(
|
|
2102
|
+
visiting.delete(name2);
|
|
2103
|
+
visited.add(name2);
|
|
1429
2104
|
};
|
|
1430
2105
|
for (const view of views) visit(view.view, []);
|
|
1431
2106
|
});
|
|
2107
|
+
var canonicalDefinitionSchema = localDefinitionSchema.safeExtend({
|
|
2108
|
+
/** Flattened, pinned snapshots. Imported relations never participate in this namespace's DDL. */
|
|
2109
|
+
imports: z.array(localDefinitionSchema).max(32).optional()
|
|
2110
|
+
});
|
|
1432
2111
|
function canonicalRelations(definition) {
|
|
1433
2112
|
const views = (definition.views ?? []).map((view) => ({
|
|
1434
2113
|
table: view.view,
|
|
@@ -1437,8 +2116,12 @@ function canonicalRelations(definition) {
|
|
|
1437
2116
|
...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
|
|
1438
2117
|
links: view.links,
|
|
1439
2118
|
indexes: [],
|
|
1440
|
-
|
|
1441
|
-
|
|
2119
|
+
// A resolved table's write policies (evaluated against the resolved row)
|
|
2120
|
+
// ride alongside the view's read policy.
|
|
2121
|
+
policies: { read: view.policies.read, ...view.resolved?.policies ?? {} },
|
|
2122
|
+
actions: [],
|
|
2123
|
+
...view.resolved !== void 0 ? { resolved: view.resolved } : {},
|
|
2124
|
+
...view.internal ? { internal: true } : {}
|
|
1442
2125
|
}));
|
|
1443
2126
|
return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
|
|
1444
2127
|
}
|
|
@@ -1448,13 +2131,14 @@ function normalizeCanonical(definition) {
|
|
|
1448
2131
|
...definition,
|
|
1449
2132
|
enums: sortByName(definition.enums),
|
|
1450
2133
|
tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
|
|
1451
|
-
const { cowMutations, foreignKeys, ...rest } = t2;
|
|
2134
|
+
const { cowMutations, foreignKeys, checks, ...rest } = t2;
|
|
1452
2135
|
return {
|
|
1453
2136
|
...rest,
|
|
1454
2137
|
links: sortByName(t2.links),
|
|
1455
2138
|
indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
|
|
1456
2139
|
actions: sortByName(t2.actions),
|
|
1457
2140
|
...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
|
|
2141
|
+
...checks?.length ? { checks: sortByName(checks) } : {},
|
|
1458
2142
|
// Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
|
|
1459
2143
|
...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
|
|
1460
2144
|
};
|
|
@@ -1467,7 +2151,460 @@ function normalizeCanonical(definition) {
|
|
|
1467
2151
|
dependencies: [...view.dependencies].sort()
|
|
1468
2152
|
}));
|
|
1469
2153
|
}
|
|
1470
|
-
|
|
2154
|
+
if (definition.imports?.length) {
|
|
2155
|
+
normalized.imports = definition.imports.map(normalizeCanonical).sort((a, b) => a.namespace.localeCompare(b.namespace));
|
|
2156
|
+
} else {
|
|
2157
|
+
delete normalized.imports;
|
|
2158
|
+
}
|
|
2159
|
+
return normalized;
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
// src/authoring/resolved-sources.ts
|
|
2163
|
+
function cloneColumn({ builder, property, notNull, primaryKey: primaryKey2 }) {
|
|
2164
|
+
const canonical = builder.toCanonicalColumn(property);
|
|
2165
|
+
let clone = new ColumnBuilder(canonical.type, canonical.columnName);
|
|
2166
|
+
if (primaryKey2) clone = clone.primaryKey();
|
|
2167
|
+
if (notNull) clone = clone.notNull();
|
|
2168
|
+
if (canonical.description !== void 0) clone = clone.description(canonical.description);
|
|
2169
|
+
return clone;
|
|
2170
|
+
}
|
|
2171
|
+
function planSource(input) {
|
|
2172
|
+
const { sourceName, input: source, fail } = input;
|
|
2173
|
+
if (!IDENTIFIER_PATTERN.test(sourceName)) fail(`source name "${sourceName}" must match ${IDENTIFIER_PATTERN}`);
|
|
2174
|
+
switch (source.kind) {
|
|
2175
|
+
case "manual":
|
|
2176
|
+
return {
|
|
2177
|
+
name: sourceName,
|
|
2178
|
+
kind: "manual",
|
|
2179
|
+
columns: input.dataColumns,
|
|
2180
|
+
table: internalTable(input, input.dataColumns, {
|
|
2181
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.name]: tombstoned(),
|
|
2182
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackStatus.name]: t.text(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackStatus.columnName),
|
|
2183
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackError.name]: t.text(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackError.columnName),
|
|
2184
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackAt.name]: t.timestamptz(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackAt.columnName)
|
|
2185
|
+
}, { revisions: true })
|
|
2186
|
+
};
|
|
2187
|
+
case "etl":
|
|
2188
|
+
return planEtl(input, source);
|
|
2189
|
+
case "prefetch":
|
|
2190
|
+
return planPrefetch(input, source);
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
var tombstoned = () => t.boolean(RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName).notNull().default(false);
|
|
2194
|
+
function internalTable(input, supplied, system, options = {}) {
|
|
2195
|
+
const { viewName, sourceName, columns, pk, pkBuilder } = input;
|
|
2196
|
+
const tableColumns = { [pk]: cloneColumn({ builder: pkBuilder, property: pk, notNull: true, primaryKey: true }) };
|
|
2197
|
+
for (const property of supplied) {
|
|
2198
|
+
tableColumns[property] = cloneColumn({ builder: columns[property], property, notNull: false, primaryKey: false });
|
|
2199
|
+
}
|
|
2200
|
+
return defineTable(`${viewName}__${sourceName}`, { ...tableColumns, ...system }, {
|
|
2201
|
+
description: `Internal ${input.input.kind} source "${sourceName}" of resolved table "${viewName}". System-maintained.`,
|
|
2202
|
+
cellMetadata: true,
|
|
2203
|
+
// The manual source identifies each edit (`c__rev`) so an async
|
|
2204
|
+
// confirmation clears only the exact edit it delivered.
|
|
2205
|
+
...options.revisions ? { cellRevisions: true } : {},
|
|
2206
|
+
internal: true
|
|
2207
|
+
});
|
|
2208
|
+
}
|
|
2209
|
+
function assertColumns(input, list, what) {
|
|
2210
|
+
for (const column of list) if (!input.dataColumns.includes(column)) input.fail(`${what} names unknown column "${column}"`);
|
|
2211
|
+
}
|
|
2212
|
+
function planEtl(input, source) {
|
|
2213
|
+
const { sourceName, pk, dataColumns, fail } = input;
|
|
2214
|
+
const table = typeof source.table === "function" ? source.table() : source.table;
|
|
2215
|
+
if (!isSeqTable(table) || table.source === void 0) fail(`etl source "${sourceName}" must reference a defineSyncedTable`);
|
|
2216
|
+
const synced = table;
|
|
2217
|
+
if (!(pk in synced.columns)) fail(`etl source "${sourceName}" table "${synced.tableName}" has no "${pk}" column`);
|
|
2218
|
+
const supplied = source.columns ? [...source.columns] : dataColumns.filter((column) => column in synced.columns);
|
|
2219
|
+
assertColumns(input, supplied, `etl source "${sourceName}"`);
|
|
2220
|
+
if (source.updatedAt !== void 0 && !(source.updatedAt in synced.columns)) {
|
|
2221
|
+
fail(`etl source "${sourceName}" updatedAt "${source.updatedAt}" is not a column of "${synced.tableName}"`);
|
|
2222
|
+
}
|
|
2223
|
+
return { name: sourceName, kind: "etl", table: synced, columns: supplied, ...source.updatedAt ? { updatedAt: source.updatedAt } : {} };
|
|
2224
|
+
}
|
|
2225
|
+
function planPrefetch(input, source) {
|
|
2226
|
+
const { sourceName, pk, fail } = input;
|
|
2227
|
+
assertColumns(input, source.columns, `prefetch source "${sourceName}"`);
|
|
2228
|
+
if (source.key !== pk && !source.columns.includes(source.key)) {
|
|
2229
|
+
fail(`prefetch source "${sourceName}" key "${source.key}" must be the primary key or one of its columns`);
|
|
2230
|
+
}
|
|
2231
|
+
for (const column of source.writeback?.columns ?? []) {
|
|
2232
|
+
if (!source.columns.includes(column)) fail(`prefetch source "${sourceName}" writes back "${column}", which it does not supply`);
|
|
2233
|
+
}
|
|
2234
|
+
const writeback = source.writeback;
|
|
2235
|
+
return {
|
|
2236
|
+
name: sourceName,
|
|
2237
|
+
kind: "prefetch",
|
|
2238
|
+
columns: [...source.columns],
|
|
2239
|
+
key: source.key,
|
|
2240
|
+
function: source.function,
|
|
2241
|
+
fetch: source.fetch === "single" ? { shape: "single" } : { shape: "batch", maxKeys: source.fetch.batch.maxKeys },
|
|
2242
|
+
prefetchDefaults: {
|
|
2243
|
+
maxStalenessMs: parseDuration(source.prefetchDefaults?.maxStaleness ?? "30s"),
|
|
2244
|
+
timeoutMs: parseDuration(source.prefetchDefaults?.timeout ?? "1500ms"),
|
|
2245
|
+
onError: source.prefetchDefaults?.onError ?? "fallback"
|
|
2246
|
+
},
|
|
2247
|
+
...writeback ? {
|
|
2248
|
+
writeback: {
|
|
2249
|
+
columns: [...writeback.columns],
|
|
2250
|
+
defaultMode: writeback.defaultMode ?? "sync",
|
|
2251
|
+
timeoutMs: parseDuration(writeback.timeout ?? "5s"),
|
|
2252
|
+
clearManualOnConfirm: writeback.clearManualOnConfirm ?? true
|
|
2253
|
+
}
|
|
2254
|
+
} : {},
|
|
2255
|
+
table: internalTable(input, source.columns, {
|
|
2256
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.fetchedAt.name]: t.timestamptz(RESOLVED_SOURCE_SYSTEM_COLUMNS.fetchedAt.columnName).notNull().default("now()"),
|
|
2257
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.name]: tombstoned()
|
|
2258
|
+
})
|
|
2259
|
+
};
|
|
2260
|
+
}
|
|
2261
|
+
function parseDuration(input) {
|
|
2262
|
+
const value = parseDurationMs(input);
|
|
2263
|
+
if (value === void 0) throw new OrmDefinitionError(`invalid duration "${input}" \u2014 use e.g. '1500ms', '30s', '5m'`);
|
|
2264
|
+
return value;
|
|
2265
|
+
}
|
|
2266
|
+
var PG_TYPE = {
|
|
2267
|
+
uuid: "uuid",
|
|
2268
|
+
text: "text",
|
|
2269
|
+
integer: "integer",
|
|
2270
|
+
bigint: "bigint",
|
|
2271
|
+
numeric: "numeric",
|
|
2272
|
+
double: "double precision",
|
|
2273
|
+
boolean: "boolean",
|
|
2274
|
+
timestamptz: "timestamptz",
|
|
2275
|
+
date: "date",
|
|
2276
|
+
jsonb: "jsonb"
|
|
2277
|
+
};
|
|
2278
|
+
var ident = (value) => `"${value.replace(/"/g, '""')}"`;
|
|
2279
|
+
var literal = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
2280
|
+
var physical = ({ source, property }) => source.table.columns[property].columnNameFor(property);
|
|
2281
|
+
function candidateSql({ source, column, pk, pgType }) {
|
|
2282
|
+
const a = ident(`s_${source.name}`);
|
|
2283
|
+
const name2 = physical({ source, property: column });
|
|
2284
|
+
const value = `CAST(${a}.${ident(name2)} AS ${pgType})`;
|
|
2285
|
+
if (source.kind === "etl") {
|
|
2286
|
+
return {
|
|
2287
|
+
source: source.name,
|
|
2288
|
+
value,
|
|
2289
|
+
has: `(${a}.${ident(physical({ source, property: pk }))} IS NOT NULL)`,
|
|
2290
|
+
at: source.updatedAt ? `CAST(${a}.${ident(physical({ source, property: source.updatedAt }))} AS timestamptz)` : "CAST(NULL AS timestamptz)",
|
|
2291
|
+
by: "CAST(NULL AS text)"
|
|
2292
|
+
};
|
|
2293
|
+
}
|
|
2294
|
+
const at = `${a}.${ident(cellColumnName(name2, "at"))}`;
|
|
2295
|
+
const tombstoned2 = source.kind === "prefetch" ? ` AND NOT ${a}.${ident(RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName)}` : "";
|
|
2296
|
+
return { source: source.name, value, has: `(${at} IS NOT NULL${tombstoned2})`, at, by: `${a}.${ident(cellColumnName(name2, "by"))}` };
|
|
2297
|
+
}
|
|
2298
|
+
function outranks({ i, j, key, direction, inclusive }) {
|
|
2299
|
+
const ki = i[key];
|
|
2300
|
+
const kj = j[key];
|
|
2301
|
+
const op = inclusive ? `${direction}=` : direction;
|
|
2302
|
+
return inclusive ? `(${kj} IS NULL OR (${ki} IS NOT NULL AND ${ki} ${op} ${kj}))` : `(${ki} IS NOT NULL AND (${kj} IS NULL OR ${ki} ${op} ${kj}))`;
|
|
2303
|
+
}
|
|
2304
|
+
function rankedWinner({ candidates, key, direction }) {
|
|
2305
|
+
const branches = candidates.map((candidate, index3) => {
|
|
2306
|
+
const beats = candidates.map((other, otherIndex) => otherIndex === index3 ? null : `(NOT ${other.has} OR ${outranks({ i: candidate, j: other, key, direction, inclusive: index3 < otherIndex })})`).filter((clause) => clause !== null);
|
|
2307
|
+
return `WHEN ${[candidate.has, ...beats].join(" AND ")} THEN ${index3 + 1}`;
|
|
2308
|
+
});
|
|
2309
|
+
return `CASE ${branches.join(" ")} END`;
|
|
2310
|
+
}
|
|
2311
|
+
function winnerIndex({ comparator, candidates }) {
|
|
2312
|
+
const firstWith = (test) => candidates.map((candidate, index3) => `WHEN ${test(candidate)} THEN ${index3 + 1}`).join(" ");
|
|
2313
|
+
if (candidates.length === 1 || comparator === "priority") return `CASE ${firstWith((c) => c.has)} END`;
|
|
2314
|
+
switch (comparator) {
|
|
2315
|
+
case "latest":
|
|
2316
|
+
return rankedWinner({ candidates, key: "at", direction: ">" });
|
|
2317
|
+
case "highest":
|
|
2318
|
+
return rankedWinner({ candidates, key: "value", direction: ">" });
|
|
2319
|
+
case "lowest":
|
|
2320
|
+
return rankedWinner({ candidates, key: "value", direction: "<" });
|
|
2321
|
+
case "nonNullFirst":
|
|
2322
|
+
return `CASE ${firstWith((c) => `${c.has} AND ${c.value} IS NOT NULL`)} ${firstWith((c) => c.has)} END`;
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
function pick(winner, candidates, field) {
|
|
2326
|
+
return `CASE ${winner} ${candidates.map((candidate, index3) => `WHEN ${index3 + 1} THEN ${field(candidate)}`).join(" ")} END`;
|
|
2327
|
+
}
|
|
2328
|
+
function loserSql({ loser, winner }) {
|
|
2329
|
+
return `CASE WHEN ${loser.has} AND ${loser.value} IS DISTINCT FROM ${winner.value} AND ${loser.at} > ${winner.at} THEN jsonb_build_array(jsonb_build_object('source', ${literal(loser.source)}, 'value', to_jsonb(${loser.value}), 'at', ${loser.at})) ELSE '[]'::jsonb END`;
|
|
2330
|
+
}
|
|
2331
|
+
function losersAgainst({ candidates, winner, isWinner }) {
|
|
2332
|
+
const losers = candidates.flatMap((loser, index3) => {
|
|
2333
|
+
const excluded = isWinner(loser, index3);
|
|
2334
|
+
if (excluded === true) return [];
|
|
2335
|
+
const clause = loserSql({ loser, winner });
|
|
2336
|
+
return [excluded === false ? clause : `CASE WHEN ${excluded} THEN '[]'::jsonb ELSE ${clause} END`];
|
|
2337
|
+
});
|
|
2338
|
+
return losers.length === 0 ? "CAST(NULL AS jsonb)" : `NULLIF(${losers.join(" || ")}, '[]'::jsonb)`;
|
|
2339
|
+
}
|
|
2340
|
+
function builtinCell({ comparator, candidates }) {
|
|
2341
|
+
const winner = winnerIndex({ comparator, candidates });
|
|
2342
|
+
const conflicts = comparator === "latest" || candidates.length < 2 ? "CAST(NULL AS jsonb)" : `CASE ${winner} ${candidates.map((known, index3) => `WHEN ${index3 + 1} THEN ${losersAgainst({ candidates, winner: known, isWinner: (_c, other) => other === index3 })}`).join(" ")} END`;
|
|
2343
|
+
return {
|
|
2344
|
+
value: pick(winner, candidates, (c) => c.value),
|
|
2345
|
+
parts: {
|
|
2346
|
+
source: pick(winner, candidates, (c) => literal(c.source)),
|
|
2347
|
+
at: pick(winner, candidates, (c) => c.at),
|
|
2348
|
+
by: pick(winner, candidates, (c) => c.by),
|
|
2349
|
+
conflicts
|
|
2350
|
+
}
|
|
2351
|
+
};
|
|
2352
|
+
}
|
|
2353
|
+
function customCell({ candidates, orderBy, index: index3 }) {
|
|
2354
|
+
const w = ident(`w_${index3}`);
|
|
2355
|
+
const rows = candidates.map((c, ord) => `(${literal(c.source)}, ${c.has}, ${c.value}, ${c.at}, ${c.by}, ${ord})`);
|
|
2356
|
+
const lateral = ` LEFT JOIN LATERAL (SELECT max(winner.source) AS source, (array_agg(winner.value))[1] AS value, max(winner.updated_at) AS updated_at, max(winner.actor) AS actor FROM (WITH cand(source, has, value, updated_at, actor, ord) AS (VALUES ${rows.join(", ")}) SELECT c.source, c.value, c.updated_at, c.actor FROM cand c WHERE c.has ORDER BY ${orderBy}, c.ord LIMIT 1) AS winner) AS ${w} ON true`;
|
|
2357
|
+
const winner = { value: `${w}.value`, at: `${w}.updated_at` };
|
|
2358
|
+
return {
|
|
2359
|
+
value: winner.value,
|
|
2360
|
+
lateral,
|
|
2361
|
+
parts: {
|
|
2362
|
+
source: `${w}.source`,
|
|
2363
|
+
at: winner.at,
|
|
2364
|
+
by: `${w}.actor`,
|
|
2365
|
+
conflicts: candidates.length < 2 ? "CAST(NULL AS jsonb)" : losersAgainst({ candidates, winner, isWinner: (c) => `${w}.source = ${literal(c.source)}` })
|
|
2366
|
+
}
|
|
2367
|
+
};
|
|
2368
|
+
}
|
|
2369
|
+
function buildResolvedQuery({ context, plan }) {
|
|
2370
|
+
const { qb, relation } = context;
|
|
2371
|
+
const pkColumn = plan.columns[plan.pk].columnNameFor(plan.pk);
|
|
2372
|
+
const alias = (source) => ident(`s_${source}`);
|
|
2373
|
+
const tombstoned2 = RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName;
|
|
2374
|
+
const sourceByName = new Map(plan.plans.map((source) => [source.name, source]));
|
|
2375
|
+
const schema = getTableConfig(relation(plan.plans[0].table)).schema;
|
|
2376
|
+
const keyBranches = plan.plans.map((source) => {
|
|
2377
|
+
const where = source.kind === "prefetch" ? sql.raw(` WHERE NOT ${ident(tombstoned2)}`) : sql.raw("");
|
|
2378
|
+
const sourcePk = ident(physical({ source, property: plan.pk }));
|
|
2379
|
+
const selectedPk = sourcePk === ident(pkColumn) ? sourcePk : `${sourcePk} AS ${ident(pkColumn)}`;
|
|
2380
|
+
return sql`SELECT ${sql.raw(selectedPk)} FROM ${relation(source.table)}${where}`;
|
|
2381
|
+
});
|
|
2382
|
+
const joins = plan.plans.map(
|
|
2383
|
+
(source) => sql` LEFT JOIN ${relation(source.table)} AS ${sql.raw(alias(source.name))} ON ${sql.raw(`${alias(source.name)}.${ident(physical({ source, property: plan.pk }))} = "k".${ident(pkColumn)}`)}`
|
|
2384
|
+
);
|
|
2385
|
+
const cells = /* @__PURE__ */ new Map();
|
|
2386
|
+
plan.rules.forEach((rule, index3) => {
|
|
2387
|
+
const canonical = plan.columns[rule.column].toCanonicalColumn(rule.column);
|
|
2388
|
+
const pgType = typeof canonical.type === "string" ? PG_TYPE[canonical.type] : `${schema === void 0 ? "" : `${ident(schema)}.`}${ident(canonical.type.enum)}`;
|
|
2389
|
+
const candidates = rule.candidates.map((name2) => candidateSql({ source: sourceByName.get(name2), column: rule.column, pk: plan.pk, pgType }));
|
|
2390
|
+
cells.set(rule.column, rule.comparator === "custom" ? customCell({ candidates, orderBy: rule.orderBy, index: index3 }) : builtinCell({ comparator: rule.comparator, candidates }));
|
|
2391
|
+
});
|
|
2392
|
+
const laterals = [...cells.values()].flatMap((cell) => cell.lateral ? [sql.raw(cell.lateral)] : []);
|
|
2393
|
+
const from = sql.join([sql`(${sql.join(keyBranches, sql.raw(" UNION "))}) AS "k"`, ...joins, ...laterals], sql.raw(""));
|
|
2394
|
+
const selected = {
|
|
2395
|
+
[plan.pk]: sql.raw(`"k".${ident(pkColumn)}`).as(pkColumn)
|
|
2396
|
+
};
|
|
2397
|
+
for (const [property, builder] of Object.entries(plan.columns)) {
|
|
2398
|
+
if (property === plan.pk) continue;
|
|
2399
|
+
const cell = cells.get(property);
|
|
2400
|
+
const columnName = builder.columnNameFor(property);
|
|
2401
|
+
selected[property] = sql.raw(cell.value).as(columnName);
|
|
2402
|
+
for (const [part, expression] of Object.entries(cell.parts)) {
|
|
2403
|
+
selected[cellColumnName(property, part)] = sql.raw(expression).as(cellColumnName(columnName, part));
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
const manual = plan.plans.find((source) => source.kind === "manual");
|
|
2407
|
+
const system = RESOLVED_VIEW_SYSTEM_COLUMNS;
|
|
2408
|
+
selected[system.writebackStatus.name] = sql.raw(`${alias(manual.name)}.${ident(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackStatus.columnName)}`).as(system.writebackStatus.columnName);
|
|
2409
|
+
selected[system.writebackError.name] = sql.raw(`${alias(manual.name)}.${ident(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackError.columnName)}`).as(system.writebackError.columnName);
|
|
2410
|
+
const hidden = plan.deletedBy.map((name2) => `NOT COALESCE(${alias(name2)}.${ident(tombstoned2)}, false)`);
|
|
2411
|
+
const query = qb.select(selected).from(from);
|
|
2412
|
+
return hidden.length > 0 ? query.where(sql.raw(hidden.join(" AND "))) : query;
|
|
2413
|
+
}
|
|
2414
|
+
function managedFn(id) {
|
|
2415
|
+
return { kind: "managed", id };
|
|
2416
|
+
}
|
|
2417
|
+
function manualSource() {
|
|
2418
|
+
return { kind: "manual" };
|
|
2419
|
+
}
|
|
2420
|
+
function etlSource(input) {
|
|
2421
|
+
return { kind: "etl", ...input };
|
|
2422
|
+
}
|
|
2423
|
+
function prefetchSource(input) {
|
|
2424
|
+
return { kind: "prefetch", ...input };
|
|
2425
|
+
}
|
|
2426
|
+
var latest = (candidates) => ({ kind: "latest", candidates });
|
|
2427
|
+
var highest = (candidates) => ({ kind: "highest", candidates });
|
|
2428
|
+
var lowest = (candidates) => ({ kind: "lowest", candidates });
|
|
2429
|
+
var priority = (candidates) => ({ kind: "priority", candidates });
|
|
2430
|
+
var nonNullFirst = (candidates) => ({ kind: "nonNullFirst", candidates });
|
|
2431
|
+
var custom = (candidates, orderBy) => ({ kind: "custom", candidates, orderBy });
|
|
2432
|
+
var SYSTEM_VIEW_COLUMNS = {
|
|
2433
|
+
[RESOLVED_VIEW_SYSTEM_COLUMNS.writebackStatus.name]: () => t.text(RESOLVED_VIEW_SYSTEM_COLUMNS.writebackStatus.columnName),
|
|
2434
|
+
[RESOLVED_VIEW_SYSTEM_COLUMNS.writebackError.name]: () => t.text(RESOLVED_VIEW_SYSTEM_COLUMNS.writebackError.columnName)
|
|
2435
|
+
};
|
|
2436
|
+
function defineResolvedTable(name2, columns, config) {
|
|
2437
|
+
const file = activeAuthoringContext()?.currentFile;
|
|
2438
|
+
const fail = (message) => {
|
|
2439
|
+
throw new OrmDefinitionError(`resolved table "${name2}": ${message}`, file);
|
|
2440
|
+
};
|
|
2441
|
+
if (!IDENTIFIER_PATTERN.test(name2)) fail(`name must match ${IDENTIFIER_PATTERN}`);
|
|
2442
|
+
for (const property of Object.keys(columns)) {
|
|
2443
|
+
if (RESERVED_RESOLVED_COLUMN_NAMES.has(property) || RESERVED_RESOLVED_COLUMN_NAMES.has(columns[property].columnNameFor(property))) {
|
|
2444
|
+
fail(`column "${property}" collides with a reserved system column`);
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
for (const property of Object.keys(columns)) {
|
|
2448
|
+
if (columns[property].columnNameFor(property).length > MAX_TRACKED_COLUMN_NAME) {
|
|
2449
|
+
fail(`column "${property}" is too long for a resolved table (max ${MAX_TRACKED_COLUMN_NAME} characters, room for cell parts)`);
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
const pkEntries = Object.entries(columns).filter(([, builder]) => builder.isPrimaryKey);
|
|
2453
|
+
if (pkEntries.length !== 1) fail("declare exactly one .primaryKey() column");
|
|
2454
|
+
const [pk, pkBuilder] = pkEntries[0];
|
|
2455
|
+
const dataColumns = Object.keys(columns).filter((property) => property !== pk);
|
|
2456
|
+
if (dataColumns.length === 0) fail("declare at least one non-key column");
|
|
2457
|
+
const sourceEntries = Object.entries(config.sources);
|
|
2458
|
+
if (sourceEntries.filter(([, source]) => source.kind === "manual").length !== 1) fail("declare exactly one manualSource()");
|
|
2459
|
+
const plans = sourceEntries.map(
|
|
2460
|
+
([sourceName, input]) => planSource({ viewName: name2, sourceName, input, columns, pk, pkBuilder, dataColumns, fail })
|
|
2461
|
+
);
|
|
2462
|
+
const rules = planRules({ config, plans, dataColumns, fail });
|
|
2463
|
+
const deletedBy = [...config.rows?.deletedBy ?? []];
|
|
2464
|
+
for (const source of deletedBy) {
|
|
2465
|
+
if (plans.find((plan) => plan.name === source)?.kind !== "manual") fail(`rows.deletedBy "${source}" must be the manual source`);
|
|
2466
|
+
}
|
|
2467
|
+
const partType = { source: t.text, at: t.timestamptz, by: t.text, conflicts: t.jsonb };
|
|
2468
|
+
const viewColumns = {};
|
|
2469
|
+
for (const [property, builder] of Object.entries(columns)) {
|
|
2470
|
+
viewColumns[property] = cloneColumn({ builder, property, notNull: property === pk, primaryKey: property === pk });
|
|
2471
|
+
if (property === pk) continue;
|
|
2472
|
+
const columnName = builder.columnNameFor(property);
|
|
2473
|
+
for (const part of RESOLVED_VIEW_CELL_PARTS) {
|
|
2474
|
+
viewColumns[cellColumnName(property, part)] = partType[part](cellColumnName(columnName, part)).cellPart(property, part);
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
for (const [property, make] of Object.entries(SYSTEM_VIEW_COLUMNS)) viewColumns[property] = make();
|
|
2478
|
+
const { read, ...writePolicies } = config.policies;
|
|
2479
|
+
const resolved = {
|
|
2480
|
+
internalTables: plans.filter((plan) => plan.kind !== "etl").map((plan) => plan.table),
|
|
2481
|
+
sources: plans,
|
|
2482
|
+
rules,
|
|
2483
|
+
deletedBy,
|
|
2484
|
+
policies: writePolicies
|
|
2485
|
+
};
|
|
2486
|
+
const queryPlan = { columns, pk, plans, rules, deletedBy };
|
|
2487
|
+
const typedColumns = viewColumns;
|
|
2488
|
+
const surface = {
|
|
2489
|
+
...config.description !== void 0 ? { description: config.description } : {},
|
|
2490
|
+
...config.links !== void 0 ? { links: config.links } : {},
|
|
2491
|
+
policies: { read }
|
|
2492
|
+
};
|
|
2493
|
+
if (!config.materialize) {
|
|
2494
|
+
return defineView(name2, typedColumns, { ...surface, query: (context) => buildResolvedQuery({ context, plan: queryPlan }), resolved });
|
|
2495
|
+
}
|
|
2496
|
+
const materialize = config.materialize === true ? {} : config.materialize;
|
|
2497
|
+
const computeName = `${name2}__compute`;
|
|
2498
|
+
const tableName = `${name2}__materialized`;
|
|
2499
|
+
const compute = defineView(computeName, typedColumns, {
|
|
2500
|
+
policies: { read },
|
|
2501
|
+
query: (context) => buildResolvedQuery({ context, plan: queryPlan }),
|
|
2502
|
+
internal: true
|
|
2503
|
+
});
|
|
2504
|
+
const table = defineTable(tableName, viewColumns, {
|
|
2505
|
+
description: `Materialized rows of resolved table "${name2}". System-maintained from "${computeName}".`,
|
|
2506
|
+
internal: true,
|
|
2507
|
+
materializes: name2,
|
|
2508
|
+
...materialize.indexes !== void 0 ? { indexes: materialize.indexes } : {}
|
|
2509
|
+
});
|
|
2510
|
+
return defineView(name2, typedColumns, {
|
|
2511
|
+
...surface,
|
|
2512
|
+
query: (context) => {
|
|
2513
|
+
const handle = context.relation(table);
|
|
2514
|
+
const selected = Object.fromEntries(Object.entries(viewColumns).map(([property, builder]) => [
|
|
2515
|
+
property,
|
|
2516
|
+
sql`${handle[property]}`.as(builder.columnNameFor(property))
|
|
2517
|
+
]));
|
|
2518
|
+
return context.qb.select(selected).from(context.relation(table));
|
|
2519
|
+
},
|
|
2520
|
+
resolved: {
|
|
2521
|
+
...resolved,
|
|
2522
|
+
internalTables: [...resolved.internalTables, table],
|
|
2523
|
+
internalViews: [compute],
|
|
2524
|
+
materialized: { table: tableName, compute: computeName }
|
|
2525
|
+
}
|
|
2526
|
+
});
|
|
2527
|
+
}
|
|
2528
|
+
function planRules({ config, plans, dataColumns, fail }) {
|
|
2529
|
+
const kindOrder = { manual: 0, prefetch: 1, etl: 2 };
|
|
2530
|
+
const fallback = config.default ?? priority([...plans].sort((a, b) => kindOrder[a.kind] - kindOrder[b.kind]).map((plan) => plan.name));
|
|
2531
|
+
for (const column of Object.keys(config.resolve ?? {})) {
|
|
2532
|
+
if (!dataColumns.includes(column)) fail(`resolve names unknown column "${column}"`);
|
|
2533
|
+
}
|
|
2534
|
+
return dataColumns.map((column) => {
|
|
2535
|
+
const explicit = config.resolve?.[column];
|
|
2536
|
+
const comparator = explicit ?? fallback;
|
|
2537
|
+
const candidates = explicit ? [...explicit.candidates] : comparator.candidates.filter((name2) => plans.find((plan) => plan.name === name2)?.columns.includes(column));
|
|
2538
|
+
for (const candidate of candidates) {
|
|
2539
|
+
const plan = plans.find((entry) => entry.name === candidate);
|
|
2540
|
+
if (!plan) fail(`column "${column}" names unknown source "${candidate}"`);
|
|
2541
|
+
else if (!plan.columns.includes(column)) fail(`column "${column}" candidate "${candidate}" does not supply it`);
|
|
2542
|
+
}
|
|
2543
|
+
if (candidates.length === 0) fail(`column "${column}" has no candidate source`);
|
|
2544
|
+
if (comparator.kind === "custom") assertSafeOrderBy({ orderBy: comparator.orderBy ?? "", column, fail });
|
|
2545
|
+
return {
|
|
2546
|
+
column,
|
|
2547
|
+
comparator: comparator.kind,
|
|
2548
|
+
candidates,
|
|
2549
|
+
...comparator.kind === "custom" && comparator.orderBy !== void 0 ? { orderBy: comparator.orderBy } : {}
|
|
2550
|
+
};
|
|
2551
|
+
});
|
|
2552
|
+
}
|
|
2553
|
+
var ORDER_BY_KEYWORDS = /* @__PURE__ */ new Set([
|
|
2554
|
+
"value",
|
|
2555
|
+
"updated_at",
|
|
2556
|
+
"source",
|
|
2557
|
+
"actor",
|
|
2558
|
+
"ord",
|
|
2559
|
+
"asc",
|
|
2560
|
+
"desc",
|
|
2561
|
+
"nulls",
|
|
2562
|
+
"first",
|
|
2563
|
+
"last",
|
|
2564
|
+
"case",
|
|
2565
|
+
"when",
|
|
2566
|
+
"then",
|
|
2567
|
+
"else",
|
|
2568
|
+
"end",
|
|
2569
|
+
"and",
|
|
2570
|
+
"or",
|
|
2571
|
+
"not",
|
|
2572
|
+
"is",
|
|
2573
|
+
"null",
|
|
2574
|
+
"true",
|
|
2575
|
+
"false",
|
|
2576
|
+
"in",
|
|
2577
|
+
"distinct",
|
|
2578
|
+
"from",
|
|
2579
|
+
"coalesce",
|
|
2580
|
+
"nullif",
|
|
2581
|
+
"greatest",
|
|
2582
|
+
"least",
|
|
2583
|
+
"text",
|
|
2584
|
+
"numeric",
|
|
2585
|
+
"integer",
|
|
2586
|
+
"bigint",
|
|
2587
|
+
"boolean",
|
|
2588
|
+
"timestamptz",
|
|
2589
|
+
"date",
|
|
2590
|
+
"jsonb",
|
|
2591
|
+
"interval"
|
|
2592
|
+
]);
|
|
2593
|
+
var ALLOWED_FUNCTIONS = new Set(VIEW_QUERY_FUNCTION_ALLOWLIST);
|
|
2594
|
+
var ORDER_BY_TOKEN = /\s+|'(?:[^']|'')*'|\d+(?:\.\d+)?|[a-z_][a-z0-9_]*|::|<=|>=|<>|!=|[-+*/%<>=(),|]/giy;
|
|
2595
|
+
function assertSafeOrderBy({ orderBy, column, fail }) {
|
|
2596
|
+
ORDER_BY_TOKEN.lastIndex = 0;
|
|
2597
|
+
let consumed = 0;
|
|
2598
|
+
for (let match = ORDER_BY_TOKEN.exec(orderBy); match; match = ORDER_BY_TOKEN.exec(orderBy)) {
|
|
2599
|
+
consumed = ORDER_BY_TOKEN.lastIndex;
|
|
2600
|
+
const token = match[0].toLowerCase();
|
|
2601
|
+
if (/^[a-z_]/.test(token) && !ORDER_BY_KEYWORDS.has(token) && !ALLOWED_FUNCTIONS.has(token)) {
|
|
2602
|
+
fail(`column "${column}" custom orderBy uses "${match[0]}" \u2014 only value, updated_at, source, actor, ord, SQL keywords and allowlisted functions`);
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
if (consumed !== orderBy.length || orderBy.trim() === "") {
|
|
2606
|
+
fail(`column "${column}" custom orderBy contains an unsupported character near "${orderBy.slice(consumed, consumed + 10)}"`);
|
|
2607
|
+
}
|
|
1471
2608
|
}
|
|
1472
2609
|
function stableJson(value) {
|
|
1473
2610
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
@@ -1499,8 +2636,8 @@ function canonicalTable(table) {
|
|
|
1499
2636
|
},
|
|
1500
2637
|
actions: emptyToNull(
|
|
1501
2638
|
Object.fromEntries(
|
|
1502
|
-
Object.entries(table.actions ?? {}).map(([
|
|
1503
|
-
|
|
2639
|
+
Object.entries(table.actions ?? {}).map(([name2, action]) => [
|
|
2640
|
+
name2,
|
|
1504
2641
|
{
|
|
1505
2642
|
description: action.description ?? null,
|
|
1506
2643
|
requires: action.requires?.source ?? null,
|
|
@@ -1554,6 +2691,72 @@ function computeDefinitionHash(definition) {
|
|
|
1554
2691
|
return createHash("sha256").update(HASH_DOMAIN).update(stableJson(canonicalizeDefinition(definition))).digest("hex");
|
|
1555
2692
|
}
|
|
1556
2693
|
|
|
2694
|
+
// src/canonical/composition.ts
|
|
2695
|
+
function localContract(definition) {
|
|
2696
|
+
return normalizeCanonical(localDefinitionSchema.parse(definition));
|
|
2697
|
+
}
|
|
2698
|
+
function flattenNamespaceImports(inputs) {
|
|
2699
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
2700
|
+
const visit = (value, depth) => {
|
|
2701
|
+
if (depth > 16 || definitions.size > 32) throw new OrmDefinitionError("namespace import graph is too large");
|
|
2702
|
+
const definition = localContract(localDefinitionSchema.parse(value));
|
|
2703
|
+
const previous = definitions.get(definition.namespace);
|
|
2704
|
+
if (previous) {
|
|
2705
|
+
if (stableJson(previous) !== stableJson(definition)) throw new OrmDefinitionError(`conflicting contracts for namespace '${definition.namespace}'`);
|
|
2706
|
+
}
|
|
2707
|
+
definitions.set(definition.namespace, definition);
|
|
2708
|
+
if (typeof value === "object" && value !== null && "imports" in value && Array.isArray(value.imports)) {
|
|
2709
|
+
for (const dependency of value.imports) visit(dependency, depth + 1);
|
|
2710
|
+
}
|
|
2711
|
+
};
|
|
2712
|
+
for (const input of inputs) visit(input, 0);
|
|
2713
|
+
return [...definitions.values()].sort((a, b) => a.namespace.localeCompare(b.namespace));
|
|
2714
|
+
}
|
|
2715
|
+
function composeNamespace(definition) {
|
|
2716
|
+
const all = [localContract(definition), ...definition.imports ?? []];
|
|
2717
|
+
const owners = /* @__PURE__ */ new Map();
|
|
2718
|
+
const namespaces = /* @__PURE__ */ new Set();
|
|
2719
|
+
const enums = /* @__PURE__ */ new Map();
|
|
2720
|
+
for (const owner of all) {
|
|
2721
|
+
if (namespaces.has(owner.namespace)) throw new OrmDefinitionError(`duplicate imported namespace '${owner.namespace}'`);
|
|
2722
|
+
namespaces.add(owner.namespace);
|
|
2723
|
+
for (const value of owner.enums) {
|
|
2724
|
+
const previous = enums.get(value.name);
|
|
2725
|
+
if (previous && stableJson(previous) !== stableJson(value)) {
|
|
2726
|
+
throw new OrmDefinitionError(`conflicting imported enum '${value.name}'`);
|
|
2727
|
+
}
|
|
2728
|
+
enums.set(value.name, value);
|
|
2729
|
+
}
|
|
2730
|
+
for (const relation of canonicalRelations(owner)) {
|
|
2731
|
+
if (owners.has(relation.table)) throw new OrmDefinitionError(`ambiguous imported relation '${relation.table}'`);
|
|
2732
|
+
owners.set(relation.table, owner);
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
for (const owner of definition.imports?.length ? all : []) {
|
|
2736
|
+
for (const relation of canonicalRelations(owner)) {
|
|
2737
|
+
for (const link of relation.links) {
|
|
2738
|
+
if (link.kind === "toPlatformUser") continue;
|
|
2739
|
+
const target = owners.get(link.target ?? "");
|
|
2740
|
+
if (!target || target.namespace !== (link.namespace ?? owner.namespace)) {
|
|
2741
|
+
throw new OrmDefinitionError(`link '${owner.namespace}/${relation.table}.${link.name}' has no matching imported target`);
|
|
2742
|
+
}
|
|
2743
|
+
if (target.namespace !== owner.namespace && !target.exports?.[link.target ?? ""]) {
|
|
2744
|
+
throw new OrmDefinitionError(`target '${target.namespace}/${link.target}' is not exported`);
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
return {
|
|
2750
|
+
owners,
|
|
2751
|
+
definition: {
|
|
2752
|
+
...localContract(definition),
|
|
2753
|
+
tables: all.flatMap((item) => item.tables),
|
|
2754
|
+
views: all.flatMap((item) => item.views ?? []),
|
|
2755
|
+
enums: [...enums.values()]
|
|
2756
|
+
}
|
|
2757
|
+
};
|
|
2758
|
+
}
|
|
2759
|
+
|
|
1557
2760
|
// src/authoring/namespace.ts
|
|
1558
2761
|
var NAMESPACE_CONFIG_BRAND = "seq-namespace-config";
|
|
1559
2762
|
function isSeqNamespaceConfig(value) {
|
|
@@ -1569,14 +2772,27 @@ function defineNamespace(input) {
|
|
|
1569
2772
|
namespace: input.namespace,
|
|
1570
2773
|
description: input.description,
|
|
1571
2774
|
capabilities: input.capabilities ?? {},
|
|
2775
|
+
imports: input.imports,
|
|
2776
|
+
exports: input.exports,
|
|
1572
2777
|
file
|
|
1573
2778
|
};
|
|
1574
2779
|
registerNamespaceConfig(config);
|
|
1575
2780
|
return config;
|
|
1576
2781
|
}
|
|
1577
2782
|
function serializeNamespace(input) {
|
|
1578
|
-
const { config
|
|
1579
|
-
const views = input.views ?? [];
|
|
2783
|
+
const { config } = input;
|
|
2784
|
+
const views = [...input.views ?? []];
|
|
2785
|
+
const tables = [...input.tables];
|
|
2786
|
+
for (const view of views) {
|
|
2787
|
+
for (const internal of view.config.resolved?.internalTables ?? []) {
|
|
2788
|
+
if (!tables.includes(internal)) tables.push(internal);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
for (const view of [...views]) {
|
|
2792
|
+
for (const internal of view.config.resolved?.internalViews ?? []) {
|
|
2793
|
+
if (!views.includes(internal)) views.push(internal);
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
1580
2796
|
const enums = input.enums ?? [];
|
|
1581
2797
|
const tablesByName = /* @__PURE__ */ new Map();
|
|
1582
2798
|
for (const table of tables) {
|
|
@@ -1602,15 +2818,15 @@ function serializeNamespace(input) {
|
|
|
1602
2818
|
}
|
|
1603
2819
|
const relationsByName = /* @__PURE__ */ new Map();
|
|
1604
2820
|
for (const relation of [...tables, ...views]) {
|
|
1605
|
-
const
|
|
1606
|
-
const existing = relationsByName.get(
|
|
2821
|
+
const name2 = relationName(relation);
|
|
2822
|
+
const existing = relationsByName.get(name2);
|
|
1607
2823
|
if (existing) {
|
|
1608
2824
|
throw new OrmDefinitionError(
|
|
1609
|
-
`duplicate relation "${
|
|
2825
|
+
`duplicate relation "${name2}" (defined as ${relationKind(existing)} in ${describeFile(existing)} and ${relationKind(relation)} in ${describeFile(relation)})`,
|
|
1610
2826
|
relation.file
|
|
1611
2827
|
);
|
|
1612
2828
|
}
|
|
1613
|
-
relationsByName.set(
|
|
2829
|
+
relationsByName.set(name2, relation);
|
|
1614
2830
|
}
|
|
1615
2831
|
const enumsByName = /* @__PURE__ */ new Map();
|
|
1616
2832
|
for (const ref of enums) {
|
|
@@ -1651,6 +2867,8 @@ function serializeNamespace(input) {
|
|
|
1651
2867
|
};
|
|
1652
2868
|
if (config.description !== void 0) candidate.description = config.description;
|
|
1653
2869
|
if (canonicalViews.length > 0) candidate.views = canonicalViews;
|
|
2870
|
+
if (config.imports?.length) candidate.imports = flattenNamespaceImports(config.imports);
|
|
2871
|
+
if (config.exports) candidate.exports = config.exports;
|
|
1654
2872
|
let parsed;
|
|
1655
2873
|
try {
|
|
1656
2874
|
parsed = canonicalDefinitionSchema.parse(candidate);
|
|
@@ -1660,7 +2878,9 @@ function serializeNamespace(input) {
|
|
|
1660
2878
|
}
|
|
1661
2879
|
throw error;
|
|
1662
2880
|
}
|
|
1663
|
-
|
|
2881
|
+
const normalized = normalizeCanonical(parsed);
|
|
2882
|
+
if (normalized.imports?.length) composeNamespace(normalized);
|
|
2883
|
+
return normalized;
|
|
1664
2884
|
}
|
|
1665
2885
|
function computeCanonicalHash(definition) {
|
|
1666
2886
|
return createHash("sha256").update(HASH_DOMAIN).update(stableJson(definition)).digest("hex");
|
|
@@ -1674,10 +2894,14 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1674
2894
|
policies: {},
|
|
1675
2895
|
actions: []
|
|
1676
2896
|
};
|
|
2897
|
+
if (table.config.displayName !== void 0) candidate.displayName = table.config.displayName;
|
|
1677
2898
|
if (table.config.description !== void 0) candidate.description = table.config.description;
|
|
1678
2899
|
if (table.config.renamedFrom !== void 0) candidate.renamedFrom = table.config.renamedFrom;
|
|
1679
2900
|
if (table.source !== void 0) candidate.source = table.source;
|
|
1680
2901
|
if (table.source !== void 0) {
|
|
2902
|
+
if (table.config.checks?.length) {
|
|
2903
|
+
throw new OrmDefinitionError(`synced table "${table.tableName}" is system-owned; checks are not allowed`, table.file);
|
|
2904
|
+
}
|
|
1681
2905
|
if (table.config.indexes !== void 0 && table.config.indexes.length > 0) {
|
|
1682
2906
|
throw new OrmDefinitionError(
|
|
1683
2907
|
`synced table "${table.tableName}" is system-owned; indexes are not allowed`,
|
|
@@ -1715,6 +2939,11 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1715
2939
|
if (builder.isPrimaryKey) columnLevelPk.push(column.columnName);
|
|
1716
2940
|
candidate.columns?.push(column);
|
|
1717
2941
|
}
|
|
2942
|
+
if (table.config.internal === true) candidate.internal = true;
|
|
2943
|
+
if (table.config.materializes !== void 0) candidate.materializes = table.config.materializes;
|
|
2944
|
+
if (table.config.cellMetadata === true && table.source !== void 0) {
|
|
2945
|
+
throw new OrmDefinitionError(`synced table "${table.tableName}" cannot track cell metadata \u2014 its DDL is owned by the sync`, table.file);
|
|
2946
|
+
}
|
|
1718
2947
|
if (table.config.primaryKey !== void 0) {
|
|
1719
2948
|
if (columnLevelPk.length > 0) {
|
|
1720
2949
|
throw new OrmDefinitionError(
|
|
@@ -1728,9 +2957,30 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1728
2957
|
} else if (columnLevelPk.length > 0) {
|
|
1729
2958
|
candidate.primaryKey = columnLevelPk;
|
|
1730
2959
|
}
|
|
2960
|
+
if (table.config.cellMetadata === true) {
|
|
2961
|
+
candidate.cellMetadata = true;
|
|
2962
|
+
const parts = table.config.cellRevisions === true ? ["at", "by", "rev"] : ["at", "by"];
|
|
2963
|
+
const tracked = trackedCellColumns({ columns: candidate.columns ?? [], primaryKey: candidate.primaryKey });
|
|
2964
|
+
for (const column of tracked) {
|
|
2965
|
+
for (const part of parts) {
|
|
2966
|
+
candidate.columns?.push({
|
|
2967
|
+
name: cellColumnName(column.name, part),
|
|
2968
|
+
columnName: cellColumnName(column.columnName, part),
|
|
2969
|
+
type: CELL_PART_TYPES[part],
|
|
2970
|
+
nullable: true,
|
|
2971
|
+
unique: false,
|
|
2972
|
+
deprecated: false,
|
|
2973
|
+
cell: { of: column.name, part }
|
|
2974
|
+
});
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
1731
2978
|
if (table.config.foreignKeys?.length) {
|
|
1732
2979
|
if (table.source) throw new OrmDefinitionError(`synced table "${table.tableName}" cannot enforce foreign keys`, table.file);
|
|
1733
2980
|
candidate.foreignKeys = table.config.foreignKeys.map((fk) => {
|
|
2981
|
+
if (typeof fk.target === "object" && fk.target.kind === "seq-external-object") {
|
|
2982
|
+
throw new OrmDefinitionError("physical foreign keys require a local target; use a cross-namespace ORM link", table.file);
|
|
2983
|
+
}
|
|
1734
2984
|
const target = resolveRelationRef({ ref: fk.target, fail: (message) => {
|
|
1735
2985
|
throw new OrmDefinitionError(message, table.file);
|
|
1736
2986
|
} });
|
|
@@ -1740,10 +2990,15 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1740
2990
|
columns: fk.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: `foreign key "${fk.name}" column` })),
|
|
1741
2991
|
target: target.tableName,
|
|
1742
2992
|
references: fk.references.map((ref) => resolveColumnRef({ relation: target, ref, role: `foreign key "${fk.name}" reference` })),
|
|
1743
|
-
...fk.deferrable ? { deferrable: fk.deferrable } : {}
|
|
2993
|
+
...fk.deferrable ? { deferrable: fk.deferrable } : {},
|
|
2994
|
+
...fk.onDelete ? { onDelete: fk.onDelete } : {},
|
|
2995
|
+
...fk.onUpdate ? { onUpdate: fk.onUpdate } : {}
|
|
1744
2996
|
};
|
|
1745
2997
|
});
|
|
1746
2998
|
}
|
|
2999
|
+
if (table.config.checks?.length) {
|
|
3000
|
+
candidate.checks = table.config.checks.map((check) => ({ name: check.name, expression: check.expression }));
|
|
3001
|
+
}
|
|
1747
3002
|
for (const index3 of table.config.indexes ?? []) {
|
|
1748
3003
|
const entry = {
|
|
1749
3004
|
columns: index3.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
|
|
@@ -1839,7 +3094,33 @@ function buildCanonicalView({
|
|
|
1839
3094
|
columns,
|
|
1840
3095
|
...primaryKey2 !== void 0 ? { primaryKey: primaryKey2 } : {},
|
|
1841
3096
|
links,
|
|
1842
|
-
policies: { read: toCanonicalPolicy(view.config.policies.read) }
|
|
3097
|
+
policies: { read: toCanonicalPolicy(view.config.policies.read) },
|
|
3098
|
+
...view.config.resolved !== void 0 ? { resolved: buildCanonicalResolved(view.config.resolved) } : {},
|
|
3099
|
+
...view.config.internal === true ? { internal: true } : {}
|
|
3100
|
+
};
|
|
3101
|
+
}
|
|
3102
|
+
function buildCanonicalResolved(resolved) {
|
|
3103
|
+
const policies = {};
|
|
3104
|
+
if (resolved.policies.write !== void 0) policies.write = toCanonicalPolicy(resolved.policies.write);
|
|
3105
|
+
if (resolved.policies.insert !== void 0) policies.insert = toCanonicalPolicy(resolved.policies.insert);
|
|
3106
|
+
if (resolved.policies.update !== void 0) policies.update = toCanonicalPolicy(resolved.policies.update);
|
|
3107
|
+
return {
|
|
3108
|
+
sources: resolved.sources.map((source) => ({
|
|
3109
|
+
name: source.name,
|
|
3110
|
+
kind: source.kind,
|
|
3111
|
+
table: source.table.tableName,
|
|
3112
|
+
columns: [...source.columns],
|
|
3113
|
+
...source.updatedAt !== void 0 ? { updatedAt: source.updatedAt } : {},
|
|
3114
|
+
...source.key !== void 0 ? { key: source.key } : {},
|
|
3115
|
+
...source.fetch !== void 0 ? { fetch: source.fetch } : {},
|
|
3116
|
+
...source.function !== void 0 ? { function: { ...source.function } } : {},
|
|
3117
|
+
...source.prefetchDefaults !== void 0 ? { prefetchDefaults: { ...source.prefetchDefaults } } : {},
|
|
3118
|
+
...source.writeback !== void 0 ? { writeback: { ...source.writeback, columns: [...source.writeback.columns] } } : {}
|
|
3119
|
+
})),
|
|
3120
|
+
resolve: resolved.rules.map((rule) => ({ ...rule, candidates: [...rule.candidates] })),
|
|
3121
|
+
deletedBy: [...resolved.deletedBy],
|
|
3122
|
+
...resolved.materialized ? { materialized: { ...resolved.materialized } } : {},
|
|
3123
|
+
policies
|
|
1843
3124
|
};
|
|
1844
3125
|
}
|
|
1845
3126
|
function buildCanonicalLink({
|
|
@@ -1856,6 +3137,9 @@ function buildCanonicalLink({
|
|
|
1856
3137
|
if (link.enforce && link.linkKind !== "toOne") {
|
|
1857
3138
|
return fail("sets enforce: true, which is only supported on toOne links");
|
|
1858
3139
|
}
|
|
3140
|
+
if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && !link.enforce) {
|
|
3141
|
+
return fail("sets a referential action without enforce: true");
|
|
3142
|
+
}
|
|
1859
3143
|
if (link.linkKind === "toPlatformUser") {
|
|
1860
3144
|
if (link.target !== void 0) return fail("is toPlatformUser and must not declare a target table");
|
|
1861
3145
|
return {
|
|
@@ -1867,6 +3151,19 @@ function buildCanonicalLink({
|
|
|
1867
3151
|
...link.description !== void 0 ? { description: link.description } : {}
|
|
1868
3152
|
};
|
|
1869
3153
|
}
|
|
3154
|
+
if (typeof link.target === "object" && link.target?.kind === "seq-external-object") {
|
|
3155
|
+
if (link.enforce) return fail("cannot enforce a cross-namespace physical foreign key; use an ORM link without enforce");
|
|
3156
|
+
return {
|
|
3157
|
+
name: linkName,
|
|
3158
|
+
kind: link.linkKind,
|
|
3159
|
+
namespace: link.target.namespace,
|
|
3160
|
+
target: link.target.table,
|
|
3161
|
+
on: link.linkKind === "toOne" ? resolveColumnRef({ relation, ref: link.on, role: "link on" }) : link.on,
|
|
3162
|
+
...link.references !== void 0 ? { references: link.linkKind === "toMany" ? resolveColumnRef({ relation, ref: link.references, role: "link references" }) : link.references } : {},
|
|
3163
|
+
enforce: false,
|
|
3164
|
+
...link.description !== void 0 ? { description: link.description } : {}
|
|
3165
|
+
};
|
|
3166
|
+
}
|
|
1870
3167
|
const target = resolveRelationRef({ ref: link.target, fail });
|
|
1871
3168
|
const targetName = relationName(target);
|
|
1872
3169
|
if (!relationsByName.has(targetName)) {
|
|
@@ -1884,6 +3181,8 @@ function buildCanonicalLink({
|
|
|
1884
3181
|
on: resolveColumnRef({ relation: owningSide, ref: link.on, role: `link "${linkName}" on` }),
|
|
1885
3182
|
...link.references !== void 0 ? { references: resolveColumnRef({ relation: referencedSide, ref: link.references, role: `link "${linkName}" references` }) } : {},
|
|
1886
3183
|
enforce: link.enforce,
|
|
3184
|
+
...link.onDelete !== void 0 ? { onDelete: link.onDelete } : {},
|
|
3185
|
+
...link.onUpdate !== void 0 ? { onUpdate: link.onUpdate } : {},
|
|
1887
3186
|
...link.description !== void 0 ? { description: link.description } : {}
|
|
1888
3187
|
};
|
|
1889
3188
|
}
|
|
@@ -1921,11 +3220,11 @@ function assertAcyclicViewDependencies(views) {
|
|
|
1921
3220
|
views.map((view) => [view.view, new Set(view.dependencies.filter((dependency) => viewNames.has(dependency)))])
|
|
1922
3221
|
);
|
|
1923
3222
|
while (remaining.size > 0) {
|
|
1924
|
-
const ready = [...remaining.entries()].filter(([, dependencies]) => [...dependencies].every((dependency) => !remaining.has(dependency))).map(([
|
|
3223
|
+
const ready = [...remaining.entries()].filter(([, dependencies]) => [...dependencies].every((dependency) => !remaining.has(dependency))).map(([name2]) => name2);
|
|
1925
3224
|
if (ready.length === 0) {
|
|
1926
3225
|
throw new OrmDefinitionError(`view dependencies form a cycle among: ${[...remaining.keys()].sort().join(", ")}`);
|
|
1927
3226
|
}
|
|
1928
|
-
for (const
|
|
3227
|
+
for (const name2 of ready) remaining.delete(name2);
|
|
1929
3228
|
}
|
|
1930
3229
|
}
|
|
1931
3230
|
function toCanonicalPolicy(policy) {
|
|
@@ -1971,8 +3270,8 @@ function syncedContractSurface({
|
|
|
1971
3270
|
namespace,
|
|
1972
3271
|
table,
|
|
1973
3272
|
columns: Object.fromEntries(
|
|
1974
|
-
Object.entries(columns).map(([
|
|
1975
|
-
|
|
3273
|
+
Object.entries(columns).map(([name2, column]) => [
|
|
3274
|
+
name2,
|
|
1976
3275
|
{ type: column.type, nullable: column.nullable, key: column.key }
|
|
1977
3276
|
])
|
|
1978
3277
|
),
|
|
@@ -1993,7 +3292,7 @@ function extractOrmSyncedTableContract({
|
|
|
1993
3292
|
}
|
|
1994
3293
|
if (table.source === void 0) {
|
|
1995
3294
|
throw new OrmDefinitionError(
|
|
1996
|
-
`table "${table.table}" is not a synced table \u2014 extractOrmSyncedTableContract requires source
|
|
3295
|
+
`table "${table.table}" is not a synced table \u2014 extractOrmSyncedTableContract requires a qualified source`
|
|
1997
3296
|
);
|
|
1998
3297
|
}
|
|
1999
3298
|
if (table.columns.length === 0) {
|
|
@@ -2140,6 +3439,53 @@ function purgeModuleCache(root) {
|
|
|
2140
3439
|
if (key.startsWith(prefix)) delete require2.cache[key];
|
|
2141
3440
|
}
|
|
2142
3441
|
}
|
|
3442
|
+
var sqlScalarSchema = z.enum(["text", "boolean", "integer", "bigint", "numeric", "double", "uuid", "date", "timestamptz"]);
|
|
3443
|
+
var name = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).max(63).refine((value) => !["__proto__", "prototype", "constructor"].includes(value), "reserved name");
|
|
3444
|
+
var base = z.object({
|
|
3445
|
+
name,
|
|
3446
|
+
namespace: name,
|
|
3447
|
+
language: z.literal("sql"),
|
|
3448
|
+
kind: z.literal("query"),
|
|
3449
|
+
requires: z.object({ capability: z.string().min(1).max(128) }).strict(),
|
|
3450
|
+
ontology: z.object({ read: z.array(z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*\/[A-Za-z_][A-Za-z0-9_]*$/)).min(1).max(256) }).strict(),
|
|
3451
|
+
maxRows: z.number().int().min(1).max(1e4).default(1e3)
|
|
3452
|
+
});
|
|
3453
|
+
var fixed = base.extend({
|
|
3454
|
+
parameters: z.record(name, z.object({ type: sqlScalarSchema, required: z.boolean().default(true) }).strict()).default({}),
|
|
3455
|
+
sql: z.string().min(1).max(65536)
|
|
3456
|
+
}).strict();
|
|
3457
|
+
var runtime = base.extend({ mode: z.literal("runtime") }).strict();
|
|
3458
|
+
var sqlOperationDefinitionSchema = z.union([fixed, runtime]);
|
|
3459
|
+
function defineOperation(input) {
|
|
3460
|
+
return sqlOperationDefinitionSchema.parse(input);
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
// src/authoring/catalog.ts
|
|
3464
|
+
var SEGMENT = /^[^./\\\s][^/\\]*[^./\\\s]$|^[^./\\\s]$/;
|
|
3465
|
+
function validateNamespaceCatalog(catalog, file) {
|
|
3466
|
+
const paths = /* @__PURE__ */ new Set([""]);
|
|
3467
|
+
function visit(folders, parent) {
|
|
3468
|
+
for (const [name2, children] of Object.entries(folders)) {
|
|
3469
|
+
if (!SEGMENT.test(name2) || ["__proto__", "constructor", "prototype"].includes(name2)) {
|
|
3470
|
+
throw new OrmDefinitionError(`invalid catalog folder name "${name2}"`, file);
|
|
3471
|
+
}
|
|
3472
|
+
const path = parent ? `${parent}/${name2}` : name2;
|
|
3473
|
+
paths.add(path);
|
|
3474
|
+
if (children !== null) {
|
|
3475
|
+
if (typeof children !== "object" || Array.isArray(children)) {
|
|
3476
|
+
throw new OrmDefinitionError(`catalog folder "${path}" must contain folders or null`, file);
|
|
3477
|
+
}
|
|
3478
|
+
visit(children, path);
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
visit(catalog.folders, "");
|
|
3483
|
+
for (const [object, path] of Object.entries(catalog.objects)) {
|
|
3484
|
+
if (!object || !paths.has(path)) {
|
|
3485
|
+
throw new OrmDefinitionError(`catalog object "${object}" references missing folder "${path}"`, file);
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
2143
3489
|
|
|
2144
3490
|
// src/codegen/types.ts
|
|
2145
3491
|
var WIRE_TYPES = {
|
|
@@ -2171,9 +3517,9 @@ function generateNamespaceTypes(definition) {
|
|
|
2171
3517
|
lines.push(` /** Synced from ${table.source} \u2014 read-only typed contract. */`);
|
|
2172
3518
|
}
|
|
2173
3519
|
lines.push(` ${table.table}: {`);
|
|
2174
|
-
for (const [
|
|
3520
|
+
for (const [name2, column] of columns) {
|
|
2175
3521
|
if (column.deprecated) continue;
|
|
2176
|
-
lines.push(` ${
|
|
3522
|
+
lines.push(` ${name2}: ${columnWireType(column)}`);
|
|
2177
3523
|
}
|
|
2178
3524
|
lines.push(" }");
|
|
2179
3525
|
}
|
|
@@ -2184,8 +3530,8 @@ function generateNamespaceTypes(definition) {
|
|
|
2184
3530
|
function actionTypeLines(definition) {
|
|
2185
3531
|
const actions = definition.tables.flatMap((table) => Object.entries(table.actions ?? {}));
|
|
2186
3532
|
const lines = [`export type ${actionsInterfaceName(definition.namespace)} = {`];
|
|
2187
|
-
for (const [
|
|
2188
|
-
lines.push(` ${
|
|
3533
|
+
for (const [name2, action] of actions.sort(([a], [b]) => a.localeCompare(b))) {
|
|
3534
|
+
lines.push(` ${name2}: { params: ${actionParamsType(action)}; result: ${actionResultType(action)} }`);
|
|
2189
3535
|
}
|
|
2190
3536
|
lines.push("}", "");
|
|
2191
3537
|
return lines;
|
|
@@ -2193,17 +3539,17 @@ function actionTypeLines(definition) {
|
|
|
2193
3539
|
function actionParamsType(action) {
|
|
2194
3540
|
const entries = Object.entries(action.params ?? {});
|
|
2195
3541
|
if (entries.length === 0) return "Record<never, never>";
|
|
2196
|
-
const fields = entries.map(([
|
|
2197
|
-
const
|
|
2198
|
-
return param.optional ? `${
|
|
3542
|
+
const fields = entries.map(([name2, param]) => {
|
|
3543
|
+
const base2 = WIRE_TYPES[param.type];
|
|
3544
|
+
return param.optional ? `${name2}?: ${base2} | null` : `${name2}: ${base2}`;
|
|
2199
3545
|
});
|
|
2200
3546
|
return `{ ${fields.join("; ")} }`;
|
|
2201
3547
|
}
|
|
2202
3548
|
function actionResultType(action) {
|
|
2203
3549
|
if (!action.returns) return "{ rowsAffected: number }";
|
|
2204
|
-
const columns = Object.entries(action.returns).map(([
|
|
2205
|
-
const
|
|
2206
|
-
return
|
|
3550
|
+
const columns = Object.entries(action.returns).map(([name2, type]) => {
|
|
3551
|
+
const base2 = WIRE_TYPES[type];
|
|
3552
|
+
return base2 === "unknown" ? `${name2}: ${base2}` : `${name2}: ${base2} | null`;
|
|
2207
3553
|
});
|
|
2208
3554
|
return `{ rows: Array<{ ${columns.join("; ")} }> }`;
|
|
2209
3555
|
}
|
|
@@ -2217,9 +3563,9 @@ function pascalCase(namespace) {
|
|
|
2217
3563
|
return namespace.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
2218
3564
|
}
|
|
2219
3565
|
function columnWireType(column) {
|
|
2220
|
-
const
|
|
3566
|
+
const base2 = WIRE_TYPES[column.type];
|
|
2221
3567
|
const notNull = column.primary_key === true || column.nullable === false;
|
|
2222
|
-
return notNull ||
|
|
3568
|
+
return notNull || base2 === "unknown" ? base2 : `${base2} | null`;
|
|
2223
3569
|
}
|
|
2224
3570
|
|
|
2225
3571
|
// src/predicate/ast.ts
|
|
@@ -2337,7 +3683,7 @@ var OPERATOR_PATTERN = /^(!=|<=|>=|<>|=|<|>)/;
|
|
|
2337
3683
|
var IDENTIFIER_START = /[a-z_]/;
|
|
2338
3684
|
var IDENTIFIER_CHAR = /[a-z0-9_.-]/;
|
|
2339
3685
|
var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
|
|
2340
|
-
function
|
|
3686
|
+
function tokenize2(source) {
|
|
2341
3687
|
const tokens = [];
|
|
2342
3688
|
let position = 0;
|
|
2343
3689
|
while (position < source.length) {
|
|
@@ -2411,7 +3757,7 @@ function readString(source, start) {
|
|
|
2411
3757
|
}
|
|
2412
3758
|
throw new PredicateParseError("unterminated string literal", start);
|
|
2413
3759
|
}
|
|
2414
|
-
var
|
|
3760
|
+
var Parser2 = class {
|
|
2415
3761
|
constructor(tokens) {
|
|
2416
3762
|
this.tokens = tokens;
|
|
2417
3763
|
}
|
|
@@ -2543,7 +3889,7 @@ var Parser = class {
|
|
|
2543
3889
|
function parsePredicate(source) {
|
|
2544
3890
|
const trimmed = source.trim();
|
|
2545
3891
|
if (trimmed.length === 0) throw new PredicateParseError("empty predicate", 0);
|
|
2546
|
-
return { source: trimmed, ast: new
|
|
3892
|
+
return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
|
|
2547
3893
|
}
|
|
2548
3894
|
|
|
2549
3895
|
// src/schema/predicates.ts
|
|
@@ -2628,8 +3974,8 @@ var actionSchema = z.strictObject({
|
|
|
2628
3974
|
}
|
|
2629
3975
|
}
|
|
2630
3976
|
});
|
|
2631
|
-
function extractNamedParams(
|
|
2632
|
-
const withoutStrings =
|
|
3977
|
+
function extractNamedParams(sql7) {
|
|
3978
|
+
const withoutStrings = sql7.replace(QUOTED_STRING_PATTERN, "''");
|
|
2633
3979
|
const names = [...withoutStrings.matchAll(NAMED_PARAM_PATTERN)].map((match) => match[1]);
|
|
2634
3980
|
return [...new Set(names)];
|
|
2635
3981
|
}
|
|
@@ -2684,7 +4030,7 @@ function executesIssues(executes, declaredParams, hasReturns) {
|
|
|
2684
4030
|
if (hasReturns && last && !RETURNING_PATTERN.test(last.replace(QUOTED_STRING_PATTERN, "''"))) {
|
|
2685
4031
|
issues.push("the last executes statement must use RETURNING to populate the declared returns columns");
|
|
2686
4032
|
}
|
|
2687
|
-
const undeclared = extractNamedParams(statements.join("\n")).filter((
|
|
4033
|
+
const undeclared = extractNamedParams(statements.join("\n")).filter((name2) => !declaredParams.has(name2));
|
|
2688
4034
|
if (undeclared.length > 0) issues.push(`executes references undeclared params: ${undeclared.join(", ")}`);
|
|
2689
4035
|
return issues;
|
|
2690
4036
|
}
|
|
@@ -2696,17 +4042,6 @@ var SQL_TYPE_NAMES = {
|
|
|
2696
4042
|
};
|
|
2697
4043
|
var sqlTypeName = (type) => SQL_TYPE_NAMES[type] ?? type;
|
|
2698
4044
|
|
|
2699
|
-
// src/compiler/sql.ts
|
|
2700
|
-
var CLAIMS_SETTING = "request.jwt.claims";
|
|
2701
|
-
var CAPABILITIES_CLAIM_KEY = "capabilities";
|
|
2702
|
-
var ROLES_CLAIM_KEY = "roles";
|
|
2703
|
-
var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
2704
|
-
var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
2705
|
-
var CLAIMS_SOURCE = `nullif(current_setting('${CLAIMS_SETTING}', true), '')::jsonb`;
|
|
2706
|
-
var claimExpression = (key) => `(${CLAIMS_SOURCE} ->> ${quoteLiteral(key)})`;
|
|
2707
|
-
var capabilityExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${CAPABILITIES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
|
|
2708
|
-
var roleExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${ROLES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
|
|
2709
|
-
|
|
2710
4045
|
// src/compiler/predicates.ts
|
|
2711
4046
|
var SQL_COMPARE_OPS = {
|
|
2712
4047
|
"=": "=",
|
|
@@ -2770,24 +4105,24 @@ var PARAM_SCHEMAS = {
|
|
|
2770
4105
|
function buildActionParamsSchema(action) {
|
|
2771
4106
|
const shape = {};
|
|
2772
4107
|
const entries = Object.entries(action.params ?? {});
|
|
2773
|
-
for (const [
|
|
2774
|
-
shape[
|
|
4108
|
+
for (const [name2, param] of entries) {
|
|
4109
|
+
shape[name2] = param.optional ? PARAM_SCHEMAS[param.type].nullish() : PARAM_SCHEMAS[param.type];
|
|
2775
4110
|
}
|
|
2776
4111
|
return z.strictObject(shape).superRefine((obj, ctx) => {
|
|
2777
|
-
for (const [
|
|
2778
|
-
if (!param.optional && param.type === "jsonb" && obj[
|
|
2779
|
-
ctx.addIssue({ code: "custom", message: "Required", path: [
|
|
4112
|
+
for (const [name2, param] of entries) {
|
|
4113
|
+
if (!param.optional && param.type === "jsonb" && obj[name2] === void 0) {
|
|
4114
|
+
ctx.addIssue({ code: "custom", message: "Required", path: [name2] });
|
|
2780
4115
|
}
|
|
2781
4116
|
}
|
|
2782
4117
|
});
|
|
2783
4118
|
}
|
|
2784
4119
|
var actionParamOrder = (action) => Object.keys(action.params ?? {});
|
|
2785
|
-
var actionParamArg = (
|
|
4120
|
+
var actionParamArg = (name2) => `p_${name2}`;
|
|
2786
4121
|
function compileActionStatements(action, renderParam = actionParamArg) {
|
|
2787
4122
|
return actionStatementSources(action.executes).map((statement) => compileStatement(statement, renderParam));
|
|
2788
4123
|
}
|
|
2789
|
-
function compileStatement(
|
|
2790
|
-
const compiled =
|
|
4124
|
+
function compileStatement(sql7, renderParam) {
|
|
4125
|
+
const compiled = sql7.replace(TOKEN_PATTERN, (token, claimKey, capabilityKey, paramName) => {
|
|
2791
4126
|
if (claimKey !== void 0) {
|
|
2792
4127
|
if (!CLAIM_KEY_PATTERN.test(claimKey)) throw new OrmDefinitionError(`invalid claim key in executes: ${token}`);
|
|
2793
4128
|
return claimExpression(claimKey);
|
|
@@ -2833,10 +4168,12 @@ function compilePredIrToSql(pred, options = {}) {
|
|
|
2833
4168
|
case "cmp":
|
|
2834
4169
|
return compileCmp(pred.op, pred.column, pred.value, options);
|
|
2835
4170
|
case "capability":
|
|
2836
|
-
return `(${capabilityExpression(pred.key)})`;
|
|
4171
|
+
return `(${capabilityExpression(pred.key, pred.namespace ?? options.namespace)})`;
|
|
2837
4172
|
case "role":
|
|
2838
|
-
return `(${roleExpression(pred.key)})`;
|
|
4173
|
+
return `(${roleExpression(pred.key, pred.namespace ?? options.namespace)})`;
|
|
2839
4174
|
case "exists":
|
|
4175
|
+
if (options.exists) return options.exists(pred);
|
|
4176
|
+
throw new OrmDefinitionError("synced view read policy cannot use 'exists' without a relationship resolver");
|
|
2840
4177
|
case "aggPredicate":
|
|
2841
4178
|
case "dynamicFilter":
|
|
2842
4179
|
throw new OrmDefinitionError(
|
|
@@ -2852,10 +4189,11 @@ function physicalColumn(logical, options) {
|
|
|
2852
4189
|
return options.columnNames?.[logical] ?? logical;
|
|
2853
4190
|
}
|
|
2854
4191
|
function compileCmp(op, column, value, options) {
|
|
2855
|
-
const
|
|
2856
|
-
const declared = options.columnTypes?.[
|
|
4192
|
+
const physical2 = physicalColumn(column, options);
|
|
4193
|
+
const declared = options.columnTypes?.[physical2] ?? options.columnTypes?.[column];
|
|
2857
4194
|
const isEnum = typeof declared === "object" && declared !== null;
|
|
2858
|
-
const
|
|
4195
|
+
const qualified = options.alias ? `${quoteIdentifier(options.alias)}.${quoteIdentifier(physical2)}` : quoteIdentifier(physical2);
|
|
4196
|
+
const col = isEnum ? `${qualified}::text` : qualified;
|
|
2859
4197
|
const columnType = isEnum ? "text" : declared;
|
|
2860
4198
|
switch (op) {
|
|
2861
4199
|
case "eq":
|
|
@@ -2899,11 +4237,11 @@ function compileEquality(col, value, columnType, negated, options) {
|
|
|
2899
4237
|
function compileValue(value, columnType, _options) {
|
|
2900
4238
|
switch (value.kind) {
|
|
2901
4239
|
case "literal": {
|
|
2902
|
-
const
|
|
2903
|
-
if (
|
|
2904
|
-
if (typeof
|
|
2905
|
-
if (typeof
|
|
2906
|
-
return String(
|
|
4240
|
+
const literal2 = value.value;
|
|
4241
|
+
if (literal2 === null) return "NULL";
|
|
4242
|
+
if (typeof literal2 === "string") return quoteLiteral(literal2);
|
|
4243
|
+
if (typeof literal2 === "boolean") return literal2 ? "TRUE" : "FALSE";
|
|
4244
|
+
return String(literal2);
|
|
2907
4245
|
}
|
|
2908
4246
|
case "claim": {
|
|
2909
4247
|
const expression = claimExpression(value.key);
|
|
@@ -2939,22 +4277,22 @@ function returnProjection(returns, lastStatement) {
|
|
|
2939
4277
|
const projection = Object.keys(returns).map(quoteIdentifier).join(", ");
|
|
2940
4278
|
return `WITH orm_returning AS (${lastStatement}) SELECT ${projection} FROM orm_returning`;
|
|
2941
4279
|
}
|
|
2942
|
-
var allActions = (definition) => definition.tables.flatMap((table) => Object.entries(table.actions ?? {}).map(([
|
|
4280
|
+
var allActions = (definition) => definition.tables.flatMap((table) => Object.entries(table.actions ?? {}).map(([name2, action]) => ({ name: name2, action }))).sort((a, b) => a.name.localeCompare(b.name));
|
|
2943
4281
|
function actionFunctionStatements(definition) {
|
|
2944
4282
|
const schema = ormSchemaName(definition.namespace);
|
|
2945
|
-
return [dropAllActionFunctions(schema), ...allActions(definition).flatMap(({ name, action }) => createActionFunction(definition,
|
|
4283
|
+
return [dropAllActionFunctions(schema), ...allActions(definition).flatMap(({ name: name2, action }) => createActionFunction(definition, name2, action))];
|
|
2946
4284
|
}
|
|
2947
4285
|
function actionPrepareChecks(definition) {
|
|
2948
4286
|
const actions = allActions(definition);
|
|
2949
4287
|
if (actions.length === 0) return [];
|
|
2950
4288
|
const statements = [`SET LOCAL search_path TO ${quoteIdentifier(ormSchemaName(definition.namespace))}`];
|
|
2951
|
-
for (const { name, action } of actions) {
|
|
4289
|
+
for (const { name: name2, action } of actions) {
|
|
2952
4290
|
const order = actionParamOrder(action);
|
|
2953
4291
|
const signature = order.length > 0 ? ` (${order.map((param) => sqlTypeName(action.params[param].type)).join(", ")})` : "";
|
|
2954
4292
|
const compiled = compileActionStatements(action, (param) => `$${order.indexOf(param) + 1}`);
|
|
2955
|
-
compiled.forEach((
|
|
2956
|
-
const checked = action.returns && index3 === compiled.length - 1 ? returnProjection(action.returns,
|
|
2957
|
-
statements.push(`PREPARE ${quoteIdentifier(`orm_check_${
|
|
4293
|
+
compiled.forEach((sql7, index3) => {
|
|
4294
|
+
const checked = action.returns && index3 === compiled.length - 1 ? returnProjection(action.returns, sql7) : sql7;
|
|
4295
|
+
statements.push(`PREPARE ${quoteIdentifier(`orm_check_${name2}_${index3}`)}${signature} AS ${checked}`);
|
|
2958
4296
|
});
|
|
2959
4297
|
}
|
|
2960
4298
|
return statements;
|
|
@@ -2975,9 +4313,9 @@ BEGIN
|
|
|
2975
4313
|
END
|
|
2976
4314
|
$orm_reconcile_actions$`;
|
|
2977
4315
|
}
|
|
2978
|
-
function createActionFunction(definition,
|
|
4316
|
+
function createActionFunction(definition, name2, action) {
|
|
2979
4317
|
const schema = quoteIdentifier(ormSchemaName(definition.namespace));
|
|
2980
|
-
const fn = `${schema}.${quoteIdentifier(actionFunctionName(
|
|
4318
|
+
const fn = `${schema}.${quoteIdentifier(actionFunctionName(name2))}`;
|
|
2981
4319
|
const order = actionParamOrder(action);
|
|
2982
4320
|
const signature = order.map((paramName) => `${quoteIdentifier(actionParamArg(paramName))} ${sqlTypeName(action.params[paramName].type)}`).join(", ");
|
|
2983
4321
|
const argTypes = order.map((paramName) => sqlTypeName(action.params[paramName].type)).join(", ");
|
|
@@ -2997,7 +4335,7 @@ AS $orm_action$
|
|
|
2997
4335
|
-- columns; resolve unqualified names to the column.
|
|
2998
4336
|
#variable_conflict use_column
|
|
2999
4337
|
BEGIN
|
|
3000
|
-
${[functionGuards(definition,
|
|
4338
|
+
${[functionGuards(definition, name2, action), ...leading, ` RETURN QUERY ${returnProjection(action.returns, last)};`].filter(Boolean).join("\n")}
|
|
3001
4339
|
END
|
|
3002
4340
|
$orm_action$`
|
|
3003
4341
|
) : (
|
|
@@ -3011,7 +4349,7 @@ AS $orm_action$
|
|
|
3011
4349
|
DECLARE
|
|
3012
4350
|
v_count bigint;
|
|
3013
4351
|
BEGIN
|
|
3014
|
-
${[functionGuards(definition,
|
|
4352
|
+
${[functionGuards(definition, name2, action), ...leading, ` ${last};`].filter(Boolean).join("\n")}
|
|
3015
4353
|
GET DIAGNOSTICS v_count = ROW_COUNT;
|
|
3016
4354
|
RETURN v_count;
|
|
3017
4355
|
END
|
|
@@ -3023,13 +4361,13 @@ $orm_action$`
|
|
|
3023
4361
|
`GRANT EXECUTE ON FUNCTION ${fn}(${argTypes}) TO ${role}`
|
|
3024
4362
|
];
|
|
3025
4363
|
}
|
|
3026
|
-
function functionGuards(definition,
|
|
4364
|
+
function functionGuards(definition, name2, action) {
|
|
3027
4365
|
const lines = [];
|
|
3028
4366
|
const requires = compileActionRequires(action);
|
|
3029
4367
|
if (requires) {
|
|
3030
4368
|
lines.push(
|
|
3031
4369
|
` IF NOT (${requires}) THEN`,
|
|
3032
|
-
` RAISE EXCEPTION 'action ${
|
|
4370
|
+
` RAISE EXCEPTION 'action ${name2} requires a grant the caller does not hold' USING ERRCODE = ${quoteLiteral(ACTION_REQUIRES_DENIED_SQLSTATE)};`,
|
|
3033
4371
|
` END IF;`
|
|
3034
4372
|
);
|
|
3035
4373
|
}
|
|
@@ -3081,7 +4419,7 @@ var tableFileSchema = z.strictObject({
|
|
|
3081
4419
|
table: identifierSchema,
|
|
3082
4420
|
description: z.string().optional(),
|
|
3083
4421
|
columns: z.record(identifierSchema, columnSchema).optional(),
|
|
3084
|
-
source: z.string().regex(SYNCED_SOURCE_PATTERN, "source must
|
|
4422
|
+
source: z.string().regex(SYNCED_SOURCE_PATTERN, "source must be a qualified lowercase schema.table identifier").optional(),
|
|
3085
4423
|
links: z.record(identifierSchema, linkSchema).optional(),
|
|
3086
4424
|
indexes: z.array(indexSchema).optional(),
|
|
3087
4425
|
policies: policiesSchema.optional(),
|
|
@@ -3090,12 +4428,12 @@ var tableFileSchema = z.strictObject({
|
|
|
3090
4428
|
if (RESERVED_TABLE_NAMES.has(table.table)) {
|
|
3091
4429
|
ctx.addIssue({ code: "custom", message: `'${table.table}' is a reserved table name`, path: ["table"] });
|
|
3092
4430
|
}
|
|
3093
|
-
for (const
|
|
3094
|
-
if (RESERVED_COLUMN_NAMES.has(
|
|
4431
|
+
for (const name2 of Object.keys(table.columns ?? {})) {
|
|
4432
|
+
if (RESERVED_COLUMN_NAMES.has(name2)) {
|
|
3095
4433
|
ctx.addIssue({
|
|
3096
4434
|
code: "custom",
|
|
3097
|
-
message: `column '${
|
|
3098
|
-
path: ["columns",
|
|
4435
|
+
message: `column '${name2}' collides with the Data API's reserved query parameters (select, order, limit, offset) \u2014 it could never be filtered`,
|
|
4436
|
+
path: ["columns", name2]
|
|
3099
4437
|
});
|
|
3100
4438
|
}
|
|
3101
4439
|
}
|
|
@@ -3128,41 +4466,41 @@ function validateSyncedTable(table, ctx) {
|
|
|
3128
4466
|
const columns = table.columns ?? {};
|
|
3129
4467
|
const names = Object.keys(columns);
|
|
3130
4468
|
if (names.length === 0) return;
|
|
3131
|
-
const primaryKeys = names.filter((
|
|
4469
|
+
const primaryKeys = names.filter((name2) => columns[name2]?.primary_key);
|
|
3132
4470
|
if (primaryKeys.length !== 1) {
|
|
3133
4471
|
ctx.addIssue({ code: "custom", message: "exactly one column must be primary_key", path: ["columns"] });
|
|
3134
4472
|
}
|
|
3135
4473
|
validateRenames(columns, names, ctx);
|
|
3136
|
-
for (const
|
|
3137
|
-
const column = columns[
|
|
4474
|
+
for (const name2 of names) {
|
|
4475
|
+
const column = columns[name2];
|
|
3138
4476
|
if (!column) continue;
|
|
3139
4477
|
const cast = column.cast ? castParts(column.cast) : null;
|
|
3140
|
-
if (column.cast && cast?.column !==
|
|
4478
|
+
if (column.cast && cast?.column !== name2) {
|
|
3141
4479
|
ctx.addIssue({
|
|
3142
4480
|
code: "custom",
|
|
3143
4481
|
message: "cast must reference the column it is declared on",
|
|
3144
|
-
path: ["columns",
|
|
4482
|
+
path: ["columns", name2, "cast"]
|
|
3145
4483
|
});
|
|
3146
4484
|
}
|
|
3147
4485
|
if (cast && cast.type !== column.type) {
|
|
3148
4486
|
ctx.addIssue({
|
|
3149
4487
|
code: "custom",
|
|
3150
4488
|
message: `cast target '${cast.type}' must match the declared type '${column.type}'`,
|
|
3151
|
-
path: ["columns",
|
|
4489
|
+
path: ["columns", name2, "cast"]
|
|
3152
4490
|
});
|
|
3153
4491
|
}
|
|
3154
4492
|
}
|
|
3155
4493
|
}
|
|
3156
4494
|
function validateRenames(columns, names, ctx) {
|
|
3157
4495
|
const renameSources = /* @__PURE__ */ new Map();
|
|
3158
|
-
for (const
|
|
3159
|
-
const renamedFrom = columns[
|
|
4496
|
+
for (const name2 of names) {
|
|
4497
|
+
const renamedFrom = columns[name2]?.renamed_from;
|
|
3160
4498
|
if (!renamedFrom) continue;
|
|
3161
|
-
if (renamedFrom ===
|
|
4499
|
+
if (renamedFrom === name2 || names.includes(renamedFrom)) {
|
|
3162
4500
|
ctx.addIssue({
|
|
3163
4501
|
code: "custom",
|
|
3164
4502
|
message: "renamed_from must name a column that no longer exists",
|
|
3165
|
-
path: ["columns",
|
|
4503
|
+
path: ["columns", name2, "renamed_from"]
|
|
3166
4504
|
});
|
|
3167
4505
|
}
|
|
3168
4506
|
const claimed = renameSources.get(renamedFrom);
|
|
@@ -3170,10 +4508,10 @@ function validateRenames(columns, names, ctx) {
|
|
|
3170
4508
|
ctx.addIssue({
|
|
3171
4509
|
code: "custom",
|
|
3172
4510
|
message: `renamed_from '${renamedFrom}' is already claimed by '${claimed}'`,
|
|
3173
|
-
path: ["columns",
|
|
4511
|
+
path: ["columns", name2, "renamed_from"]
|
|
3174
4512
|
});
|
|
3175
4513
|
}
|
|
3176
|
-
renameSources.set(renamedFrom,
|
|
4514
|
+
renameSources.set(renamedFrom, name2);
|
|
3177
4515
|
}
|
|
3178
4516
|
}
|
|
3179
4517
|
function validateColumnTable(table, ctx) {
|
|
@@ -3183,27 +4521,27 @@ function validateColumnTable(table, ctx) {
|
|
|
3183
4521
|
ctx.addIssue({ code: "custom", message: "a table must declare columns or a source", path: ["columns"] });
|
|
3184
4522
|
return;
|
|
3185
4523
|
}
|
|
3186
|
-
const primaryKeys = names.filter((
|
|
4524
|
+
const primaryKeys = names.filter((name2) => columns[name2]?.primary_key);
|
|
3187
4525
|
if (primaryKeys.length !== 1) {
|
|
3188
4526
|
ctx.addIssue({ code: "custom", message: "exactly one column must be primary_key", path: ["columns"] });
|
|
3189
4527
|
}
|
|
3190
4528
|
validateRenames(columns, names, ctx);
|
|
3191
|
-
for (const
|
|
3192
|
-
const column = columns[
|
|
4529
|
+
for (const name2 of names) {
|
|
4530
|
+
const column = columns[name2];
|
|
3193
4531
|
if (!column) continue;
|
|
3194
4532
|
const cast = column.cast ? castParts(column.cast) : null;
|
|
3195
|
-
if (column.cast && cast?.column !==
|
|
4533
|
+
if (column.cast && cast?.column !== name2) {
|
|
3196
4534
|
ctx.addIssue({
|
|
3197
4535
|
code: "custom",
|
|
3198
4536
|
message: "cast must reference the column it is declared on",
|
|
3199
|
-
path: ["columns",
|
|
4537
|
+
path: ["columns", name2, "cast"]
|
|
3200
4538
|
});
|
|
3201
4539
|
}
|
|
3202
4540
|
if (cast && cast.type !== column.type) {
|
|
3203
4541
|
ctx.addIssue({
|
|
3204
4542
|
code: "custom",
|
|
3205
4543
|
message: `cast target '${cast.type}' must match the declared type '${column.type}'`,
|
|
3206
|
-
path: ["columns",
|
|
4544
|
+
path: ["columns", name2, "cast"]
|
|
3207
4545
|
});
|
|
3208
4546
|
}
|
|
3209
4547
|
}
|
|
@@ -3228,22 +4566,22 @@ function validateColumnTable(table, ctx) {
|
|
|
3228
4566
|
}
|
|
3229
4567
|
function validateLinks(table, ctx) {
|
|
3230
4568
|
const columnNames = Object.keys(table.columns ?? {});
|
|
3231
|
-
for (const [
|
|
4569
|
+
for (const [name2, link] of Object.entries(table.links ?? {})) {
|
|
3232
4570
|
if (columnNames.length > 0 && !columnNames.includes(link.on)) {
|
|
3233
|
-
ctx.addIssue({ code: "custom", message: `link references unknown column '${link.on}'`, path: ["links",
|
|
4571
|
+
ctx.addIssue({ code: "custom", message: `link references unknown column '${link.on}'`, path: ["links", name2, "on"] });
|
|
3234
4572
|
}
|
|
3235
4573
|
if (link.enforce && link.to.includes(".")) {
|
|
3236
4574
|
ctx.addIssue({
|
|
3237
4575
|
code: "custom",
|
|
3238
4576
|
message: "enforce is only valid for links inside the namespace; cross-boundary links stay soft",
|
|
3239
|
-
path: ["links",
|
|
4577
|
+
path: ["links", name2, "enforce"]
|
|
3240
4578
|
});
|
|
3241
4579
|
}
|
|
3242
4580
|
if (link.enforce && table.source) {
|
|
3243
4581
|
ctx.addIssue({
|
|
3244
4582
|
code: "custom",
|
|
3245
4583
|
message: "a synced table cannot enforce foreign keys \u2014 its DDL is owned by the sync",
|
|
3246
|
-
path: ["links",
|
|
4584
|
+
path: ["links", name2, "enforce"]
|
|
3247
4585
|
});
|
|
3248
4586
|
}
|
|
3249
4587
|
}
|
|
@@ -3258,32 +4596,32 @@ function validateDerivedNameLengths(table, ctx) {
|
|
|
3258
4596
|
});
|
|
3259
4597
|
}
|
|
3260
4598
|
for (const [position, index3] of (table.indexes ?? []).entries()) {
|
|
3261
|
-
const
|
|
3262
|
-
if (
|
|
4599
|
+
const name2 = indexName(table.table, index3);
|
|
4600
|
+
if (name2.length > MAX_IDENTIFIER_LENGTH) {
|
|
3263
4601
|
ctx.addIssue({
|
|
3264
4602
|
code: "custom",
|
|
3265
|
-
message: `derived index name '${
|
|
4603
|
+
message: `derived index name '${name2}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 set a shorter explicit name`,
|
|
3266
4604
|
path: ["indexes", position]
|
|
3267
4605
|
});
|
|
3268
4606
|
}
|
|
3269
4607
|
}
|
|
3270
|
-
for (const [
|
|
3271
|
-
const constraint = foreignKeyName(table.table,
|
|
4608
|
+
for (const [name2, link] of Object.entries(table.links ?? {})) {
|
|
4609
|
+
const constraint = foreignKeyName(table.table, name2);
|
|
3272
4610
|
if (link.enforce && constraint.length > MAX_IDENTIFIER_LENGTH) {
|
|
3273
4611
|
ctx.addIssue({
|
|
3274
4612
|
code: "custom",
|
|
3275
4613
|
message: `derived foreign-key name '${constraint}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 shorten the link or table name`,
|
|
3276
|
-
path: ["links",
|
|
4614
|
+
path: ["links", name2]
|
|
3277
4615
|
});
|
|
3278
4616
|
}
|
|
3279
4617
|
}
|
|
3280
|
-
for (const
|
|
3281
|
-
const fnName = `action_${
|
|
4618
|
+
for (const name2 of Object.keys(table.actions ?? {})) {
|
|
4619
|
+
const fnName = `action_${name2}`;
|
|
3282
4620
|
if (fnName.length > MAX_IDENTIFIER_LENGTH) {
|
|
3283
4621
|
ctx.addIssue({
|
|
3284
4622
|
code: "custom",
|
|
3285
4623
|
message: `derived action function name '${fnName}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 shorten the action name`,
|
|
3286
|
-
path: ["actions",
|
|
4624
|
+
path: ["actions", name2]
|
|
3287
4625
|
});
|
|
3288
4626
|
}
|
|
3289
4627
|
}
|
|
@@ -3335,14 +4673,14 @@ function comparisonTypeIssue(left, right, columns) {
|
|
|
3335
4673
|
return null;
|
|
3336
4674
|
}
|
|
3337
4675
|
const column = left.kind === "column" ? left : right.kind === "column" ? right : null;
|
|
3338
|
-
const
|
|
3339
|
-
if (!column || !
|
|
4676
|
+
const literal2 = left.kind === "literal" ? left : right.kind === "literal" ? right : null;
|
|
4677
|
+
if (!column || !literal2 || literal2.value === null) return null;
|
|
3340
4678
|
const columnGroup = groupOf(column);
|
|
3341
4679
|
if (!columnGroup) return null;
|
|
3342
|
-
if (typeof
|
|
3343
|
-
const literalGroup = typeof
|
|
4680
|
+
if (typeof literal2.value === "string") return null;
|
|
4681
|
+
const literalGroup = typeof literal2.value === "number" ? "numeric" : "boolean";
|
|
3344
4682
|
if (literalGroup !== columnGroup) {
|
|
3345
|
-
return `policy compares '${column.name}' (${columns[column.name].type}) with a ${typeof
|
|
4683
|
+
return `policy compares '${column.name}' (${columns[column.name].type}) with a ${typeof literal2.value} literal \u2014 incomparable types`;
|
|
3346
4684
|
}
|
|
3347
4685
|
return null;
|
|
3348
4686
|
}
|
|
@@ -3376,7 +4714,7 @@ function policyStatements(schema, table) {
|
|
|
3376
4714
|
const policyName2 = (suffix) => quoteIdentifier(policyName(table.table, suffix));
|
|
3377
4715
|
const statements = POLICY_SUFFIXES.map((suffix) => `DROP POLICY IF EXISTS ${policyName2(suffix)} ON ${qualified}`);
|
|
3378
4716
|
const columnTypes = Object.fromEntries(
|
|
3379
|
-
Object.entries(table.columns ?? {}).map(([
|
|
4717
|
+
Object.entries(table.columns ?? {}).map(([name2, column]) => [name2, column.type])
|
|
3380
4718
|
);
|
|
3381
4719
|
const read = table.policies?.read;
|
|
3382
4720
|
if (read) {
|
|
@@ -3428,8 +4766,8 @@ function syncedViewStatements({
|
|
|
3428
4766
|
const backing = backingMap.get(table.table);
|
|
3429
4767
|
if (!backing) continue;
|
|
3430
4768
|
const viewName = `${schema}.${quoteIdentifier(table.table)}`;
|
|
3431
|
-
const
|
|
3432
|
-
const physicalTarget = `${quoteIdentifier(
|
|
4769
|
+
const physical2 = splitPhysicalRelation(backing.physicalRelation);
|
|
4770
|
+
const physicalTarget = `${quoteIdentifier(physical2.schema)}.${quoteIdentifier(physical2.table)}`;
|
|
3433
4771
|
const columnTypes = mergeSyncedViewColumnTypes({
|
|
3434
4772
|
table,
|
|
3435
4773
|
canonicalColumnTypes: columnTypesByTable?.get(table.table),
|
|
@@ -3439,12 +4777,19 @@ function syncedViewStatements({
|
|
|
3439
4777
|
const readPolicyIr = readPolicyIrByTable?.get(table.table);
|
|
3440
4778
|
const readPolicy = table.policies?.read;
|
|
3441
4779
|
const whereClause = readPolicyIr ? ` WHERE ${compilePredIrToSql(readPolicyIr, {
|
|
4780
|
+
namespace: definition.namespace,
|
|
3442
4781
|
columnTypes,
|
|
3443
4782
|
columnNames: columnNamesByTable?.get(table.table)
|
|
3444
4783
|
})}` : readPolicy ? ` WHERE ${compilePredicate(readPolicy.ast, narrowToV1ColumnTypes(columnTypes))}` : " WHERE false";
|
|
3445
4784
|
if (backingPrivileges === "grant") {
|
|
3446
|
-
statements.push(`GRANT USAGE ON SCHEMA ${quoteIdentifier(
|
|
4785
|
+
statements.push(`GRANT USAGE ON SCHEMA ${quoteIdentifier(physical2.schema)} TO ${owner}`);
|
|
3447
4786
|
statements.push(`GRANT SELECT ON ${physicalTarget} TO ${owner}`);
|
|
4787
|
+
statements.push(`DO ${quoteLiteral(`BEGIN
|
|
4788
|
+
IF NOT (pg_catalog.has_schema_privilege(${quoteLiteral(executionOwnerRole)}, ${quoteLiteral(physical2.schema)}, 'USAGE')
|
|
4789
|
+
AND pg_catalog.has_table_privilege(${quoteLiteral(executionOwnerRole)}, ${quoteLiteral(physicalTarget)}, 'SELECT')) THEN
|
|
4790
|
+
RAISE EXCEPTION 'Synced backing owner lacks effective USAGE/SELECT; repair managed serving reader access before ORM apply' USING ERRCODE = '42501';
|
|
4791
|
+
END IF;
|
|
4792
|
+
END`)}`);
|
|
3448
4793
|
}
|
|
3449
4794
|
statements.push(
|
|
3450
4795
|
`CREATE OR REPLACE VIEW ${viewName} AS SELECT * FROM ${physicalTarget}${whereClause}`
|
|
@@ -3460,14 +4805,14 @@ function mergeSyncedViewColumnTypes({
|
|
|
3460
4805
|
columnNames
|
|
3461
4806
|
}) {
|
|
3462
4807
|
const fromTable = {};
|
|
3463
|
-
for (const [
|
|
3464
|
-
if (def?.type) fromTable[
|
|
4808
|
+
for (const [name2, def] of Object.entries(table.columns ?? {})) {
|
|
4809
|
+
if (def?.type) fromTable[name2] = def.type;
|
|
3465
4810
|
}
|
|
3466
4811
|
if (columnNames) {
|
|
3467
|
-
for (const [logical,
|
|
3468
|
-
const declared = fromTable[
|
|
4812
|
+
for (const [logical, physical2] of Object.entries(columnNames)) {
|
|
4813
|
+
const declared = fromTable[physical2] ?? fromTable[logical];
|
|
3469
4814
|
if (!declared) continue;
|
|
3470
|
-
fromTable[
|
|
4815
|
+
fromTable[physical2] = declared;
|
|
3471
4816
|
fromTable[logical] = declared;
|
|
3472
4817
|
}
|
|
3473
4818
|
}
|
|
@@ -3506,8 +4851,8 @@ function syncedBackingRevokeStatements({
|
|
|
3506
4851
|
const seen = /* @__PURE__ */ new Set();
|
|
3507
4852
|
const statements = [];
|
|
3508
4853
|
for (const backing of backings) {
|
|
3509
|
-
const
|
|
3510
|
-
const physicalTarget = `${quoteIdentifier(
|
|
4854
|
+
const physical2 = splitPhysicalRelation(backing.physicalRelation);
|
|
4855
|
+
const physicalTarget = `${quoteIdentifier(physical2.schema)}.${quoteIdentifier(physical2.table)}`;
|
|
3511
4856
|
if (seen.has(physicalTarget)) continue;
|
|
3512
4857
|
seen.add(physicalTarget);
|
|
3513
4858
|
statements.push(`REVOKE SELECT ON ${physicalTarget} FROM ${owner}`);
|
|
@@ -3515,21 +4860,21 @@ function syncedBackingRevokeStatements({
|
|
|
3515
4860
|
return statements;
|
|
3516
4861
|
}
|
|
3517
4862
|
var FACTORIES = {
|
|
3518
|
-
uuid: (
|
|
3519
|
-
text: (
|
|
3520
|
-
integer: (
|
|
4863
|
+
uuid: (name2) => uuid(name2),
|
|
4864
|
+
text: (name2) => text(name2),
|
|
4865
|
+
integer: (name2) => integer(name2),
|
|
3521
4866
|
// Mode 'bigint' keeps int8 lossless — mode 'number' silently rounds past
|
|
3522
4867
|
// 2^53. The read executor serializes BigInt to string for JSON.
|
|
3523
|
-
bigint: (
|
|
3524
|
-
numeric: (
|
|
3525
|
-
double: (
|
|
3526
|
-
boolean: (
|
|
3527
|
-
timestamptz: (
|
|
3528
|
-
date: (
|
|
3529
|
-
jsonb: (
|
|
4868
|
+
bigint: (name2) => bigint(name2, { mode: "bigint" }),
|
|
4869
|
+
numeric: (name2) => numeric(name2),
|
|
4870
|
+
double: (name2) => doublePrecision(name2),
|
|
4871
|
+
boolean: (name2) => boolean(name2),
|
|
4872
|
+
timestamptz: (name2) => timestamp(name2, { withTimezone: true }),
|
|
4873
|
+
date: (name2) => date(name2),
|
|
4874
|
+
jsonb: (name2) => jsonb(name2)
|
|
3530
4875
|
};
|
|
3531
|
-
function buildColumn(
|
|
3532
|
-
let column = FACTORIES[definition.type](
|
|
4876
|
+
function buildColumn(name2, definition) {
|
|
4877
|
+
let column = FACTORIES[definition.type](name2);
|
|
3533
4878
|
if (definition.primary_key) column = column.primaryKey();
|
|
3534
4879
|
if (definition.nullable === false && !definition.primary_key) column = column.notNull();
|
|
3535
4880
|
if (definition.unique && !definition.primary_key) column = column.unique();
|
|
@@ -3560,7 +4905,7 @@ function buildDrizzleEntities(definition) {
|
|
|
3560
4905
|
}
|
|
3561
4906
|
function buildTable(schema, table, built) {
|
|
3562
4907
|
const columns = Object.fromEntries(
|
|
3563
|
-
Object.entries(table.columns ?? {}).map(([
|
|
4908
|
+
Object.entries(table.columns ?? {}).map(([name2, column]) => [name2, buildColumn(name2, column)])
|
|
3564
4909
|
);
|
|
3565
4910
|
const drizzleTable = schema.table(table.table, columns, (self) => [
|
|
3566
4911
|
...buildIndexes(table, self),
|
|
@@ -3570,21 +4915,21 @@ function buildTable(schema, table, built) {
|
|
|
3570
4915
|
}
|
|
3571
4916
|
function buildIndexes(table, self) {
|
|
3572
4917
|
return (table.indexes ?? []).map((definition) => {
|
|
3573
|
-
const
|
|
3574
|
-
const factory = definition.unique ? uniqueIndex(
|
|
4918
|
+
const name2 = indexName(table.table, definition);
|
|
4919
|
+
const factory = definition.unique ? uniqueIndex(name2) : index(name2);
|
|
3575
4920
|
const columns = definition.columns.map((column) => self[column]);
|
|
3576
4921
|
return factory.on(...columns);
|
|
3577
4922
|
});
|
|
3578
4923
|
}
|
|
3579
4924
|
function buildForeignKeys(table, self, built) {
|
|
3580
|
-
return Object.entries(table.links ?? {}).filter(([, link]) => link.enforce).map(([
|
|
4925
|
+
return Object.entries(table.links ?? {}).filter(([, link]) => link.enforce).map(([name2, link]) => {
|
|
3581
4926
|
const target = link.to === table.table ? self : built.get(link.to);
|
|
3582
4927
|
const targetColumn = target && findPrimaryKeyColumn(target);
|
|
3583
4928
|
if (!target || !targetColumn) {
|
|
3584
|
-
throw new OrmDefinitionError(`enforced link '${
|
|
4929
|
+
throw new OrmDefinitionError(`enforced link '${name2}' on '${table.table}' has no built target '${link.to}'`);
|
|
3585
4930
|
}
|
|
3586
4931
|
return foreignKey({
|
|
3587
|
-
name: foreignKeyName(table.table,
|
|
4932
|
+
name: foreignKeyName(table.table, name2),
|
|
3588
4933
|
columns: [self[link.on]],
|
|
3589
4934
|
foreignColumns: [targetColumn]
|
|
3590
4935
|
});
|
|
@@ -3641,21 +4986,21 @@ function diffStatements(previous, current) {
|
|
|
3641
4986
|
function assertUnambiguousDiff(previous, current) {
|
|
3642
4987
|
const previousTables = tablesOf(previous);
|
|
3643
4988
|
const currentTables = tablesOf(current);
|
|
3644
|
-
const removedTables = [...previousTables.keys()].filter((
|
|
3645
|
-
const addedTables = [...currentTables.keys()].filter((
|
|
4989
|
+
const removedTables = [...previousTables.keys()].filter((name2) => !currentTables.has(name2));
|
|
4990
|
+
const addedTables = [...currentTables.keys()].filter((name2) => !previousTables.has(name2));
|
|
3646
4991
|
if (removedTables.length > 0 && addedTables.length > 0) {
|
|
3647
4992
|
throw new OrmDefinitionError(
|
|
3648
4993
|
`ambiguous diff: table(s) added (${addedTables.join(", ")}) and removed (${removedTables.join(", ")}) in the same change \u2014 ship the add and the drop as separate changes`
|
|
3649
4994
|
);
|
|
3650
4995
|
}
|
|
3651
|
-
for (const [
|
|
3652
|
-
const currentColumns = currentTables.get(
|
|
4996
|
+
for (const [name2, previousColumns] of previousTables) {
|
|
4997
|
+
const currentColumns = currentTables.get(name2);
|
|
3653
4998
|
if (!currentColumns) continue;
|
|
3654
4999
|
const removed = [...previousColumns].filter((column) => !currentColumns.has(column));
|
|
3655
5000
|
const added = [...currentColumns].filter((column) => !previousColumns.has(column));
|
|
3656
5001
|
if (removed.length > 0 && added.length > 0) {
|
|
3657
5002
|
throw new OrmDefinitionError(
|
|
3658
|
-
`ambiguous diff on '${
|
|
5003
|
+
`ambiguous diff on '${name2}': column(s) added (${added.join(", ")}) and removed (${removed.join(", ")}) in the same change \u2014 declare \`renamed_from\` for a rename, or ship the add and the drop as separate changes`
|
|
3659
5004
|
);
|
|
3660
5005
|
}
|
|
3661
5006
|
}
|
|
@@ -3674,33 +5019,33 @@ function applyMigrationHints(previous, definition) {
|
|
|
3674
5019
|
for (const table of columnTables(definition)) {
|
|
3675
5020
|
const snapshotTable = findTable(adjusted, schemaName, table.table);
|
|
3676
5021
|
if (!snapshotTable) continue;
|
|
3677
|
-
for (const [
|
|
5022
|
+
for (const [name2, column] of Object.entries(table.columns ?? {})) {
|
|
3678
5023
|
if (column.renamed_from) {
|
|
3679
5024
|
const previousColumn = snapshotTable.columns[column.renamed_from];
|
|
3680
|
-
if (previousColumn && snapshotTable.columns[
|
|
5025
|
+
if (previousColumn && snapshotTable.columns[name2]) {
|
|
3681
5026
|
throw new OrmDefinitionError(
|
|
3682
|
-
`'${table.table}.${
|
|
5027
|
+
`'${table.table}.${name2}' declares renamed_from '${column.renamed_from}', but both columns exist in the previous schema \u2014 a rename cannot overwrite a live column`
|
|
3683
5028
|
);
|
|
3684
5029
|
}
|
|
3685
5030
|
if (previousColumn) {
|
|
3686
5031
|
statements.push(
|
|
3687
|
-
`ALTER TABLE ${qualified(table.table)} RENAME COLUMN ${quoteIdentifier(column.renamed_from)} TO ${quoteIdentifier(
|
|
5032
|
+
`ALTER TABLE ${qualified(table.table)} RENAME COLUMN ${quoteIdentifier(column.renamed_from)} TO ${quoteIdentifier(name2)}`
|
|
3688
5033
|
);
|
|
3689
5034
|
delete snapshotTable.columns[column.renamed_from];
|
|
3690
|
-
snapshotTable.columns[
|
|
3691
|
-
} else if (!snapshotTable.columns[
|
|
5035
|
+
snapshotTable.columns[name2] = { ...previousColumn, name: name2 };
|
|
5036
|
+
} else if (!snapshotTable.columns[name2]) {
|
|
3692
5037
|
throw new OrmDefinitionError(
|
|
3693
|
-
`'${table.table}.${
|
|
5038
|
+
`'${table.table}.${name2}' declares renamed_from '${column.renamed_from}', but the previous schema has neither column \u2014 check the declared source name`
|
|
3694
5039
|
);
|
|
3695
5040
|
}
|
|
3696
5041
|
}
|
|
3697
5042
|
const cast = column.cast ? castParts(column.cast) : null;
|
|
3698
5043
|
if (cast) {
|
|
3699
|
-
const current = snapshotTable.columns[
|
|
5044
|
+
const current = snapshotTable.columns[name2];
|
|
3700
5045
|
const targetType = sqlTypeName(column.type);
|
|
3701
5046
|
if (current && current.type !== targetType) {
|
|
3702
5047
|
statements.push(
|
|
3703
|
-
`ALTER TABLE ${qualified(table.table)} ALTER COLUMN ${quoteIdentifier(
|
|
5048
|
+
`ALTER TABLE ${qualified(table.table)} ALTER COLUMN ${quoteIdentifier(name2)} TYPE ${targetType} USING ${quoteIdentifier(name2)}::${targetType}`
|
|
3704
5049
|
);
|
|
3705
5050
|
current.type = targetType;
|
|
3706
5051
|
}
|
|
@@ -3717,22 +5062,106 @@ function snapshotTableNames(snapshot, schema) {
|
|
|
3717
5062
|
const tables = snapshot.tables ?? {};
|
|
3718
5063
|
return Object.values(tables).filter((entry) => entry.schema === schema).map((entry) => entry.name);
|
|
3719
5064
|
}
|
|
5065
|
+
|
|
5066
|
+
// src/engine/materialized-refresh.ts
|
|
5067
|
+
function materializedRefreshFunctions(canonical) {
|
|
5068
|
+
const schema = ormSchemaName(canonical.namespace);
|
|
5069
|
+
const functions = /* @__PURE__ */ new Map();
|
|
5070
|
+
for (const view of canonical.views ?? []) {
|
|
5071
|
+
const materialized = view.resolved?.materialized;
|
|
5072
|
+
if (!materialized) continue;
|
|
5073
|
+
const table = canonical.tables.find((candidate) => candidate.table === materialized.table);
|
|
5074
|
+
const pk = view.primaryKey ?? [];
|
|
5075
|
+
if (!table || pk.length === 0) {
|
|
5076
|
+
throw new OrmDefinitionError(`materialized resolved table '${view.view}' needs its table and primary key`);
|
|
5077
|
+
}
|
|
5078
|
+
functions.set(view.view, refreshFunctionSql({
|
|
5079
|
+
schema,
|
|
5080
|
+
name: view.view,
|
|
5081
|
+
table: materialized.table,
|
|
5082
|
+
compute: materialized.compute,
|
|
5083
|
+
columns: table.columns.map((column) => column.columnName),
|
|
5084
|
+
pk
|
|
5085
|
+
}));
|
|
5086
|
+
}
|
|
5087
|
+
return functions;
|
|
5088
|
+
}
|
|
5089
|
+
function refreshFunctionSql({ schema, name: name2, table, compute, columns, pk }) {
|
|
5090
|
+
const fn = `${quoteIdentifier(schema)}.${quoteIdentifier(materializedRefreshFunctionName(name2))}`;
|
|
5091
|
+
const target = `${quoteIdentifier(schema)}.${quoteIdentifier(table)}`;
|
|
5092
|
+
const source = `${quoteIdentifier(schema)}.${quoteIdentifier(compute)}`;
|
|
5093
|
+
const list = columns.map(quoteIdentifier).join(", ");
|
|
5094
|
+
const others = columns.filter((column) => !pk.includes(column));
|
|
5095
|
+
const keys = pk.map(quoteIdentifier).join(", ");
|
|
5096
|
+
const matches = pk.map((column) => `c.${quoteIdentifier(column)} = m.${quoteIdentifier(column)}`).join(" AND ");
|
|
5097
|
+
const onConflict = others.length === 0 ? "DO NOTHING" : `DO UPDATE SET ${others.map((column) => `${quoteIdentifier(column)} = EXCLUDED.${quoteIdentifier(column)}`).join(", ")}
|
|
5098
|
+
WHERE (${others.map((column) => `m.${quoteIdentifier(column)}`).join(", ")}) IS DISTINCT FROM (${others.map((column) => `EXCLUDED.${quoteIdentifier(column)}`).join(", ")})`;
|
|
5099
|
+
const lockKey = quoteLiteral(materializeLockKey(schema, name2));
|
|
5100
|
+
return `CREATE OR REPLACE FUNCTION ${fn}(wait boolean DEFAULT false)
|
|
5101
|
+
RETURNS TABLE(upserted bigint, deleted bigint, locked boolean)
|
|
5102
|
+
LANGUAGE plpgsql
|
|
5103
|
+
SECURITY INVOKER
|
|
5104
|
+
SET search_path = pg_catalog, pg_temp
|
|
5105
|
+
AS $orm_refresh$
|
|
5106
|
+
#variable_conflict use_column
|
|
5107
|
+
DECLARE
|
|
5108
|
+
caller_claims text := current_setting('request.jwt.claims', true);
|
|
5109
|
+
BEGIN
|
|
5110
|
+
IF wait THEN
|
|
5111
|
+
PERFORM pg_advisory_xact_lock(hashtext(${lockKey}));
|
|
5112
|
+
ELSIF NOT pg_try_advisory_xact_lock(hashtext(${lockKey})) THEN
|
|
5113
|
+
RETURN QUERY SELECT 0::bigint, 0::bigint, false;
|
|
5114
|
+
RETURN;
|
|
5115
|
+
END IF;
|
|
5116
|
+
PERFORM set_config('request.jwt.claims', ${quoteLiteral(MATERIALIZE_CLAIMS_JSON)}, true);
|
|
5117
|
+
RETURN QUERY
|
|
5118
|
+
WITH up AS (
|
|
5119
|
+
INSERT INTO ${target} AS m (${list})
|
|
5120
|
+
SELECT ${list} FROM ${source}
|
|
5121
|
+
ON CONFLICT (${keys}) ${onConflict}
|
|
5122
|
+
RETURNING 1
|
|
5123
|
+
), gone AS (
|
|
5124
|
+
DELETE FROM ${target} AS m
|
|
5125
|
+
WHERE NOT EXISTS (SELECT 1 FROM ${source} AS c WHERE ${matches})
|
|
5126
|
+
RETURNING 1
|
|
5127
|
+
)
|
|
5128
|
+
SELECT (SELECT count(*) FROM up), (SELECT count(*) FROM gone), true;
|
|
5129
|
+
-- NULL resets to the session value when the caller had none.
|
|
5130
|
+
PERFORM set_config('request.jwt.claims', caller_claims, true);
|
|
5131
|
+
END
|
|
5132
|
+
$orm_refresh$`;
|
|
5133
|
+
}
|
|
5134
|
+
function refreshFunctionStatements({ namespace, previous, current }) {
|
|
5135
|
+
const schema = quoteIdentifier(ormSchemaName(namespace));
|
|
5136
|
+
const statements = [];
|
|
5137
|
+
for (const name2 of Object.keys(previous).sort()) {
|
|
5138
|
+
if (!current.has(name2)) statements.push(`DROP FUNCTION IF EXISTS ${schema}.${quoteIdentifier(materializedRefreshFunctionName(name2))}(boolean)`);
|
|
5139
|
+
}
|
|
5140
|
+
for (const [name2, create] of [...current].sort(([a], [b]) => a.localeCompare(b))) {
|
|
5141
|
+
if (previous[name2] === create) continue;
|
|
5142
|
+
const fn = `${schema}.${quoteIdentifier(materializedRefreshFunctionName(name2))}(boolean)`;
|
|
5143
|
+
statements.push(create, `REVOKE ALL ON FUNCTION ${fn} FROM PUBLIC`);
|
|
5144
|
+
}
|
|
5145
|
+
return statements;
|
|
5146
|
+
}
|
|
5147
|
+
|
|
5148
|
+
// src/engine/canonical2.ts
|
|
3720
5149
|
var FACTORIES2 = {
|
|
3721
|
-
uuid: (
|
|
3722
|
-
text: (
|
|
3723
|
-
integer: (
|
|
5150
|
+
uuid: (name2) => uuid(name2),
|
|
5151
|
+
text: (name2) => text(name2),
|
|
5152
|
+
integer: (name2) => integer(name2),
|
|
3724
5153
|
// Mode 'bigint' keeps int8 lossless — mode 'number' silently rounds past
|
|
3725
5154
|
// 2^53. The read executor serializes BigInt to string for JSON.
|
|
3726
|
-
bigint: (
|
|
3727
|
-
numeric: (
|
|
3728
|
-
double: (
|
|
3729
|
-
boolean: (
|
|
3730
|
-
timestamptz: (
|
|
3731
|
-
date: (
|
|
3732
|
-
jsonb: (
|
|
5155
|
+
bigint: (name2) => bigint(name2, { mode: "bigint" }),
|
|
5156
|
+
numeric: (name2) => numeric(name2),
|
|
5157
|
+
double: (name2) => doublePrecision(name2),
|
|
5158
|
+
boolean: (name2) => boolean(name2),
|
|
5159
|
+
timestamptz: (name2) => timestamp(name2, { withTimezone: true }),
|
|
5160
|
+
date: (name2) => date(name2),
|
|
5161
|
+
jsonb: (name2) => jsonb(name2)
|
|
3733
5162
|
};
|
|
3734
5163
|
var SCHEMA_IMPORT_KEY2 = "$schema";
|
|
3735
|
-
var enumImportKey = (
|
|
5164
|
+
var enumImportKey = (name2) => `$enum_${name2}`;
|
|
3736
5165
|
var columnTablesV2 = (definition) => definition.tables.filter((table) => !table.source);
|
|
3737
5166
|
function buildDrizzleEntitiesV2(definition, options = {}) {
|
|
3738
5167
|
const schemaName = ormSchemaName(definition.namespace);
|
|
@@ -3829,18 +5258,18 @@ function rewriteSyncedSourceRelationsInViewQuery({
|
|
|
3829
5258
|
return rewritten;
|
|
3830
5259
|
});
|
|
3831
5260
|
}
|
|
3832
|
-
function transformSqlOutsideLiterals(
|
|
5261
|
+
function transformSqlOutsideLiterals(sql7, transform) {
|
|
3833
5262
|
const out = [];
|
|
3834
5263
|
let state = { kind: "normal" };
|
|
3835
5264
|
let i = 0;
|
|
3836
5265
|
let codeStart = 0;
|
|
3837
5266
|
const emitCode = (end) => {
|
|
3838
|
-
if (end > codeStart) out.push(transform(
|
|
5267
|
+
if (end > codeStart) out.push(transform(sql7.slice(codeStart, end)));
|
|
3839
5268
|
codeStart = end;
|
|
3840
5269
|
};
|
|
3841
|
-
while (i <
|
|
3842
|
-
const c =
|
|
3843
|
-
const next = i + 1 <
|
|
5270
|
+
while (i < sql7.length) {
|
|
5271
|
+
const c = sql7[i];
|
|
5272
|
+
const next = i + 1 < sql7.length ? sql7[i + 1] : "";
|
|
3844
5273
|
switch (state.kind) {
|
|
3845
5274
|
case "normal": {
|
|
3846
5275
|
if (c === "-" && next === "-") {
|
|
@@ -3860,7 +5289,7 @@ function transformSqlOutsideLiterals(sql5, transform) {
|
|
|
3860
5289
|
break;
|
|
3861
5290
|
}
|
|
3862
5291
|
if ((c === "E" || c === "e") && next === "'") {
|
|
3863
|
-
const prev = i > 0 ?
|
|
5292
|
+
const prev = i > 0 ? sql7[i - 1] : "";
|
|
3864
5293
|
if (!/[A-Za-z0-9_]/.test(prev)) {
|
|
3865
5294
|
emitCode(i);
|
|
3866
5295
|
state = { kind: "e-single" };
|
|
@@ -3879,9 +5308,9 @@ function transformSqlOutsideLiterals(sql5, transform) {
|
|
|
3879
5308
|
break;
|
|
3880
5309
|
}
|
|
3881
5310
|
if (c === "$") {
|
|
3882
|
-
const prev = i > 0 ?
|
|
5311
|
+
const prev = i > 0 ? sql7[i - 1] : "";
|
|
3883
5312
|
if (!/[A-Za-z0-9_$]/.test(prev)) {
|
|
3884
|
-
const tagMatch = /^\$([A-Za-z_][A-Za-z0-9_]*)?\$/.exec(
|
|
5313
|
+
const tagMatch = /^\$([A-Za-z_][A-Za-z0-9_]*)?\$/.exec(sql7.slice(i));
|
|
3885
5314
|
if (tagMatch) {
|
|
3886
5315
|
emitCode(i);
|
|
3887
5316
|
state = { kind: "dollar", tag: tagMatch[1] ?? "" };
|
|
@@ -3913,8 +5342,8 @@ function transformSqlOutsideLiterals(sql5, transform) {
|
|
|
3913
5342
|
break;
|
|
3914
5343
|
}
|
|
3915
5344
|
case "e-single": {
|
|
3916
|
-
if (c === "\\" && i + 1 <
|
|
3917
|
-
out.push(c,
|
|
5345
|
+
if (c === "\\" && i + 1 < sql7.length) {
|
|
5346
|
+
out.push(c, sql7[i + 1]);
|
|
3918
5347
|
i += 2;
|
|
3919
5348
|
break;
|
|
3920
5349
|
}
|
|
@@ -3936,7 +5365,7 @@ function transformSqlOutsideLiterals(sql5, transform) {
|
|
|
3936
5365
|
}
|
|
3937
5366
|
case "dollar": {
|
|
3938
5367
|
const closer = `$${state.tag}$`;
|
|
3939
|
-
if (
|
|
5368
|
+
if (sql7.startsWith(closer, i)) {
|
|
3940
5369
|
out.push(closer);
|
|
3941
5370
|
state = { kind: "normal" };
|
|
3942
5371
|
i += closer.length;
|
|
@@ -3980,15 +5409,15 @@ function transformSqlOutsideLiterals(sql5, transform) {
|
|
|
3980
5409
|
}
|
|
3981
5410
|
}
|
|
3982
5411
|
}
|
|
3983
|
-
emitCode(
|
|
5412
|
+
emitCode(sql7.length);
|
|
3984
5413
|
return out.join("");
|
|
3985
5414
|
}
|
|
3986
5415
|
function escapeRegExp(value) {
|
|
3987
5416
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3988
5417
|
}
|
|
3989
|
-
function sqlReferencesQualifiedRelation(
|
|
5418
|
+
function sqlReferencesQualifiedRelation(sql7, sourceSchema, sourceName) {
|
|
3990
5419
|
let referenced = false;
|
|
3991
|
-
transformSqlOutsideLiterals(
|
|
5420
|
+
transformSqlOutsideLiterals(sql7, (segment) => {
|
|
3992
5421
|
if (qualifiedRelationPattern(sourceSchema, sourceName).test(segment)) referenced = true;
|
|
3993
5422
|
return segment;
|
|
3994
5423
|
});
|
|
@@ -3999,16 +5428,18 @@ function qualifiedRelationPattern(sourceSchema, sourceName) {
|
|
|
3999
5428
|
const namePattern = `(?:"${escapeRegExp(sourceName)}"|${escapeRegExp(sourceName)})`;
|
|
4000
5429
|
return new RegExp(`(?<![\\w."])${schemaPattern}\\.${namePattern}(?![\\w"])`, "g");
|
|
4001
5430
|
}
|
|
4002
|
-
function replaceQualifiedRelation(
|
|
4003
|
-
return
|
|
5431
|
+
function replaceQualifiedRelation(sql7, sourceSchema, sourceName, boundSchema, boundTable) {
|
|
5432
|
+
return sql7.replace(qualifiedRelationPattern(sourceSchema, sourceName), `"${boundSchema}"."${boundTable}"`);
|
|
4004
5433
|
}
|
|
4005
5434
|
function generateSnapshotV2(definition, previousId3) {
|
|
4006
5435
|
const { imports, schemaName } = buildDrizzleEntitiesV2(definition);
|
|
4007
5436
|
const snapshot = generateDrizzleJson(imports, previousId3, [schemaName]);
|
|
4008
5437
|
if ((definition.views?.length ?? 0) > 0 || definition.tables.some((table) => table.foreignKeys?.length)) {
|
|
4009
5438
|
const extended = snapshot;
|
|
5439
|
+
const refreshFunctions = materializedRefreshFunctions(definition);
|
|
4010
5440
|
extended._sequenceOrm = {
|
|
4011
5441
|
...definition.tables.some((table) => table.foreignKeys?.length) ? { foreignKeys: Object.fromEntries(definition.tables.flatMap((table) => (table.foreignKeys ?? []).map((fk) => [`${table.table}.${fk.name}`, structuredClone(fk)]))) } : {},
|
|
5442
|
+
...refreshFunctions.size > 0 ? { refreshFunctions: Object.fromEntries(refreshFunctions) } : {},
|
|
4012
5443
|
viewDependencies: Object.fromEntries(
|
|
4013
5444
|
(definition.views ?? []).map((view) => [view.view, [...view.dependencies].sort()])
|
|
4014
5445
|
)
|
|
@@ -4032,9 +5463,9 @@ function buildTable2(schema, table, built, enums, byName, introspection) {
|
|
|
4032
5463
|
throw new OrmDefinitionError(`table '${table.table}' declares no primaryKey`);
|
|
4033
5464
|
}
|
|
4034
5465
|
const columnNames = new Set(table.columns.map((column) => column.columnName));
|
|
4035
|
-
for (const
|
|
4036
|
-
if (!columnNames.has(
|
|
4037
|
-
throw new OrmDefinitionError(`primaryKey of '${table.table}' references unknown column '${
|
|
5466
|
+
for (const name2 of primaryKeyColumns) {
|
|
5467
|
+
if (!columnNames.has(name2)) {
|
|
5468
|
+
throw new OrmDefinitionError(`primaryKey of '${table.table}' references unknown column '${name2}'`);
|
|
4038
5469
|
}
|
|
4039
5470
|
}
|
|
4040
5471
|
const singlePrimaryKey = primaryKeyColumns.length === 1 ? primaryKeyColumns[0] : void 0;
|
|
@@ -4048,15 +5479,19 @@ function buildTable2(schema, table, built, enums, byName, introspection) {
|
|
|
4048
5479
|
const drizzleTable = schema.table(table.table, columns, (self) => [
|
|
4049
5480
|
...buildCompositePrimaryKey(table, primaryKeyColumns, self),
|
|
4050
5481
|
...buildIndexes2(table, self),
|
|
5482
|
+
...(table.checks ?? []).map((check$1) => check(check$1.name, sql.raw(compileCheckExpression(check$1.expression, table.columns)))),
|
|
4051
5483
|
...buildForeignKeys2(table, self, built, byName),
|
|
4052
5484
|
...(table.foreignKeys ?? []).map((fk) => {
|
|
4053
5485
|
const target = fk.target === table.table ? self : built.get(fk.target);
|
|
4054
5486
|
if (!target) throw new OrmDefinitionError(`foreign key '${fk.name}' has no target '${fk.target}'`);
|
|
4055
|
-
|
|
5487
|
+
let constraint = foreignKey({
|
|
4056
5488
|
name: fk.name,
|
|
4057
5489
|
columns: fk.columns.map((column) => self[column]),
|
|
4058
5490
|
foreignColumns: fk.references.map((column) => target[column])
|
|
4059
5491
|
});
|
|
5492
|
+
if (fk.onDelete !== void 0) constraint = constraint.onDelete(fk.onDelete);
|
|
5493
|
+
if (fk.onUpdate !== void 0) constraint = constraint.onUpdate(fk.onUpdate);
|
|
5494
|
+
return constraint;
|
|
4060
5495
|
})
|
|
4061
5496
|
]);
|
|
4062
5497
|
return drizzleTable.enableRLS();
|
|
@@ -4116,8 +5551,8 @@ function buildIndexes2(table, self) {
|
|
|
4116
5551
|
throw new OrmDefinitionError(`index on '${table.table}' references unknown column '${column}'`);
|
|
4117
5552
|
}
|
|
4118
5553
|
}
|
|
4119
|
-
const
|
|
4120
|
-
const factory = definition.unique ? uniqueIndex(
|
|
5554
|
+
const name2 = indexName(table.table, definition);
|
|
5555
|
+
const factory = definition.unique ? uniqueIndex(name2) : index(name2);
|
|
4121
5556
|
const columns = definition.columns.map((column) => self[column]);
|
|
4122
5557
|
return factory.on(...columns);
|
|
4123
5558
|
});
|
|
@@ -4147,11 +5582,14 @@ function buildForeignKeys2(table, self, built, byName) {
|
|
|
4147
5582
|
`enforced link '${link.name}' on '${table.table}' references unknown column '${targetColumn ? link.on : referencedColumn}'`
|
|
4148
5583
|
);
|
|
4149
5584
|
}
|
|
4150
|
-
|
|
5585
|
+
let constraint = foreignKey({
|
|
4151
5586
|
name: foreignKeyName(table.table, link.name),
|
|
4152
5587
|
columns: [sourceColumn],
|
|
4153
5588
|
foreignColumns: [targetColumn]
|
|
4154
5589
|
});
|
|
5590
|
+
if (link.onDelete !== void 0) constraint = constraint.onDelete(link.onDelete);
|
|
5591
|
+
if (link.onUpdate !== void 0) constraint = constraint.onUpdate(link.onUpdate);
|
|
5592
|
+
return constraint;
|
|
4155
5593
|
});
|
|
4156
5594
|
}
|
|
4157
5595
|
var singlePrimaryKeyOf = (table) => table.primaryKey?.length === 1 ? table.primaryKey[0] : void 0;
|
|
@@ -4193,33 +5631,33 @@ function applyMigrationHintsV2(previous, definition) {
|
|
|
4193
5631
|
const snapshotTable = findTable2(adjusted, schemaName, table.table);
|
|
4194
5632
|
if (!snapshotTable) continue;
|
|
4195
5633
|
for (const column of table.columns) {
|
|
4196
|
-
const
|
|
5634
|
+
const name2 = column.columnName;
|
|
4197
5635
|
if (column.renamedFrom) {
|
|
4198
5636
|
const previousColumn = snapshotTable.columns[column.renamedFrom];
|
|
4199
|
-
if (previousColumn && snapshotTable.columns[
|
|
5637
|
+
if (previousColumn && snapshotTable.columns[name2]) {
|
|
4200
5638
|
throw new OrmDefinitionError(
|
|
4201
|
-
`'${table.table}.${
|
|
5639
|
+
`'${table.table}.${name2}' declares renamedFrom '${column.renamedFrom}', but both columns exist in the previous schema \u2014 a rename cannot overwrite a live column`
|
|
4202
5640
|
);
|
|
4203
5641
|
}
|
|
4204
5642
|
if (previousColumn) {
|
|
4205
5643
|
statements.push(
|
|
4206
|
-
`ALTER TABLE ${qualified(table.table)} RENAME COLUMN ${quoteIdentifier(column.renamedFrom)} TO ${quoteIdentifier(
|
|
5644
|
+
`ALTER TABLE ${qualified(table.table)} RENAME COLUMN ${quoteIdentifier(column.renamedFrom)} TO ${quoteIdentifier(name2)}`
|
|
4207
5645
|
);
|
|
4208
5646
|
delete snapshotTable.columns[column.renamedFrom];
|
|
4209
|
-
snapshotTable.columns[
|
|
4210
|
-
} else if (!snapshotTable.columns[
|
|
5647
|
+
snapshotTable.columns[name2] = { ...previousColumn, name: name2 };
|
|
5648
|
+
} else if (!snapshotTable.columns[name2]) {
|
|
4211
5649
|
throw new OrmDefinitionError(
|
|
4212
|
-
`'${table.table}.${
|
|
5650
|
+
`'${table.table}.${name2}' declares renamedFrom '${column.renamedFrom}', but the previous schema has neither column \u2014 check the declared source name`
|
|
4213
5651
|
);
|
|
4214
5652
|
}
|
|
4215
5653
|
}
|
|
4216
5654
|
const cast = column.cast ? castParts(column.cast) : null;
|
|
4217
5655
|
if (cast) {
|
|
4218
|
-
const current = snapshotTable.columns[
|
|
5656
|
+
const current = snapshotTable.columns[name2];
|
|
4219
5657
|
const { snapshotType, sqlType } = castTargetTypes(column, enumsByName, schemaName, table.table);
|
|
4220
5658
|
if (current && current.type !== snapshotType) {
|
|
4221
5659
|
statements.push(
|
|
4222
|
-
`ALTER TABLE ${qualified(table.table)} ALTER COLUMN ${quoteIdentifier(
|
|
5660
|
+
`ALTER TABLE ${qualified(table.table)} ALTER COLUMN ${quoteIdentifier(name2)} TYPE ${sqlType} USING ${quoteIdentifier(name2)}::${sqlType}`
|
|
4223
5661
|
);
|
|
4224
5662
|
current.type = snapshotType;
|
|
4225
5663
|
}
|
|
@@ -4230,8 +5668,8 @@ function applyMigrationHintsV2(previous, definition) {
|
|
|
4230
5668
|
}
|
|
4231
5669
|
function castTargetTypes(column, enums, schemaName, table) {
|
|
4232
5670
|
if (typeof column.type === "string") {
|
|
4233
|
-
const
|
|
4234
|
-
return { snapshotType:
|
|
5671
|
+
const name2 = sqlTypeName(column.type);
|
|
5672
|
+
return { snapshotType: name2, sqlType: name2 };
|
|
4235
5673
|
}
|
|
4236
5674
|
const enumName = column.type.enum;
|
|
4237
5675
|
if (!enums.has(enumName)) {
|
|
@@ -4272,9 +5710,23 @@ function findTable2(snapshot, schema, table) {
|
|
|
4272
5710
|
const tables = snapshot.tables ?? {};
|
|
4273
5711
|
return Object.values(tables).find((entry) => entry.schema === schema && entry.name === table);
|
|
4274
5712
|
}
|
|
4275
|
-
|
|
5713
|
+
function planMigrationV2(input) {
|
|
5714
|
+
return planWithViewRecreation({ ...input, viewRecreation: "affected" });
|
|
5715
|
+
}
|
|
5716
|
+
async function planCommittedMigrationV2({
|
|
4276
5717
|
canonical,
|
|
4277
|
-
previousSnapshot
|
|
5718
|
+
previousSnapshot,
|
|
5719
|
+
reproduces
|
|
5720
|
+
}) {
|
|
5721
|
+
const current = await planMigrationV2({ canonical, previousSnapshot });
|
|
5722
|
+
if (reproduces(current)) return { plan: current, reproduced: true };
|
|
5723
|
+
const legacy = await planWithViewRecreation({ canonical, previousSnapshot, viewRecreation: "all-on-base-change" });
|
|
5724
|
+
return reproduces(legacy) ? { plan: legacy, reproduced: true } : { plan: current, reproduced: false };
|
|
5725
|
+
}
|
|
5726
|
+
async function planWithViewRecreation({
|
|
5727
|
+
canonical,
|
|
5728
|
+
previousSnapshot,
|
|
5729
|
+
viewRecreation
|
|
4278
5730
|
}) {
|
|
4279
5731
|
const schema = ormSchemaName(canonical.namespace);
|
|
4280
5732
|
const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
|
|
@@ -4292,14 +5744,21 @@ async function planMigrationV2({
|
|
|
4292
5744
|
currentViews,
|
|
4293
5745
|
previousDependencies,
|
|
4294
5746
|
currentDependencies,
|
|
4295
|
-
|
|
5747
|
+
reach: viewReach({
|
|
5748
|
+
viewRecreation,
|
|
5749
|
+
hintStatements: hints.statements,
|
|
5750
|
+
baseDdl,
|
|
5751
|
+
previous,
|
|
5752
|
+
adjusted: hints.adjusted,
|
|
5753
|
+
current: snapshot
|
|
5754
|
+
})
|
|
4296
5755
|
});
|
|
4297
5756
|
const dropOrder = topologicalViewNames(
|
|
4298
|
-
new Set([...affected].filter((
|
|
5757
|
+
new Set([...affected].filter((name2) => previousViews.has(name2))),
|
|
4299
5758
|
previousDependencies
|
|
4300
5759
|
).reverse();
|
|
4301
5760
|
const createOrder = topologicalViewNames(
|
|
4302
|
-
new Set([...affected].filter((
|
|
5761
|
+
new Set([...affected].filter((name2) => currentViews.has(name2))),
|
|
4303
5762
|
currentDependencies
|
|
4304
5763
|
);
|
|
4305
5764
|
const dropDdl = await generatedViewStatements({
|
|
@@ -4316,16 +5775,21 @@ async function planMigrationV2({
|
|
|
4316
5775
|
});
|
|
4317
5776
|
const currentViewDefinitions = new Map((canonical.views ?? []).map((view) => [view.view, view]));
|
|
4318
5777
|
const statements = [
|
|
4319
|
-
...dropOrder.map((
|
|
5778
|
+
...dropOrder.map((name2) => ({ kind: "ddl", sql: requiredViewStatement(dropDdl, name2, schema, "DROP") })),
|
|
4320
5779
|
...hints.statements.map((statement) => ({ kind: "hint", sql: statement })),
|
|
4321
5780
|
...baseDdl.map((statement) => ({ kind: "ddl", sql: statement.replace(/;\s*$/, "") })),
|
|
4322
|
-
...foreignKeyTimingStatements(canonical, previous, baseDdl).map((
|
|
4323
|
-
...createOrder.map((
|
|
4324
|
-
...createOrder.map((
|
|
4325
|
-
const view = currentViewDefinitions.get(
|
|
4326
|
-
if (!view) throw new OrmDefinitionError(`missing canonical definition for view '${
|
|
5781
|
+
...foreignKeyTimingStatements(canonical, previous, baseDdl).map((sql7) => ({ kind: "ddl", sql: sql7 })),
|
|
5782
|
+
...createOrder.map((name2) => ({ kind: "ddl", sql: requiredViewStatement(createDdl, name2, schema, "CREATE") })),
|
|
5783
|
+
...createOrder.map((name2) => {
|
|
5784
|
+
const view = currentViewDefinitions.get(name2);
|
|
5785
|
+
if (!view) throw new OrmDefinitionError(`missing canonical definition for view '${name2}'`);
|
|
4327
5786
|
return { kind: "ddl", sql: validateViewCatalogStatement({ canonical, view, schema }) };
|
|
4328
|
-
})
|
|
5787
|
+
}),
|
|
5788
|
+
...refreshFunctionStatements({
|
|
5789
|
+
namespace: canonical.namespace,
|
|
5790
|
+
previous: previous._sequenceOrm?.refreshFunctions ?? {},
|
|
5791
|
+
current: materializedRefreshFunctions(canonical)
|
|
5792
|
+
}).map((sql7) => ({ kind: "ddl", sql: sql7 }))
|
|
4329
5793
|
];
|
|
4330
5794
|
return { namespace: canonical.namespace, schema, statements, snapshot };
|
|
4331
5795
|
}
|
|
@@ -4348,9 +5812,9 @@ function foreignKeyTimingStatements(canonical, previous, baseDdl) {
|
|
|
4348
5812
|
}
|
|
4349
5813
|
return result;
|
|
4350
5814
|
}
|
|
4351
|
-
function foreignKeyTimingSql(schema, table,
|
|
5815
|
+
function foreignKeyTimingSql(schema, table, name2, timing) {
|
|
4352
5816
|
const clause = timing === void 0 ? "NOT DEFERRABLE" : timing === "initially-deferred" ? "DEFERRABLE INITIALLY DEFERRED" : "DEFERRABLE INITIALLY IMMEDIATE";
|
|
4353
|
-
return `ALTER TABLE ${quoteIdentifier(schema)}.${quoteIdentifier(table)} ALTER CONSTRAINT ${quoteIdentifier(
|
|
5817
|
+
return `ALTER TABLE ${quoteIdentifier(schema)}.${quoteIdentifier(table)} ALTER CONSTRAINT ${quoteIdentifier(name2)} ${clause}`;
|
|
4354
5818
|
}
|
|
4355
5819
|
function validateViewCatalogStatement({
|
|
4356
5820
|
canonical,
|
|
@@ -4369,9 +5833,9 @@ function validateViewCatalogStatement({
|
|
|
4369
5833
|
}
|
|
4370
5834
|
const expectedDependencies = sqlTextArray(
|
|
4371
5835
|
view.dependencies.map((dependency) => {
|
|
4372
|
-
const
|
|
4373
|
-
if (!
|
|
4374
|
-
return
|
|
5836
|
+
const physical2 = relationIdentity.get(dependency);
|
|
5837
|
+
if (!physical2) throw new OrmDefinitionError(`view '${view.view}' depends on unknown relation '${dependency}'`);
|
|
5838
|
+
return physical2;
|
|
4375
5839
|
}).sort()
|
|
4376
5840
|
);
|
|
4377
5841
|
const allowedFunctions = sqlTextArray(VIEW_QUERY_FUNCTION_ALLOWLIST);
|
|
@@ -4479,30 +5943,111 @@ function withoutViews(snapshot) {
|
|
|
4479
5943
|
delete clone._sequenceOrm;
|
|
4480
5944
|
return clone;
|
|
4481
5945
|
}
|
|
5946
|
+
var DROPS_TYPE = /^\s*DROP\s+TYPE\b/i;
|
|
5947
|
+
function viewReach({
|
|
5948
|
+
viewRecreation,
|
|
5949
|
+
hintStatements,
|
|
5950
|
+
baseDdl,
|
|
5951
|
+
previous,
|
|
5952
|
+
adjusted,
|
|
5953
|
+
current
|
|
5954
|
+
}) {
|
|
5955
|
+
if (hintStatements.length === 0 && baseDdl.length === 0) return /* @__PURE__ */ new Set();
|
|
5956
|
+
if (viewRecreation === "all-on-base-change") return "all";
|
|
5957
|
+
if (baseDdl.some((statement) => DROPS_TYPE.test(statement))) return "all";
|
|
5958
|
+
const dependencies = snapshotViewDependencies(adjusted);
|
|
5959
|
+
if ([...snapshotViews(adjusted).keys()].some((name2) => dependencies[name2] === void 0)) return "all";
|
|
5960
|
+
return baseChangesReachingViews({ previous, adjusted, current });
|
|
5961
|
+
}
|
|
5962
|
+
var SNAPSHOT_BOOKKEEPING = /* @__PURE__ */ new Set(["id", "prevId", "version", "dialect", "_meta", "views", "_sequenceOrm", "enums"]);
|
|
5963
|
+
var VIEW_SAFE_TABLE_FIELDS = /* @__PURE__ */ new Set([
|
|
5964
|
+
"indexes",
|
|
5965
|
+
"foreignKeys",
|
|
5966
|
+
"uniqueConstraints",
|
|
5967
|
+
"checkConstraints",
|
|
5968
|
+
"policies",
|
|
5969
|
+
"isRLSEnabled"
|
|
5970
|
+
]);
|
|
5971
|
+
var VIEW_SAFE_COLUMN_FIELDS = /* @__PURE__ */ new Set(["notNull", "default", "isUnique", "uniqueName", "nullsNotDistinct"]);
|
|
5972
|
+
function baseChangesReachingViews({
|
|
5973
|
+
previous,
|
|
5974
|
+
adjusted,
|
|
5975
|
+
current
|
|
5976
|
+
}) {
|
|
5977
|
+
const before = adjusted;
|
|
5978
|
+
const after = current;
|
|
5979
|
+
for (const section of /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])) {
|
|
5980
|
+
if (SNAPSHOT_BOOKKEEPING.has(section) || section === "tables") continue;
|
|
5981
|
+
if (stableJson(before[section]) !== stableJson(after[section])) return "all";
|
|
5982
|
+
}
|
|
5983
|
+
const relations = /* @__PURE__ */ new Set();
|
|
5984
|
+
const previousTables = snapshotTables(previous);
|
|
5985
|
+
const adjustedTables = snapshotTables(adjusted);
|
|
5986
|
+
for (const key of changedEntryKeys(previousTables, adjustedTables)) {
|
|
5987
|
+
const table = previousTables[key] ?? adjustedTables[key];
|
|
5988
|
+
if (table) relations.add(table.name);
|
|
5989
|
+
}
|
|
5990
|
+
const currentTables = snapshotTables(current);
|
|
5991
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(adjustedTables), ...Object.keys(currentTables)])) {
|
|
5992
|
+
const was = adjustedTables[key];
|
|
5993
|
+
const is2 = currentTables[key];
|
|
5994
|
+
if (!was || !is2) {
|
|
5995
|
+
const table = was ?? is2;
|
|
5996
|
+
if (table) relations.add(table.name);
|
|
5997
|
+
} else if (tableChangeReachesViews({ was, is: is2 })) {
|
|
5998
|
+
relations.add(is2.name);
|
|
5999
|
+
}
|
|
6000
|
+
}
|
|
6001
|
+
return relations;
|
|
6002
|
+
}
|
|
6003
|
+
function tableChangeReachesViews({ was, is: is2 }) {
|
|
6004
|
+
for (const field of /* @__PURE__ */ new Set([...Object.keys(was), ...Object.keys(is2)])) {
|
|
6005
|
+
if (field === "columns" || VIEW_SAFE_TABLE_FIELDS.has(field)) continue;
|
|
6006
|
+
if (stableJson(was[field]) !== stableJson(is2[field])) return true;
|
|
6007
|
+
}
|
|
6008
|
+
return Object.entries(was.columns).some(([name2, column]) => {
|
|
6009
|
+
const next = is2.columns[name2];
|
|
6010
|
+
if (!next) return true;
|
|
6011
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(column), ...Object.keys(next)])].some(
|
|
6012
|
+
(field) => !VIEW_SAFE_COLUMN_FIELDS.has(field) && stableJson(column[field]) !== stableJson(next[field])
|
|
6013
|
+
);
|
|
6014
|
+
});
|
|
6015
|
+
}
|
|
6016
|
+
function changedEntryKeys(before, after) {
|
|
6017
|
+
return new Set(
|
|
6018
|
+
[.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].filter(
|
|
6019
|
+
(key) => stableJson(before[key]) !== stableJson(after[key])
|
|
6020
|
+
)
|
|
6021
|
+
);
|
|
6022
|
+
}
|
|
6023
|
+
function snapshotTables(snapshot) {
|
|
6024
|
+
return snapshot.tables ?? {};
|
|
6025
|
+
}
|
|
4482
6026
|
function affectedViewNames({
|
|
4483
6027
|
previousViews,
|
|
4484
6028
|
currentViews,
|
|
4485
6029
|
previousDependencies,
|
|
4486
6030
|
currentDependencies,
|
|
4487
|
-
|
|
6031
|
+
reach
|
|
4488
6032
|
}) {
|
|
4489
6033
|
const allNames = /* @__PURE__ */ new Set([...previousViews.keys(), ...currentViews.keys()]);
|
|
4490
|
-
if (
|
|
6034
|
+
if (reach === "all") return allNames;
|
|
6035
|
+
const dependenciesOf = (name2) => /* @__PURE__ */ new Set([...previousDependencies[name2] ?? [], ...currentDependencies[name2] ?? []]);
|
|
4491
6036
|
const affected = new Set(
|
|
4492
|
-
[...allNames].filter((
|
|
4493
|
-
const before = previousViews.get(
|
|
4494
|
-
const after = currentViews.get(
|
|
4495
|
-
|
|
6037
|
+
[...allNames].filter((name2) => {
|
|
6038
|
+
const before = previousViews.get(name2);
|
|
6039
|
+
const after = currentViews.get(name2);
|
|
6040
|
+
if (before === void 0 || after === void 0 || stableJson(before) !== stableJson(after)) return true;
|
|
6041
|
+
return [...dependenciesOf(name2)].some((dependency) => reach.has(dependency));
|
|
4496
6042
|
})
|
|
4497
6043
|
);
|
|
4498
6044
|
let grew = true;
|
|
4499
6045
|
while (grew) {
|
|
4500
6046
|
grew = false;
|
|
4501
|
-
for (const
|
|
4502
|
-
if (affected.has(
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
affected.add(name);
|
|
6047
|
+
for (const name2 of allNames) {
|
|
6048
|
+
if (affected.has(name2)) continue;
|
|
6049
|
+
if ([...dependenciesOf(name2)].some((dependency) => affected.has(dependency))) {
|
|
6050
|
+
affected.add(name2);
|
|
4506
6051
|
grew = true;
|
|
4507
6052
|
}
|
|
4508
6053
|
}
|
|
@@ -4513,13 +6058,13 @@ function topologicalViewNames(names, dependencies) {
|
|
|
4513
6058
|
const remaining = new Set(names);
|
|
4514
6059
|
const ordered = [];
|
|
4515
6060
|
while (remaining.size > 0) {
|
|
4516
|
-
const ready = [...remaining].filter((
|
|
6061
|
+
const ready = [...remaining].filter((name2) => (dependencies[name2] ?? []).every((dependency) => !remaining.has(dependency))).sort();
|
|
4517
6062
|
if (ready.length === 0) {
|
|
4518
6063
|
throw new OrmDefinitionError(`view dependencies form a cycle among: ${[...remaining].sort().join(", ")}`);
|
|
4519
6064
|
}
|
|
4520
|
-
for (const
|
|
4521
|
-
ordered.push(
|
|
4522
|
-
remaining.delete(
|
|
6065
|
+
for (const name2 of ready) {
|
|
6066
|
+
ordered.push(name2);
|
|
6067
|
+
remaining.delete(name2);
|
|
4523
6068
|
}
|
|
4524
6069
|
}
|
|
4525
6070
|
return ordered;
|
|
@@ -4537,18 +6082,18 @@ async function generatedViewStatements({
|
|
|
4537
6082
|
holder.views = Object.fromEntries(Object.entries(views).filter(([, view]) => !names.has(view.name)));
|
|
4538
6083
|
const generated = mode === "drop" ? await diffStatements(snapshot, without) : await diffStatements(without, snapshot);
|
|
4539
6084
|
const statements = /* @__PURE__ */ new Map();
|
|
4540
|
-
for (const
|
|
4541
|
-
const qualified = `${quoteIdentifier(schema)}.${quoteIdentifier(
|
|
6085
|
+
for (const name2 of names) {
|
|
6086
|
+
const qualified = `${quoteIdentifier(schema)}.${quoteIdentifier(name2)}`;
|
|
4542
6087
|
const prefix = mode === "drop" ? "DROP VIEW" : "CREATE VIEW";
|
|
4543
6088
|
const statement = generated.find((candidate) => candidate.trimStart().startsWith(`${prefix} ${qualified}`));
|
|
4544
|
-
if (statement) statements.set(
|
|
6089
|
+
if (statement) statements.set(name2, statement.replace(/;\s*$/, ""));
|
|
4545
6090
|
}
|
|
4546
6091
|
return statements;
|
|
4547
6092
|
}
|
|
4548
|
-
function requiredViewStatement(statements,
|
|
4549
|
-
const statement = statements.get(
|
|
6093
|
+
function requiredViewStatement(statements, name2, schema, operation) {
|
|
6094
|
+
const statement = statements.get(name2);
|
|
4550
6095
|
if (!statement) {
|
|
4551
|
-
throw new OrmDefinitionError(`drizzle-kit did not generate ${operation} VIEW for '${schema}.${
|
|
6096
|
+
throw new OrmDefinitionError(`drizzle-kit did not generate ${operation} VIEW for '${schema}.${name2}'`);
|
|
4552
6097
|
}
|
|
4553
6098
|
return statement;
|
|
4554
6099
|
}
|
|
@@ -4739,15 +6284,15 @@ async function planMigration({
|
|
|
4739
6284
|
const ddl = await diffStatements(hints.adjusted, snapshot);
|
|
4740
6285
|
const policyDrops = policyDropStatements(schema, snapshotTableNames(previous, schema));
|
|
4741
6286
|
const statements = [
|
|
4742
|
-
...policyDrops.map((
|
|
4743
|
-
...hints.statements.map((
|
|
4744
|
-
...ddl.map((
|
|
4745
|
-
...securityStatements(definition).map((
|
|
6287
|
+
...policyDrops.map((sql7) => ({ kind: "security", sql: sql7 })),
|
|
6288
|
+
...hints.statements.map((sql7) => ({ kind: "hint", sql: sql7 })),
|
|
6289
|
+
...ddl.map((sql7) => ({ kind: "ddl", sql: sql7.replace(/;\s*$/, "") })),
|
|
6290
|
+
...securityStatements(definition).map((sql7) => ({ kind: "security", sql: sql7 })),
|
|
4746
6291
|
// PREPARE each action's executes first (plpgsql bodies don't check columns
|
|
4747
6292
|
// at CREATE), then create the functions. Both reference the tables the DDL
|
|
4748
6293
|
// above creates, so they close the plan.
|
|
4749
|
-
...actionPrepareChecks(definition).map((
|
|
4750
|
-
...actionFunctionStatements(definition).map((
|
|
6294
|
+
...actionPrepareChecks(definition).map((sql7) => ({ kind: "function", sql: sql7 })),
|
|
6295
|
+
...actionFunctionStatements(definition).map((sql7) => ({ kind: "function", sql: sql7 }))
|
|
4751
6296
|
];
|
|
4752
6297
|
return { namespace: definition.namespace, schema, statements, snapshot };
|
|
4753
6298
|
}
|
|
@@ -4794,9 +6339,13 @@ function renderOrmMigrationExport(exported) {
|
|
|
4794
6339
|
if (Buffer.byteLength(text4, "utf8") > ORM_MIGRATION_EXPORT_MAX_BYTES) throw new Error("ORM migration export exceeds the size limit");
|
|
4795
6340
|
return text4;
|
|
4796
6341
|
}
|
|
4797
|
-
var IR_VERSION =
|
|
6342
|
+
var IR_VERSION = 5;
|
|
4798
6343
|
var LEGACY_IR_VERSION = 1;
|
|
4799
|
-
var
|
|
6344
|
+
var GUARDED_IR_VERSION = 2;
|
|
6345
|
+
var COMPOSED_IR_VERSION = 3;
|
|
6346
|
+
var SQL_IR_VERSION = 4;
|
|
6347
|
+
var RUNTIME_SQL_IR_VERSION = 5;
|
|
6348
|
+
var irVersionSchema = z.union([z.literal(LEGACY_IR_VERSION), z.literal(GUARDED_IR_VERSION), z.literal(COMPOSED_IR_VERSION), z.literal(SQL_IR_VERSION), z.literal(RUNTIME_SQL_IR_VERSION)]);
|
|
4800
6349
|
var operationVersionSchema = z.object({ irVersion: irVersionSchema });
|
|
4801
6350
|
function requiredArtifactIrVersion(operations) {
|
|
4802
6351
|
let required = LEGACY_IR_VERSION;
|
|
@@ -4848,8 +6397,8 @@ var predIrSchema = z.lazy(
|
|
|
4848
6397
|
value: z.number(),
|
|
4849
6398
|
where: predIrSchema.optional()
|
|
4850
6399
|
}),
|
|
4851
|
-
z.object({ kind: z.literal("capability"), key: z.string() }),
|
|
4852
|
-
z.object({ kind: z.literal("role"), key: z.string() }),
|
|
6400
|
+
z.object({ kind: z.literal("capability"), key: z.string(), namespace: z.string().optional() }),
|
|
6401
|
+
z.object({ kind: z.literal("role"), key: z.string(), namespace: z.string().optional() }),
|
|
4853
6402
|
z.object({ kind: z.literal("dynamicFilter"), slot: z.string() }),
|
|
4854
6403
|
z.object({ kind: z.literal("const"), value: z.boolean() })
|
|
4855
6404
|
])
|
|
@@ -4877,6 +6426,21 @@ var selectionIrSchema = z.lazy(
|
|
|
4877
6426
|
() => z.object({
|
|
4878
6427
|
typename: z.string(),
|
|
4879
6428
|
columns: z.array(z.object({ alias: z.string(), column: z.string() })),
|
|
6429
|
+
cells: z.object({
|
|
6430
|
+
alias: z.string(),
|
|
6431
|
+
typename: z.string(),
|
|
6432
|
+
fields: z.array(z.object({
|
|
6433
|
+
alias: z.string(),
|
|
6434
|
+
column: z.string(),
|
|
6435
|
+
typename: z.string(),
|
|
6436
|
+
parts: z.array(z.object({
|
|
6437
|
+
alias: z.string(),
|
|
6438
|
+
part: z.enum(["source", "at", "by", "conflicts"]),
|
|
6439
|
+
fields: z.array(z.object({ alias: z.string(), field: z.enum(["source", "at", "value"]) })).optional(),
|
|
6440
|
+
typename: z.string().optional()
|
|
6441
|
+
}))
|
|
6442
|
+
}))
|
|
6443
|
+
}).optional(),
|
|
4880
6444
|
relations: z.array(z.object({
|
|
4881
6445
|
alias: z.string(),
|
|
4882
6446
|
link: z.string(),
|
|
@@ -4898,6 +6462,11 @@ var selectionIrSchema = z.lazy(
|
|
|
4898
6462
|
})
|
|
4899
6463
|
);
|
|
4900
6464
|
var includeIfSchema = z.object({ variable: z.string(), negated: z.boolean() });
|
|
6465
|
+
var prefetchPlanIrSchema = z.object({
|
|
6466
|
+
source: z.string(),
|
|
6467
|
+
maxStalenessMs: z.number().int().positive().optional(),
|
|
6468
|
+
key: z.union([z.object({ one: valueIrSchema }), z.object({ many: valueIrSchema })])
|
|
6469
|
+
});
|
|
4901
6470
|
var selectRootSchema = z.object({
|
|
4902
6471
|
kind: z.literal("select"),
|
|
4903
6472
|
alias: z.string(),
|
|
@@ -4909,7 +6478,8 @@ var selectRootSchema = z.object({
|
|
|
4909
6478
|
distinctOn: z.union([z.array(z.string()), z.object({ slot: z.string() })]).optional(),
|
|
4910
6479
|
limit: limitIrSchema,
|
|
4911
6480
|
offset: z.object({ variable: z.string().optional(), value: z.number().int().optional() }).optional(),
|
|
4912
|
-
includeIf: includeIfSchema.optional()
|
|
6481
|
+
includeIf: includeIfSchema.optional(),
|
|
6482
|
+
prefetch: z.array(prefetchPlanIrSchema).optional()
|
|
4913
6483
|
});
|
|
4914
6484
|
var byPkRootSchema = z.object({
|
|
4915
6485
|
kind: z.literal("byPk"),
|
|
@@ -4919,7 +6489,8 @@ var byPkRootSchema = z.object({
|
|
|
4919
6489
|
pk: z.array(z.object({ column: z.string(), value: valueIrSchema })).min(1),
|
|
4920
6490
|
/** Policy predicate — ANDed with the PK match. */
|
|
4921
6491
|
where: predIrSchema,
|
|
4922
|
-
includeIf: includeIfSchema.optional()
|
|
6492
|
+
includeIf: includeIfSchema.optional(),
|
|
6493
|
+
prefetch: z.array(prefetchPlanIrSchema).optional()
|
|
4923
6494
|
});
|
|
4924
6495
|
var aggregateRootSchema = z.object({
|
|
4925
6496
|
kind: z.literal("aggregate"),
|
|
@@ -4977,7 +6548,20 @@ var mutationRootSchema = z.object({
|
|
|
4977
6548
|
* coalesce view falls back to synced. Both are gated on a matching synced
|
|
4978
6549
|
* row and carry the write policy in `where` like update/delete.
|
|
4979
6550
|
*/
|
|
4980
|
-
op: z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
|
|
6551
|
+
op: z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert", "resolvedWrite"]),
|
|
6552
|
+
/**
|
|
6553
|
+
* resolvedWrite: a write to a resolved table (`table` is its view). The
|
|
6554
|
+
* executor gates on the resolved row (write policy in `where`), writes only
|
|
6555
|
+
* the touched cells into the manual source, then re-checks `insertCheck`
|
|
6556
|
+
* against the resolved row. `revert` clears `revertColumns` from the manual
|
|
6557
|
+
* source; `delete` tombstones the row there.
|
|
6558
|
+
*/
|
|
6559
|
+
resolvedAction: z.enum(["insert", "update", "delete", "revert"]).optional(),
|
|
6560
|
+
revertColumns: z.array(z.string()).optional(),
|
|
6561
|
+
/** `@writeback(source:, mode:)` directives on this root. */
|
|
6562
|
+
writeback: z.array(z.object({ source: z.string(), mode: z.enum(["sync", "async"]) })).optional(),
|
|
6563
|
+
/** `@localOnly(source:)` — writeback columns deliberately kept local for these sources. */
|
|
6564
|
+
localOnly: z.array(z.string()).optional(),
|
|
4981
6565
|
alias: z.string(),
|
|
4982
6566
|
table: z.string(),
|
|
4983
6567
|
/** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
|
|
@@ -5041,7 +6625,31 @@ var variableIrSchema = z.object({
|
|
|
5041
6625
|
/** Absent = no default declared; see {@link variableDefaultIrSchema}. */
|
|
5042
6626
|
defaultValue: variableDefaultIrSchema.optional()
|
|
5043
6627
|
});
|
|
6628
|
+
var sqlColumnSchema = z.object({ name: z.string(), type: z.enum(["text", "boolean", "integer", "bigint", "numeric", "double", "uuid", "date", "timestamptz"]), nullable: z.boolean() });
|
|
5044
6629
|
var compiledOperationSchema = z.object({
|
|
6630
|
+
language: z.literal("sql").optional(),
|
|
6631
|
+
/** Server-compiled object catalog and permission ceiling, never caller SQL. */
|
|
6632
|
+
sqlQuery: z.object({
|
|
6633
|
+
catalog: z.object({
|
|
6634
|
+
identity: z.string(),
|
|
6635
|
+
objects: z.array(z.object({
|
|
6636
|
+
ref: z.string(),
|
|
6637
|
+
table: z.string(),
|
|
6638
|
+
columns: z.array(sqlColumnSchema.extend({ type: z.string() })).min(1),
|
|
6639
|
+
scan: z.string().min(1),
|
|
6640
|
+
read: z.array(z.string()).min(1)
|
|
6641
|
+
}).strict()).min(1).max(256)
|
|
6642
|
+
}).strict(),
|
|
6643
|
+
read: z.array(z.string()).min(1).max(256),
|
|
6644
|
+
maxRows: z.number().int().min(1).max(1e4)
|
|
6645
|
+
}).strict().optional(),
|
|
6646
|
+
sql: z.object({
|
|
6647
|
+
text: z.string().min(1).max(1048576),
|
|
6648
|
+
columns: z.array(sqlColumnSchema).min(1).max(256),
|
|
6649
|
+
parameters: z.array(z.string()).max(128),
|
|
6650
|
+
read: z.array(z.string()).min(1).max(256),
|
|
6651
|
+
maxRows: z.number().int().min(1).max(1e4)
|
|
6652
|
+
}).strict().optional(),
|
|
5045
6653
|
irVersion: irVersionSchema,
|
|
5046
6654
|
name: z.string(),
|
|
5047
6655
|
opHash: z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
@@ -5053,8 +6661,16 @@ var compiledOperationSchema = z.object({
|
|
|
5053
6661
|
variables: z.array(variableIrSchema),
|
|
5054
6662
|
/** Every table the op touches — data-token scoping + audit. */
|
|
5055
6663
|
tableSet: z.array(z.string()).min(1),
|
|
5056
|
-
roots: z.array(rootIrSchema)
|
|
6664
|
+
roots: z.array(rootIrSchema)
|
|
5057
6665
|
}).superRefine((operation, ctx) => {
|
|
6666
|
+
if (operation.language === "sql") {
|
|
6667
|
+
const validPlan = operation.sqlQuery ? !operation.sql && operation.irVersion === RUNTIME_SQL_IR_VERSION : !!operation.sql && operation.irVersion === SQL_IR_VERSION;
|
|
6668
|
+
if (!validPlan || operation.kind !== "query" || operation.roots.length !== 0 || !operation.requires?.capability) {
|
|
6669
|
+
ctx.addIssue({ code: "custom", message: "SQL operations require a query-only v4 plan or v5 runtime contract, capability, and no GraphQL roots" });
|
|
6670
|
+
}
|
|
6671
|
+
} else if (operation.sql || operation.sqlQuery || operation.roots.length === 0) {
|
|
6672
|
+
ctx.addIssue({ code: "custom", message: "GraphQL operations require roots and cannot carry SQL" });
|
|
6673
|
+
}
|
|
5058
6674
|
for (const [index3, root] of operation.roots.entries()) {
|
|
5059
6675
|
if (root.kind !== "mutation" || root.assertAffectedRows === void 0) continue;
|
|
5060
6676
|
if (operation.irVersion < 2 || operation.kind !== "mutation" || root.includeIf !== void 0) {
|
|
@@ -5062,13 +6678,55 @@ var compiledOperationSchema = z.object({
|
|
|
5062
6678
|
}
|
|
5063
6679
|
}
|
|
5064
6680
|
});
|
|
6681
|
+
var resolvedSourceIrSchema = z.object({
|
|
6682
|
+
name: z.string(),
|
|
6683
|
+
kind: z.enum(["manual", "etl", "prefetch"]),
|
|
6684
|
+
/** Logical table key of the backing relation. */
|
|
6685
|
+
table: z.string(),
|
|
6686
|
+
columns: z.array(z.string()),
|
|
6687
|
+
key: z.string().optional(),
|
|
6688
|
+
fetch: z.union([
|
|
6689
|
+
z.object({ shape: z.literal("single") }),
|
|
6690
|
+
z.object({ shape: z.literal("batch"), maxKeys: z.number().int().positive() })
|
|
6691
|
+
]).optional(),
|
|
6692
|
+
function: z.object({ kind: z.enum(["managed", "native"]), id: z.string() }).optional(),
|
|
6693
|
+
prefetchDefaults: z.object({
|
|
6694
|
+
maxStalenessMs: z.number().int().positive(),
|
|
6695
|
+
timeoutMs: z.number().int().positive(),
|
|
6696
|
+
onError: z.enum(["fallback", "fail"])
|
|
6697
|
+
}).optional(),
|
|
6698
|
+
writeback: z.object({
|
|
6699
|
+
columns: z.array(z.string()),
|
|
6700
|
+
defaultMode: z.enum(["sync", "async"]),
|
|
6701
|
+
timeoutMs: z.number().int().positive(),
|
|
6702
|
+
clearManualOnConfirm: z.boolean()
|
|
6703
|
+
}).optional()
|
|
6704
|
+
});
|
|
6705
|
+
var resolvedMetaIrSchema = z.object({
|
|
6706
|
+
/** Logical pk column of the resolved view (single-column). */
|
|
6707
|
+
pk: z.string(),
|
|
6708
|
+
/** Source name of the manual source. */
|
|
6709
|
+
manual: z.string(),
|
|
6710
|
+
sources: z.array(resolvedSourceIrSchema),
|
|
6711
|
+
deletedBy: z.array(z.string()),
|
|
6712
|
+
/**
|
|
6713
|
+
* Materialized: the view reads `table`, which the data plane refreshes from
|
|
6714
|
+
* the `compute` view (claims-gated) whenever it writes a source row.
|
|
6715
|
+
*/
|
|
6716
|
+
materialized: z.object({ table: z.string(), compute: z.string() }).optional()
|
|
6717
|
+
});
|
|
5065
6718
|
var tableMetaIrSchema = z.object({
|
|
5066
6719
|
table: z.string(),
|
|
6720
|
+
namespace: z.string().optional(),
|
|
6721
|
+
dependencies: z.array(z.string()).optional(),
|
|
6722
|
+
foreignReferences: z.array(z.object({ column: z.string(), target: z.string(), targetColumn: z.string() })).optional(),
|
|
6723
|
+
exportRead: z.union([z.literal(true), z.object({ capability: z.string() }), z.object({ role: z.string() })]).optional(),
|
|
6724
|
+
exportWrite: z.union([z.literal(true), z.object({ capability: z.string() }), z.object({ role: z.string() })]).optional(),
|
|
5067
6725
|
schema: z.string(),
|
|
5068
6726
|
typename: z.string(),
|
|
5069
6727
|
/**
|
|
5070
6728
|
* True when statements over this relation evaluate a claims-gated synced
|
|
5071
|
-
* view: synced twins (`source:
|
|
6729
|
+
* view: synced twins (`source: schema.table`) materialize as views whose baked
|
|
5072
6730
|
* policy reads `current_setting('request.jwt.claims')`, and namespace views
|
|
5073
6731
|
* inherit the gate transitively through their declared dependencies. The
|
|
5074
6732
|
* executor must stamp that GUC (transaction-scoped) before any statement
|
|
@@ -5102,6 +6760,22 @@ var tableMetaIrSchema = z.object({
|
|
|
5102
6760
|
* `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
|
|
5103
6761
|
*/
|
|
5104
6762
|
touchOnUpdate: z.array(z.string()).optional(),
|
|
6763
|
+
/**
|
|
6764
|
+
* Per-cell metadata: logical column → the logical names of its cell-part
|
|
6765
|
+
* columns (which are listed in `columns`, but never filterable, orderable or
|
|
6766
|
+
* writable). Tables stamp `at`/`by` (and a manual source `rev`) on every
|
|
6767
|
+
* write; resolved views expose `source`/`at`/`by`/`conflicts`. Omitted when
|
|
6768
|
+
* the relation tracks no cells.
|
|
6769
|
+
*/
|
|
6770
|
+
cells: z.record(z.string(), z.object({
|
|
6771
|
+
at: z.string().optional(),
|
|
6772
|
+
by: z.string().optional(),
|
|
6773
|
+
rev: z.string().optional(),
|
|
6774
|
+
source: z.string().optional(),
|
|
6775
|
+
conflicts: z.string().optional()
|
|
6776
|
+
}).strict()).optional(),
|
|
6777
|
+
/** Resolved-table view metadata (sources, prefetch/writeback config) for the data plane. */
|
|
6778
|
+
resolved: resolvedMetaIrSchema.optional(),
|
|
5105
6779
|
constraints: z.array(z.object({ name: z.string(), columns: z.array(z.string()) })),
|
|
5106
6780
|
/** Depth/node caps for dynamic filter trees on this table. */
|
|
5107
6781
|
dynamicFilterMaxDepth: z.number().int().positive().default(6),
|
|
@@ -5111,13 +6785,21 @@ var compiledArtifactSchema = z.object({
|
|
|
5111
6785
|
irVersion: irVersionSchema,
|
|
5112
6786
|
namespace: z.string(),
|
|
5113
6787
|
schemaHash: z.string(),
|
|
6788
|
+
ownSchemaHash: z.string().optional(),
|
|
6789
|
+
dependencies: z.record(z.string(), z.string()).optional(),
|
|
6790
|
+
readPolicies: z.record(z.string(), predIrSchema).optional(),
|
|
5114
6791
|
opsHash: z.string(),
|
|
6792
|
+
/** Apply-observed synced objects without a serving backing; not part of the authored schema hash. */
|
|
6793
|
+
unbackedTables: z.array(z.string()).optional(),
|
|
5115
6794
|
tables: z.record(z.string(), tableMetaIrSchema),
|
|
5116
6795
|
operations: z.record(z.string(), compiledOperationSchema)
|
|
5117
6796
|
}).superRefine((artifact, ctx) => {
|
|
5118
|
-
|
|
6797
|
+
if (Object.keys(artifact.dependencies ?? {}).length > 0 && artifact.irVersion < COMPOSED_IR_VERSION) {
|
|
6798
|
+
ctx.addIssue({ code: "custom", path: ["irVersion"], message: "cross-namespace dependencies require IR v3" });
|
|
6799
|
+
}
|
|
6800
|
+
for (const [name2, operation] of Object.entries(artifact.operations)) {
|
|
5119
6801
|
if (operation.irVersion > artifact.irVersion) {
|
|
5120
|
-
ctx.addIssue({ code: "custom", path: ["operations",
|
|
6802
|
+
ctx.addIssue({ code: "custom", path: ["operations", name2, "irVersion"], message: "operation requires a newer IR version than its artifact" });
|
|
5121
6803
|
}
|
|
5122
6804
|
}
|
|
5123
6805
|
});
|
|
@@ -5130,7 +6812,7 @@ var IrVersionError = class extends Error {
|
|
|
5130
6812
|
function normalizeIr(raw) {
|
|
5131
6813
|
if (!raw || typeof raw !== "object") throw new IrVersionError(void 0);
|
|
5132
6814
|
const version = raw.irVersion;
|
|
5133
|
-
if (version
|
|
6815
|
+
if (!irVersionSchema.safeParse(version).success) throw new IrVersionError(version);
|
|
5134
6816
|
return compiledArtifactSchema.parse(raw);
|
|
5135
6817
|
}
|
|
5136
6818
|
function indexByHash(artifact) {
|
|
@@ -5163,10 +6845,10 @@ function buildNamespaceDefinition(files, config) {
|
|
|
5163
6845
|
}
|
|
5164
6846
|
const actionsSeen = /* @__PURE__ */ new Map();
|
|
5165
6847
|
for (const { file, table } of files) {
|
|
5166
|
-
for (const
|
|
5167
|
-
const existing = actionsSeen.get(
|
|
5168
|
-
if (existing) throw new OrmDefinitionError(`action '${
|
|
5169
|
-
actionsSeen.set(
|
|
6848
|
+
for (const name2 of Object.keys(table.actions ?? {})) {
|
|
6849
|
+
const existing = actionsSeen.get(name2);
|
|
6850
|
+
if (existing) throw new OrmDefinitionError(`action '${name2}' is already defined in ${existing}`, file);
|
|
6851
|
+
actionsSeen.set(name2, file);
|
|
5170
6852
|
}
|
|
5171
6853
|
}
|
|
5172
6854
|
const capabilities = config?.capabilities ? { roles: config.capabilities.roles ?? EMPTY_CAPABILITIES.roles, uses: config.capabilities.uses ?? [] } : EMPTY_CAPABILITIES;
|
|
@@ -5209,12 +6891,12 @@ function validateActionValidates(files, byName) {
|
|
|
5209
6891
|
}
|
|
5210
6892
|
function validateIndexNames(files) {
|
|
5211
6893
|
const seen = /* @__PURE__ */ new Map();
|
|
5212
|
-
const claim = (
|
|
5213
|
-
const existing = seen.get(
|
|
6894
|
+
const claim = (name2, label, file) => {
|
|
6895
|
+
const existing = seen.get(name2);
|
|
5214
6896
|
if (existing) {
|
|
5215
|
-
throw new OrmDefinitionError(`relation name '${
|
|
6897
|
+
throw new OrmDefinitionError(`relation name '${name2}' (${label}) collides with ${existing}`, file);
|
|
5216
6898
|
}
|
|
5217
|
-
seen.set(
|
|
6899
|
+
seen.set(name2, `${label} (${file})`);
|
|
5218
6900
|
};
|
|
5219
6901
|
for (const { file, table } of files) {
|
|
5220
6902
|
claim(table.table, `table '${table.table}'`, file);
|
|
@@ -5222,9 +6904,9 @@ function validateIndexNames(files) {
|
|
|
5222
6904
|
for (const { file, table } of files) {
|
|
5223
6905
|
if (table.source) continue;
|
|
5224
6906
|
claim(`${table.table}_pkey`, `implicit primary-key index of '${table.table}'`, file);
|
|
5225
|
-
for (const [
|
|
6907
|
+
for (const [name2, column] of Object.entries(table.columns ?? {})) {
|
|
5226
6908
|
if (column.unique && !column.primary_key) {
|
|
5227
|
-
claim(`${table.table}_${
|
|
6909
|
+
claim(`${table.table}_${name2}_unique`, `implicit unique constraint of '${table.table}.${name2}'`, file);
|
|
5228
6910
|
}
|
|
5229
6911
|
}
|
|
5230
6912
|
}
|
|
@@ -5236,20 +6918,20 @@ function validateIndexNames(files) {
|
|
|
5236
6918
|
}
|
|
5237
6919
|
function validateLocalLinks(files, byName) {
|
|
5238
6920
|
for (const { file, table } of files) {
|
|
5239
|
-
for (const [
|
|
6921
|
+
for (const [name2, link] of Object.entries(table.links ?? {})) {
|
|
5240
6922
|
if (isQualifiedReference(link.to)) continue;
|
|
5241
6923
|
const target = byName.get(link.to);
|
|
5242
6924
|
if (!target) {
|
|
5243
|
-
throw new OrmDefinitionError(`link '${
|
|
6925
|
+
throw new OrmDefinitionError(`link '${name2}' references unknown table '${link.to}'`, file);
|
|
5244
6926
|
}
|
|
5245
6927
|
if (link.enforce && target.source) {
|
|
5246
|
-
throw new OrmDefinitionError(`link '${
|
|
6928
|
+
throw new OrmDefinitionError(`link '${name2}' cannot enforce a foreign key onto a synced table`, file);
|
|
5247
6929
|
}
|
|
5248
6930
|
const sourceType = table.columns?.[link.on]?.type;
|
|
5249
6931
|
const targetPrimaryKey = Object.values(target.columns ?? {}).find((column) => column.primary_key);
|
|
5250
6932
|
if (sourceType && targetPrimaryKey && columnTypeGroup(sourceType) !== columnTypeGroup(targetPrimaryKey.type)) {
|
|
5251
6933
|
throw new OrmDefinitionError(
|
|
5252
|
-
`link '${
|
|
6934
|
+
`link '${name2}' can never join: '${link.on}' is ${sourceType} but '${link.to}' has a ${targetPrimaryKey.type} primary key`,
|
|
5253
6935
|
file
|
|
5254
6936
|
);
|
|
5255
6937
|
}
|
|
@@ -5329,7 +7011,7 @@ function parseYaml(file, contents) {
|
|
|
5329
7011
|
throw new OrmDefinitionError(error instanceof Error ? error.message : "invalid YAML", file);
|
|
5330
7012
|
}
|
|
5331
7013
|
}
|
|
5332
|
-
var isDefinitionFile = (
|
|
7014
|
+
var isDefinitionFile = (name2) => DEFINITION_FILE_EXTENSIONS.some((extension) => extname(name2) === extension);
|
|
5333
7015
|
async function listDefinitionFiles(dir) {
|
|
5334
7016
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
5335
7017
|
return entries.filter((entry) => entry.isFile() && isDefinitionFile(entry.name)).map((entry) => entry.name).sort();
|
|
@@ -5423,13 +7105,13 @@ async function readMigrationSql(dir, entry) {
|
|
|
5423
7105
|
var checksumOf = (text4) => createHash("sha256").update(text4).digest("hex");
|
|
5424
7106
|
var migrationName = (ordinal, contentHash) => `${pad(ordinal)}_${contentHash.slice(0, 12)}`;
|
|
5425
7107
|
function renderMigrationSql({
|
|
5426
|
-
name,
|
|
7108
|
+
name: name2,
|
|
5427
7109
|
namespace,
|
|
5428
7110
|
contentHash,
|
|
5429
7111
|
plan
|
|
5430
7112
|
}) {
|
|
5431
7113
|
const header = [
|
|
5432
|
-
`-- ${
|
|
7114
|
+
`-- ${name2}`,
|
|
5433
7115
|
`-- namespace: ${namespace}`,
|
|
5434
7116
|
`-- content hash: ${contentHash}`,
|
|
5435
7117
|
"-- generated by `seq-studio orm diff` \u2014 review and commit, do not edit"
|
|
@@ -5445,13 +7127,13 @@ async function writeMigration({
|
|
|
5445
7127
|
}) {
|
|
5446
7128
|
const tip = workspace.journal.entries.at(-1);
|
|
5447
7129
|
const ordinal = (tip?.ordinal ?? 0) + 1;
|
|
5448
|
-
const
|
|
5449
|
-
const
|
|
7130
|
+
const name2 = migrationName(ordinal, contentHash);
|
|
7131
|
+
const sql7 = renderMigrationSql({ name: name2, namespace: workspace.journal.namespace, contentHash, plan });
|
|
5450
7132
|
const metaDir = join(dir, MIGRATIONS_DIRNAME, META_DIRNAME);
|
|
5451
7133
|
await mkdir(metaDir, { recursive: true });
|
|
5452
7134
|
const snapshotText = `${JSON.stringify(plan.snapshot, null, 2)}
|
|
5453
7135
|
`;
|
|
5454
|
-
await writeFile(join(dir, MIGRATIONS_DIRNAME, `${
|
|
7136
|
+
await writeFile(join(dir, MIGRATIONS_DIRNAME, `${name2}.sql`), sql7);
|
|
5455
7137
|
await writeFile(join(metaDir, `${pad(ordinal)}.snapshot.json`), snapshotText);
|
|
5456
7138
|
const journal = {
|
|
5457
7139
|
...workspace.journal,
|
|
@@ -5459,10 +7141,10 @@ async function writeMigration({
|
|
|
5459
7141
|
...workspace.journal.entries,
|
|
5460
7142
|
{
|
|
5461
7143
|
ordinal,
|
|
5462
|
-
name,
|
|
7144
|
+
name: name2,
|
|
5463
7145
|
content_hash: contentHash,
|
|
5464
7146
|
previous_content_hash: tip?.content_hash ?? null,
|
|
5465
|
-
checksum: checksumOf(
|
|
7147
|
+
checksum: checksumOf(sql7),
|
|
5466
7148
|
snapshot_checksum: checksumOf(snapshotText),
|
|
5467
7149
|
destructive
|
|
5468
7150
|
}
|
|
@@ -5470,7 +7152,7 @@ async function writeMigration({
|
|
|
5470
7152
|
};
|
|
5471
7153
|
await writeFile(join(metaDir, JOURNAL_FILENAME), `${JSON.stringify(journal, null, 2)}
|
|
5472
7154
|
`);
|
|
5473
|
-
return { name, ordinal, destructive };
|
|
7155
|
+
return { name: name2, ordinal, destructive };
|
|
5474
7156
|
}
|
|
5475
7157
|
var pad = (ordinal) => String(ordinal).padStart(4, "0");
|
|
5476
7158
|
function parseJson(path, raw) {
|
|
@@ -5577,8 +7259,8 @@ async function checkWorkspace({
|
|
|
5577
7259
|
let previousId3;
|
|
5578
7260
|
let previousHash = null;
|
|
5579
7261
|
for (const entry of workspace.journal.entries) {
|
|
5580
|
-
const
|
|
5581
|
-
if (checksumOf(
|
|
7262
|
+
const sql7 = await readMigrationSql(dir, entry);
|
|
7263
|
+
if (checksumOf(sql7) !== entry.checksum) {
|
|
5582
7264
|
log(`${MIGRATIONS_DIRNAME}/${entry.name}.sql does not match its journal checksum \u2014 committed migrations must not be edited`);
|
|
5583
7265
|
return 1;
|
|
5584
7266
|
}
|
|
@@ -5618,7 +7300,7 @@ async function checkWorkspace({
|
|
|
5618
7300
|
log(`migrations in sync \u2014 ${workspace.journal.entries.length} committed, tip ${tip.name}`);
|
|
5619
7301
|
return 0;
|
|
5620
7302
|
}
|
|
5621
|
-
var firstLine = (
|
|
7303
|
+
var firstLine = (sql7) => sql7.split("\n")[0] ?? sql7;
|
|
5622
7304
|
|
|
5623
7305
|
// src/migrations/from-yaml.ts
|
|
5624
7306
|
function convertV1DefinitionToCanonical(definition) {
|
|
@@ -5644,15 +7326,15 @@ function convertV1DefinitionToCanonical(definition) {
|
|
|
5644
7326
|
}
|
|
5645
7327
|
function convertTable(table) {
|
|
5646
7328
|
const columns = Object.entries(table.columns ?? {});
|
|
5647
|
-
const primaryKey2 = columns.filter(([, column]) => column.primary_key).map(([
|
|
7329
|
+
const primaryKey2 = columns.filter(([, column]) => column.primary_key).map(([name2]) => name2);
|
|
5648
7330
|
return {
|
|
5649
7331
|
table: table.table,
|
|
5650
7332
|
description: table.description,
|
|
5651
7333
|
source: table.source,
|
|
5652
|
-
columns: columns.map(([
|
|
7334
|
+
columns: columns.map(([name2, column]) => convertColumn(name2, column)),
|
|
5653
7335
|
// Synced tables may declare a typed column/PK contract; untyped remain empty.
|
|
5654
7336
|
primaryKey: primaryKey2.length > 0 ? primaryKey2 : void 0,
|
|
5655
|
-
links: Object.entries(table.links ?? {}).map(([
|
|
7337
|
+
links: Object.entries(table.links ?? {}).map(([name2, link]) => convertLink(table.table, name2, link)),
|
|
5656
7338
|
indexes: (table.indexes ?? []).map((index3) => ({
|
|
5657
7339
|
columns: [...index3.columns],
|
|
5658
7340
|
unique: index3.unique ?? false,
|
|
@@ -5669,11 +7351,11 @@ function convertTable(table) {
|
|
|
5669
7351
|
actions: []
|
|
5670
7352
|
};
|
|
5671
7353
|
}
|
|
5672
|
-
function convertColumn(
|
|
7354
|
+
function convertColumn(name2, column) {
|
|
5673
7355
|
const isDefaultFn = column.default !== void 0 && isFunctionDefault(column.default, column.type);
|
|
5674
7356
|
return {
|
|
5675
|
-
name,
|
|
5676
|
-
columnName:
|
|
7357
|
+
name: name2,
|
|
7358
|
+
columnName: name2,
|
|
5677
7359
|
type: column.type,
|
|
5678
7360
|
nullable: column.primary_key ? false : column.nullable !== false,
|
|
5679
7361
|
unique: column.unique === true,
|
|
@@ -5687,14 +7369,14 @@ function convertColumn(name, column) {
|
|
|
5687
7369
|
deprecated: column.deprecated === true
|
|
5688
7370
|
};
|
|
5689
7371
|
}
|
|
5690
|
-
function convertLink(table,
|
|
7372
|
+
function convertLink(table, name2, link) {
|
|
5691
7373
|
if (link.to.includes(".")) {
|
|
5692
7374
|
throw new OrmDefinitionError(
|
|
5693
|
-
`link '${
|
|
7375
|
+
`link '${name2}' on '${table}' targets '${link.to}' \u2014 cross-namespace links have no canonical v2 representation (links are namespace-local in v2)`
|
|
5694
7376
|
);
|
|
5695
7377
|
}
|
|
5696
7378
|
return {
|
|
5697
|
-
name,
|
|
7379
|
+
name: name2,
|
|
5698
7380
|
kind: "toOne",
|
|
5699
7381
|
target: link.to,
|
|
5700
7382
|
on: link.on,
|
|
@@ -5723,29 +7405,29 @@ function buildRebaseJournalEntry({ namespace, previousHash, newHash, ordinal, sn
|
|
|
5723
7405
|
if (!Number.isInteger(ordinal) || ordinal < 2) {
|
|
5724
7406
|
throw new OrmDefinitionError(`rebase ordinal must be an integer \u2265 2 (a rebase sits on an existing journal tip); got ${ordinal}`);
|
|
5725
7407
|
}
|
|
5726
|
-
const
|
|
5727
|
-
const
|
|
7408
|
+
const name2 = migrationName(ordinal, newHash);
|
|
7409
|
+
const sql7 = renderRebaseMigrationSql({ name: name2, namespace, previousHash, newHash });
|
|
5728
7410
|
const snapshotText = `${JSON.stringify(snapshot, null, 2)}
|
|
5729
7411
|
`;
|
|
5730
7412
|
const entry = {
|
|
5731
7413
|
ordinal,
|
|
5732
|
-
name,
|
|
7414
|
+
name: name2,
|
|
5733
7415
|
content_hash: newHash,
|
|
5734
7416
|
previous_content_hash: previousHash,
|
|
5735
|
-
checksum: checksumOf(
|
|
7417
|
+
checksum: checksumOf(sql7),
|
|
5736
7418
|
snapshot_checksum: checksumOf(snapshotText),
|
|
5737
7419
|
destructive: false
|
|
5738
7420
|
};
|
|
5739
|
-
return { name, entry, sql:
|
|
7421
|
+
return { name: name2, entry, sql: sql7, snapshotText };
|
|
5740
7422
|
}
|
|
5741
7423
|
function renderRebaseMigrationSql({
|
|
5742
|
-
name,
|
|
7424
|
+
name: name2,
|
|
5743
7425
|
namespace,
|
|
5744
7426
|
previousHash,
|
|
5745
7427
|
newHash
|
|
5746
7428
|
}) {
|
|
5747
7429
|
return [
|
|
5748
|
-
`-- ${
|
|
7430
|
+
`-- ${name2}`,
|
|
5749
7431
|
`-- namespace: ${namespace}`,
|
|
5750
7432
|
`-- content hash: ${newHash}`,
|
|
5751
7433
|
`-- rebase: v1 definitions (${previousHash}) converted to canonical v2 \u2014 verified no-op, zero statements`,
|
|
@@ -5819,8 +7501,8 @@ function canonicalActions(value) {
|
|
|
5819
7501
|
const entries = Object.entries(value);
|
|
5820
7502
|
if (entries.length === 0) return void 0;
|
|
5821
7503
|
return Object.fromEntries(
|
|
5822
|
-
entries.map(([
|
|
5823
|
-
|
|
7504
|
+
entries.map(([name2, action]) => [
|
|
7505
|
+
name2,
|
|
5824
7506
|
{
|
|
5825
7507
|
description: action["description"] ?? void 0,
|
|
5826
7508
|
requires: action["requires"] ?? void 0,
|
|
@@ -5833,7 +7515,527 @@ function canonicalActions(value) {
|
|
|
5833
7515
|
])
|
|
5834
7516
|
);
|
|
5835
7517
|
}
|
|
7518
|
+
var IMPORT_CONFLICT_MODES = ["error", "skip", "update"];
|
|
7519
|
+
var columnMapSchema = z.object({
|
|
7520
|
+
/** ORM property name or Postgres column name. */
|
|
7521
|
+
target: z.string().min(1).max(63),
|
|
7522
|
+
/** Column on the source relation. Identifiers only — never an expression. */
|
|
7523
|
+
source: z.string().min(1).max(63),
|
|
7524
|
+
/**
|
|
7525
|
+
* Used when the source value is NULL. Required columns without a database
|
|
7526
|
+
* default fail the load if the source contains nulls and this is omitted.
|
|
7527
|
+
*/
|
|
7528
|
+
nullDefault: z.union([z.string().max(1e4), z.number(), z.boolean()]).optional()
|
|
7529
|
+
}).strict();
|
|
7530
|
+
var importSpecSchema = z.object({
|
|
7531
|
+
table: z.string().min(1).max(63),
|
|
7532
|
+
from: z.string().regex(QUALIFIED_TABLE_PATTERN),
|
|
7533
|
+
columns: z.array(columnMapSchema).min(1).max(500),
|
|
7534
|
+
onConflict: z.enum(IMPORT_CONFLICT_MODES).default("skip"),
|
|
7535
|
+
batchSize: z.number().int().min(1).max(2e4).default(5e3)
|
|
7536
|
+
}).strict();
|
|
7537
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
7538
|
+
var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
7539
|
+
var TIMESTAMPTZ_RE = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})$/;
|
|
7540
|
+
var INTEGER_RE = /^-?\d+$/;
|
|
7541
|
+
var NUMERIC_RE = /^-?\d+(\.\d+)?$/;
|
|
7542
|
+
function sourceSchemaAllowed(schema) {
|
|
7543
|
+
return schema !== "information_schema" && !schema.startsWith("pg_");
|
|
7544
|
+
}
|
|
7545
|
+
function isRelationIdentifier(value) {
|
|
7546
|
+
return value.length <= 63 && IDENTIFIER_PATTERN.test(value);
|
|
7547
|
+
}
|
|
7548
|
+
function isSourceColumn(value) {
|
|
7549
|
+
return value.length <= 63 && COLUMN_IDENTIFIER_PATTERN.test(value);
|
|
7550
|
+
}
|
|
7551
|
+
function splitQualifiedRelation(from) {
|
|
7552
|
+
const dot = from.indexOf(".");
|
|
7553
|
+
if (dot <= 0 || dot !== from.lastIndexOf(".")) return null;
|
|
7554
|
+
const schema = from.slice(0, dot);
|
|
7555
|
+
const table = from.slice(dot + 1);
|
|
7556
|
+
if (!isRelationIdentifier(schema) || !isRelationIdentifier(table)) return null;
|
|
7557
|
+
return { schema, table };
|
|
7558
|
+
}
|
|
7559
|
+
function postgresTypeName(type) {
|
|
7560
|
+
if (type === "double") return "double precision";
|
|
7561
|
+
if (type === "timestamptz") return "timestamp with time zone";
|
|
7562
|
+
return type;
|
|
7563
|
+
}
|
|
7564
|
+
function columnPostgresType(column, schemaName, enums) {
|
|
7565
|
+
if (typeof column.type === "string") return postgresTypeName(column.type);
|
|
7566
|
+
if (!enums.has(column.type.enum)) return null;
|
|
7567
|
+
return `${quoteIdentifier(schemaName)}.${quoteIdentifier(column.type.enum)}`;
|
|
7568
|
+
}
|
|
7569
|
+
function quoteQualified(schema, table) {
|
|
7570
|
+
return `${quoteIdentifier(schema)}.${quoteIdentifier(table)}`;
|
|
7571
|
+
}
|
|
7572
|
+
var INT4_MIN = -2147483648;
|
|
7573
|
+
var INT4_MAX = 2147483647;
|
|
7574
|
+
function resumeLiteralSql({
|
|
7575
|
+
column,
|
|
7576
|
+
after,
|
|
7577
|
+
schemaName,
|
|
7578
|
+
enums
|
|
7579
|
+
}) {
|
|
7580
|
+
const coerced = coerceResumeValue(column, after);
|
|
7581
|
+
if (!coerced.ok) return coerced;
|
|
7582
|
+
return literalSql({ column, value: coerced.value, schemaName, enums });
|
|
7583
|
+
}
|
|
7584
|
+
function coerceResumeValue(column, after) {
|
|
7585
|
+
const type = typeof column.type === "string" ? column.type : null;
|
|
7586
|
+
if (type === "integer") {
|
|
7587
|
+
if (!INTEGER_RE.test(after)) return { ok: false, error: `${column.name}: after must be an integer` };
|
|
7588
|
+
const parsed = Number(after);
|
|
7589
|
+
if (!Number.isSafeInteger(parsed) || parsed < INT4_MIN || parsed > INT4_MAX) {
|
|
7590
|
+
return { ok: false, error: `${column.name}: after must be an integer` };
|
|
7591
|
+
}
|
|
7592
|
+
return { ok: true, value: parsed };
|
|
7593
|
+
}
|
|
7594
|
+
if (type === "boolean") {
|
|
7595
|
+
if (after !== "true" && after !== "false") return { ok: false, error: `${column.name}: after must be true or false` };
|
|
7596
|
+
return { ok: true, value: after === "true" };
|
|
7597
|
+
}
|
|
7598
|
+
return { ok: true, value: after };
|
|
7599
|
+
}
|
|
7600
|
+
function literalSql({
|
|
7601
|
+
column,
|
|
7602
|
+
value,
|
|
7603
|
+
schemaName,
|
|
7604
|
+
enums
|
|
7605
|
+
}) {
|
|
7606
|
+
const label = column.name;
|
|
7607
|
+
if (typeof value === "string" && value.includes("\0")) {
|
|
7608
|
+
return { ok: false, error: `${label}: null default contains a NUL character` };
|
|
7609
|
+
}
|
|
7610
|
+
if (typeof column.type !== "string") {
|
|
7611
|
+
const enumDef = enums.get(column.type.enum);
|
|
7612
|
+
if (!enumDef) return { ok: false, error: `${label}: enum '${column.type.enum}' is not declared` };
|
|
7613
|
+
if (typeof value !== "string" || !enumDef.values.includes(value)) {
|
|
7614
|
+
return { ok: false, error: `${label}: null default must be one of ${enumDef.values.join(", ")}` };
|
|
7615
|
+
}
|
|
7616
|
+
const typeName = columnPostgresType(column, schemaName, enums);
|
|
7617
|
+
return { ok: true, sql: `${quoteLiteral(value)}::${typeName}` };
|
|
7618
|
+
}
|
|
7619
|
+
return scalarLiteral2({ column, type: column.type, value, label });
|
|
7620
|
+
}
|
|
7621
|
+
var SCALAR_LITERALS = {
|
|
7622
|
+
text: (value, label) => typeof value === "string" ? { ok: true, sql: quoteLiteral(value) } : { ok: false, error: `${label}: null default must be a string` },
|
|
7623
|
+
uuid: (value, label) => typeof value === "string" && UUID_RE.test(value) ? { ok: true, sql: `${quoteLiteral(value)}::uuid` } : { ok: false, error: `${label}: null default must be a UUID` },
|
|
7624
|
+
boolean: (value, label) => typeof value === "boolean" ? { ok: true, sql: value ? "TRUE" : "FALSE" } : { ok: false, error: `${label}: null default must be true or false` },
|
|
7625
|
+
integer: (value, label) => typeof value === "number" && Number.isSafeInteger(value) ? { ok: true, sql: String(value) } : { ok: false, error: `${label}: null default must be an integer` },
|
|
7626
|
+
bigint: (value, label) => typeof value === "string" && INTEGER_RE.test(value) ? { ok: true, sql: `${value}::bigint` } : { ok: false, error: `${label}: null default must be a base-10 integer string` },
|
|
7627
|
+
numeric: numericLiteral,
|
|
7628
|
+
double: numericLiteral,
|
|
7629
|
+
date: (value, label) => typeof value === "string" && DATE_RE.test(value) ? { ok: true, sql: `${quoteLiteral(value)}::date` } : { ok: false, error: `${label}: null default must be a YYYY-MM-DD date` },
|
|
7630
|
+
timestamptz: (value, label) => typeof value === "string" && TIMESTAMPTZ_RE.test(value) ? { ok: true, sql: `${quoteLiteral(value)}::${postgresTypeName("timestamptz")}` } : { ok: false, error: `${label}: null default must be an ISO-8601 timestamp` },
|
|
7631
|
+
jsonb: jsonbLiteral
|
|
7632
|
+
};
|
|
7633
|
+
function numericLiteral(value, label) {
|
|
7634
|
+
if (typeof value === "number" && Number.isFinite(value)) return { ok: true, sql: String(value) };
|
|
7635
|
+
if (typeof value === "string" && NUMERIC_RE.test(value)) return { ok: true, sql: value };
|
|
7636
|
+
return { ok: false, error: `${label}: null default must be a finite number` };
|
|
7637
|
+
}
|
|
7638
|
+
function scalarLiteral2({
|
|
7639
|
+
column,
|
|
7640
|
+
type,
|
|
7641
|
+
value,
|
|
7642
|
+
label
|
|
7643
|
+
}) {
|
|
7644
|
+
const encode = SCALAR_LITERALS[type];
|
|
7645
|
+
if (!encode) return { ok: false, error: `${column.name}: unsupported type '${type}'` };
|
|
7646
|
+
return encode(value, label);
|
|
7647
|
+
}
|
|
7648
|
+
function jsonbLiteral(value, label) {
|
|
7649
|
+
if (typeof value !== "string") return { ok: false, error: `${label}: null default must be a JSON string` };
|
|
7650
|
+
try {
|
|
7651
|
+
JSON.parse(value);
|
|
7652
|
+
} catch {
|
|
7653
|
+
return { ok: false, error: `${label}: null default must be valid JSON` };
|
|
7654
|
+
}
|
|
7655
|
+
return { ok: true, sql: `${quoteLiteral(value)}::jsonb` };
|
|
7656
|
+
}
|
|
7657
|
+
function dollarQuote(body) {
|
|
7658
|
+
const digest = createHash("sha256").update(body).digest("hex");
|
|
7659
|
+
const candidates = ["seq_orm_import", `seq_orm_${digest.slice(0, 16)}`, `seq_orm_${digest}`];
|
|
7660
|
+
const tag = candidates.find((candidate) => !body.includes(`$${candidate}$`)) ?? `seq_orm_${digest}_body`;
|
|
7661
|
+
return `$${tag}$${body}$${tag}$`;
|
|
7662
|
+
}
|
|
7663
|
+
|
|
7664
|
+
// src/import/script.ts
|
|
7665
|
+
function renderImportScript({
|
|
7666
|
+
namespace,
|
|
7667
|
+
schemaName,
|
|
7668
|
+
table,
|
|
7669
|
+
source,
|
|
7670
|
+
mapped,
|
|
7671
|
+
primaryKey: primaryKey2,
|
|
7672
|
+
onConflict,
|
|
7673
|
+
batchSize,
|
|
7674
|
+
omitted,
|
|
7675
|
+
resume
|
|
7676
|
+
}) {
|
|
7677
|
+
const sourceSql = `${quoteQualified(source.schema, source.table)} AS src`;
|
|
7678
|
+
const targetSql = quoteQualified(schemaName, table.table);
|
|
7679
|
+
const keys = primaryKey2.map((name2) => mapped.find((column) => column.column.columnName === name2)).filter((column) => column !== void 0);
|
|
7680
|
+
const probeSql = renderProbe({ sourceSql, keys, mapped });
|
|
7681
|
+
const procedure = renderProcedure({ targetSql, sourceSql, mapped, keys, onConflict, batchSize });
|
|
7682
|
+
const call = `CALL pg_temp.seq_orm_import(${resume.join(", ")});`;
|
|
7683
|
+
const omittedLine = omitted.length > 0 ? `-- Omitted columns keep their database default or NULL: ${omitted.join(", ")}` : "-- Every column is mapped.";
|
|
7684
|
+
const text4 = [
|
|
7685
|
+
"-- Sequence ORM data import",
|
|
7686
|
+
`-- namespace: ${namespace}`,
|
|
7687
|
+
`-- target: ${schemaName}.${table.table}`,
|
|
7688
|
+
`-- source: ${source.schema}.${source.table}`,
|
|
7689
|
+
`-- conflict: ${onConflict}`,
|
|
7690
|
+
`-- batch size: ${batchSize}`,
|
|
7691
|
+
"--",
|
|
7692
|
+
"-- Runs as the namespace owner. Does not evaluate insert or update policies,",
|
|
7693
|
+
"-- capability gates, or the operation journal. Sync the warehouse table into",
|
|
7694
|
+
"-- this Postgres relation first, review the probe, then run the load.",
|
|
7695
|
+
"-- skip and update are safe to re-run from the start after a failure. error stops on an existing key.",
|
|
7696
|
+
"-- Each batch commits. Do not resume from the maximum key in the target:",
|
|
7697
|
+
"-- a higher key that was already there would skip source rows this run never copied.",
|
|
7698
|
+
"-- --after is only the last key this procedure committed. This script does not print it.",
|
|
7699
|
+
omittedLine,
|
|
7700
|
+
"-- cast_* columns force every mapped value through the target type. An invalid value aborts before any insert.",
|
|
7701
|
+
"",
|
|
7702
|
+
probeSql,
|
|
7703
|
+
"",
|
|
7704
|
+
procedure,
|
|
7705
|
+
call,
|
|
7706
|
+
""
|
|
7707
|
+
].join("\n");
|
|
7708
|
+
return { probeSql, text: text4 };
|
|
7709
|
+
}
|
|
7710
|
+
function renderProbe({
|
|
7711
|
+
sourceSql,
|
|
7712
|
+
keys,
|
|
7713
|
+
mapped
|
|
7714
|
+
}) {
|
|
7715
|
+
const nullKey = keys.map((key) => `${key.expression} IS NULL`).join(" OR ");
|
|
7716
|
+
const distinct = keys.length === 1 ? keys[0].expression : `(${keys.map((key) => key.expression).join(", ")})`;
|
|
7717
|
+
const requiredNulls = nullChecked(mapped, keys).map((column) => `count(*) FILTER (WHERE src.${quoteIdentifier(column.source)} IS NULL) AS ${quoteIdentifier(`null_${column.column.columnName}`)}`);
|
|
7718
|
+
const selects = [
|
|
7719
|
+
"count(*) AS source_rows",
|
|
7720
|
+
`count(*) FILTER (WHERE ${nullKey}) AS null_primary_key`,
|
|
7721
|
+
`count(*) FILTER (WHERE NOT (${nullKey})) - count(DISTINCT ${distinct}) AS duplicate_primary_key`,
|
|
7722
|
+
...requiredNulls,
|
|
7723
|
+
...mapped.map((column) => `count(${column.expression}) AS ${quoteIdentifier(`cast_${column.column.columnName}`)}`)
|
|
7724
|
+
];
|
|
7725
|
+
return `SELECT
|
|
7726
|
+
${selects.join(",\n ")}
|
|
7727
|
+
FROM ${sourceSql};`;
|
|
7728
|
+
}
|
|
7729
|
+
function renderProcedure({
|
|
7730
|
+
targetSql,
|
|
7731
|
+
sourceSql,
|
|
7732
|
+
mapped,
|
|
7733
|
+
keys,
|
|
7734
|
+
onConflict,
|
|
7735
|
+
batchSize
|
|
7736
|
+
}) {
|
|
7737
|
+
const args = keys.map((key, index3) => `IN start_${index3} ${key.pgType} DEFAULT NULL`).join(", ");
|
|
7738
|
+
const declarations = [
|
|
7739
|
+
"DECLARE",
|
|
7740
|
+
" batch_scanned integer;",
|
|
7741
|
+
" batch_affected integer;",
|
|
7742
|
+
" total_rows bigint;",
|
|
7743
|
+
" null_keys bigint;",
|
|
7744
|
+
" distinct_keys bigint;",
|
|
7745
|
+
...keys.map((key, index3) => ` watermark_${index3} ${key.pgType};`),
|
|
7746
|
+
...requiredNullDeclarations(mapped, keys),
|
|
7747
|
+
...mapped.map((column) => ` cast_${column.column.columnName} bigint;`)
|
|
7748
|
+
];
|
|
7749
|
+
const body = [
|
|
7750
|
+
"BEGIN",
|
|
7751
|
+
...keys.map((_, index3) => ` watermark_${index3} := start_${index3};`),
|
|
7752
|
+
` ${renderGuard({ sourceSql, keys, mapped })}`,
|
|
7753
|
+
" LOOP",
|
|
7754
|
+
` ${renderPage({ targetSql, sourceSql, mapped, keys, onConflict, batchSize })}`,
|
|
7755
|
+
" EXIT WHEN batch_scanned = 0;",
|
|
7756
|
+
" RAISE NOTICE 'orm import scanned % affected %', batch_scanned, batch_affected;",
|
|
7757
|
+
" COMMIT;",
|
|
7758
|
+
" END LOOP;",
|
|
7759
|
+
"END"
|
|
7760
|
+
].join("\n");
|
|
7761
|
+
return [
|
|
7762
|
+
`CREATE OR REPLACE PROCEDURE pg_temp.seq_orm_import(${args})`,
|
|
7763
|
+
"LANGUAGE plpgsql",
|
|
7764
|
+
`AS ${dollarQuote(`
|
|
7765
|
+
${declarations.join("\n")}
|
|
7766
|
+
${body}
|
|
7767
|
+
`)}`,
|
|
7768
|
+
";",
|
|
7769
|
+
""
|
|
7770
|
+
].join("\n");
|
|
7771
|
+
}
|
|
7772
|
+
function nullChecked(mapped, keys) {
|
|
7773
|
+
return mapped.filter((column) => !column.fillsNull && column.column.nullable === false && !keys.includes(column));
|
|
7774
|
+
}
|
|
7775
|
+
function requiredNullDeclarations(mapped, keys) {
|
|
7776
|
+
return nullChecked(mapped, keys).map((column) => ` null_${column.column.columnName} bigint;`);
|
|
7777
|
+
}
|
|
7778
|
+
function renderGuard({
|
|
7779
|
+
sourceSql,
|
|
7780
|
+
keys,
|
|
7781
|
+
mapped
|
|
7782
|
+
}) {
|
|
7783
|
+
const nullKey = keys.map((key) => `${key.expression} IS NULL`).join(" OR ");
|
|
7784
|
+
const distinct = keys.length === 1 ? keys[0].expression : `(${keys.map((key) => key.expression).join(", ")})`;
|
|
7785
|
+
const nullColumns = nullChecked(mapped, keys);
|
|
7786
|
+
const into = [
|
|
7787
|
+
"total_rows",
|
|
7788
|
+
"null_keys",
|
|
7789
|
+
"distinct_keys",
|
|
7790
|
+
...nullColumns.map((column) => `null_${column.column.columnName}`),
|
|
7791
|
+
...mapped.map((column) => `cast_${column.column.columnName}`)
|
|
7792
|
+
];
|
|
7793
|
+
const selects = [
|
|
7794
|
+
"count(*)::bigint",
|
|
7795
|
+
`count(*) FILTER (WHERE ${nullKey})::bigint`,
|
|
7796
|
+
`count(DISTINCT ${distinct})::bigint`,
|
|
7797
|
+
...nullColumns.map((column) => `count(*) FILTER (WHERE src.${quoteIdentifier(column.source)} IS NULL)::bigint`),
|
|
7798
|
+
...mapped.map((column) => `count(${column.expression})::bigint`)
|
|
7799
|
+
];
|
|
7800
|
+
const checks = [
|
|
7801
|
+
"IF null_keys > 0 THEN RAISE EXCEPTION 'import refused: source primary key has % null values', null_keys; END IF;",
|
|
7802
|
+
"IF total_rows <> distinct_keys THEN RAISE EXCEPTION 'import refused: source primary key has % duplicate values', total_rows - distinct_keys; END IF;",
|
|
7803
|
+
...nullColumns.map(
|
|
7804
|
+
(column) => `IF null_${column.column.columnName} > 0 THEN RAISE EXCEPTION 'import refused: source column ${column.source} is null on % rows and ${column.column.columnName} is required', null_${column.column.columnName}; END IF;`
|
|
7805
|
+
)
|
|
7806
|
+
];
|
|
7807
|
+
return [`SELECT ${selects.join(", ")}`, ` INTO ${into.join(", ")}`, ` FROM ${sourceSql};`, ...checks.map((check) => ` ${check}`)].join("\n");
|
|
7808
|
+
}
|
|
7809
|
+
function renderPage({
|
|
7810
|
+
targetSql,
|
|
7811
|
+
sourceSql,
|
|
7812
|
+
mapped,
|
|
7813
|
+
keys,
|
|
7814
|
+
onConflict,
|
|
7815
|
+
batchSize
|
|
7816
|
+
}) {
|
|
7817
|
+
const keyExprs = keys.map((key) => key.expression);
|
|
7818
|
+
const where = `watermark_0 IS NULL OR (${keyExprs.join(", ")}) > (${keys.map((_, index3) => `watermark_${index3}`).join(", ")})`;
|
|
7819
|
+
const order = keyExprs.join(", ");
|
|
7820
|
+
const pageColumns = mapped.map((column) => `${column.expression} AS ${quoteIdentifier(column.column.columnName)}`);
|
|
7821
|
+
const insertColumns = mapped.map((column) => quoteIdentifier(column.column.columnName)).join(", ");
|
|
7822
|
+
const conflict = conflictClause(onConflict, keys, mapped);
|
|
7823
|
+
const keyOrder = keys.map((key) => `${quoteIdentifier(key.column.columnName)} DESC`).join(", ");
|
|
7824
|
+
const lastKeys = keys.map((key) => `(SELECT ${quoteIdentifier(key.column.columnName)} FROM page ORDER BY ${keyOrder} LIMIT 1)`).join(", ");
|
|
7825
|
+
const watermarkTargets = keys.map((_, index3) => `watermark_${index3}`).join(", ");
|
|
7826
|
+
return [
|
|
7827
|
+
"WITH page AS (",
|
|
7828
|
+
` SELECT ${pageColumns.join(", ")}`,
|
|
7829
|
+
` FROM ${sourceSql}`,
|
|
7830
|
+
` WHERE ${where}`,
|
|
7831
|
+
` ORDER BY ${order}`,
|
|
7832
|
+
` LIMIT ${batchSize}`,
|
|
7833
|
+
" ),",
|
|
7834
|
+
" ins AS (",
|
|
7835
|
+
` INSERT INTO ${targetSql} (${insertColumns})`,
|
|
7836
|
+
` SELECT ${insertColumns} FROM page`,
|
|
7837
|
+
` ${conflict}`,
|
|
7838
|
+
" RETURNING 1",
|
|
7839
|
+
" )",
|
|
7840
|
+
` SELECT (SELECT count(*)::int FROM page), (SELECT count(*)::int FROM ins), ${lastKeys}`,
|
|
7841
|
+
` INTO batch_scanned, batch_affected, ${watermarkTargets};`
|
|
7842
|
+
].join("\n");
|
|
7843
|
+
}
|
|
7844
|
+
function conflictClause(onConflict, keys, mapped) {
|
|
7845
|
+
if (onConflict === "error") return "";
|
|
7846
|
+
const target = keys.map((key) => quoteIdentifier(key.column.columnName)).join(", ");
|
|
7847
|
+
if (onConflict === "skip") return `ON CONFLICT (${target}) DO NOTHING`;
|
|
7848
|
+
const assignments = mapped.filter((column) => !keys.includes(column)).map((column) => `${quoteIdentifier(column.column.columnName)} = EXCLUDED.${quoteIdentifier(column.column.columnName)}`);
|
|
7849
|
+
return `ON CONFLICT (${target}) DO UPDATE SET ${assignments.join(", ")}`;
|
|
7850
|
+
}
|
|
7851
|
+
|
|
7852
|
+
// src/import/plan.ts
|
|
7853
|
+
function planTableImport({
|
|
7854
|
+
definition,
|
|
7855
|
+
spec,
|
|
7856
|
+
after
|
|
7857
|
+
}) {
|
|
7858
|
+
const parsedDefinition = canonicalDefinitionSchema.safeParse(definition);
|
|
7859
|
+
if (!parsedDefinition.success) return { ok: false, errors: ["namespace definition is not canonical ORM v2"] };
|
|
7860
|
+
const parsedSpec = importSpecSchema.safeParse(spec);
|
|
7861
|
+
if (!parsedSpec.success) {
|
|
7862
|
+
return { ok: false, errors: parsedSpec.error.issues.map((issue) => `${issue.path.join(".") || "spec"}: ${issue.message}`) };
|
|
7863
|
+
}
|
|
7864
|
+
return planParsed({ definition: parsedDefinition.data, spec: parsedSpec.data, after });
|
|
7865
|
+
}
|
|
7866
|
+
function planParsed({
|
|
7867
|
+
definition,
|
|
7868
|
+
spec,
|
|
7869
|
+
after
|
|
7870
|
+
}) {
|
|
7871
|
+
const source = splitQualifiedRelation(spec.from);
|
|
7872
|
+
const target = resolveTarget(definition, spec.table);
|
|
7873
|
+
const errors = [];
|
|
7874
|
+
if (!source) errors.push(`from '${spec.from}' is not a schema.table identifier`);
|
|
7875
|
+
else if (!sourceSchemaAllowed(source.schema)) errors.push(`from schema '${source.schema}' is not a warehouse relation`);
|
|
7876
|
+
if (!target.table) errors.push(...target.errors);
|
|
7877
|
+
if (!source || !target.table) return { ok: false, errors };
|
|
7878
|
+
const schemaName = ormSchemaName(definition.namespace);
|
|
7879
|
+
if (source.schema === schemaName && source.table === target.table.table) {
|
|
7880
|
+
errors.push(`from '${spec.from}' is the import target`);
|
|
7881
|
+
}
|
|
7882
|
+
const enums = new Map(definition.enums.map((entry) => [entry.name, entry]));
|
|
7883
|
+
const mapped = mapColumns({ table: target.table, spec, schemaName, enums, errors });
|
|
7884
|
+
const primaryKey2 = target.table.primaryKey ?? [];
|
|
7885
|
+
if (primaryKey2.length === 0) errors.push(`table '${target.table.table}' needs a primary key`);
|
|
7886
|
+
for (const key of primaryKey2) {
|
|
7887
|
+
if (!mapped.some((column) => column.column.columnName === key)) {
|
|
7888
|
+
errors.push(`primary key column '${key}' must be mapped so existing identities are preserved`);
|
|
7889
|
+
}
|
|
7890
|
+
}
|
|
7891
|
+
collectUnmapped({ table: target.table, mapped, errors });
|
|
7892
|
+
if (spec.onConflict === "update" && mapped.every((column) => primaryKey2.includes(column.column.columnName))) {
|
|
7893
|
+
errors.push('onConflict "update" needs a mapped column that is not part of the primary key');
|
|
7894
|
+
}
|
|
7895
|
+
const resume = resumeArguments({ mapped, primaryKey: primaryKey2, after, schemaName, enums, errors });
|
|
7896
|
+
if (errors.length > 0) return { ok: false, errors };
|
|
7897
|
+
const omitted = omittedColumns(target.table, mapped);
|
|
7898
|
+
const script = renderImportScript({
|
|
7899
|
+
namespace: definition.namespace,
|
|
7900
|
+
schemaName,
|
|
7901
|
+
table: target.table,
|
|
7902
|
+
source,
|
|
7903
|
+
mapped,
|
|
7904
|
+
primaryKey: primaryKey2,
|
|
7905
|
+
onConflict: spec.onConflict,
|
|
7906
|
+
batchSize: spec.batchSize,
|
|
7907
|
+
omitted,
|
|
7908
|
+
resume
|
|
7909
|
+
});
|
|
7910
|
+
const warnings = [
|
|
7911
|
+
"This load runs as the namespace owner. It does not evaluate insert or update policies.",
|
|
7912
|
+
...spec.onConflict === "error" ? ['onConflict "error" is not safe to re-run: the first existing key aborts the batch.'] : []
|
|
7913
|
+
];
|
|
7914
|
+
return {
|
|
7915
|
+
ok: true,
|
|
7916
|
+
plan: { probeSql: script.probeSql, script: script.text, batchSize: spec.batchSize, onConflict: spec.onConflict, warnings }
|
|
7917
|
+
};
|
|
7918
|
+
}
|
|
7919
|
+
function resolveTarget(definition, name2) {
|
|
7920
|
+
const table = definition.tables.find((candidate) => candidate.table === name2);
|
|
7921
|
+
if (!table) {
|
|
7922
|
+
const view = definition.views?.find((candidate) => candidate.view === name2);
|
|
7923
|
+
return { table: null, errors: [view ? `'${name2}' is a view, not an owned table` : `table '${name2}' is not in this namespace`] };
|
|
7924
|
+
}
|
|
7925
|
+
if (table.source !== void 0) return { table: null, errors: [`'${name2}' is a synced reflection and cannot be imported into`] };
|
|
7926
|
+
if (table.internal || table.materializes !== void 0) {
|
|
7927
|
+
return { table: null, errors: [`'${name2}' is an internal relation, not an import target`] };
|
|
7928
|
+
}
|
|
7929
|
+
return { table, errors: [] };
|
|
7930
|
+
}
|
|
7931
|
+
function mapColumns({
|
|
7932
|
+
table,
|
|
7933
|
+
spec,
|
|
7934
|
+
schemaName,
|
|
7935
|
+
enums,
|
|
7936
|
+
errors
|
|
7937
|
+
}) {
|
|
7938
|
+
const mapped = [];
|
|
7939
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7940
|
+
for (const entry of spec.columns) {
|
|
7941
|
+
const column = findColumn(table, entry.target);
|
|
7942
|
+
if (column === "ambiguous") {
|
|
7943
|
+
errors.push(`target '${entry.target}' matches more than one column`);
|
|
7944
|
+
continue;
|
|
7945
|
+
}
|
|
7946
|
+
if (!column) {
|
|
7947
|
+
errors.push(`target '${entry.target}' is not a column of '${table.table}'`);
|
|
7948
|
+
continue;
|
|
7949
|
+
}
|
|
7950
|
+
if (column.cell !== void 0) {
|
|
7951
|
+
errors.push(`target '${entry.target}' is cell metadata and cannot be imported`);
|
|
7952
|
+
continue;
|
|
7953
|
+
}
|
|
7954
|
+
if (seen.has(column.columnName)) {
|
|
7955
|
+
errors.push(`column '${column.columnName}' is mapped more than once`);
|
|
7956
|
+
continue;
|
|
7957
|
+
}
|
|
7958
|
+
if (!isSourceColumn(entry.source)) {
|
|
7959
|
+
errors.push(`source '${entry.source}' is not a column identifier`);
|
|
7960
|
+
continue;
|
|
7961
|
+
}
|
|
7962
|
+
seen.add(column.columnName);
|
|
7963
|
+
const pgType = columnPostgresType(column, schemaName, enums);
|
|
7964
|
+
if (!pgType) {
|
|
7965
|
+
errors.push(`column '${column.name}' references undeclared enum '${typeof column.type === "string" ? column.type : column.type.enum}'`);
|
|
7966
|
+
continue;
|
|
7967
|
+
}
|
|
7968
|
+
const expression = valueExpression({ column, source: entry.source, pgType, nullDefault: entry.nullDefault, schemaName, enums, errors });
|
|
7969
|
+
if (expression) mapped.push({ column, source: entry.source, expression, pgType, fillsNull: entry.nullDefault !== void 0 });
|
|
7970
|
+
}
|
|
7971
|
+
return mapped;
|
|
7972
|
+
}
|
|
7973
|
+
function findColumn(table, target) {
|
|
7974
|
+
const matches = table.columns.filter((column) => column.name === target || column.columnName === target);
|
|
7975
|
+
if (matches.length > 1) return "ambiguous";
|
|
7976
|
+
return matches[0];
|
|
7977
|
+
}
|
|
7978
|
+
function valueExpression({
|
|
7979
|
+
column,
|
|
7980
|
+
source,
|
|
7981
|
+
pgType,
|
|
7982
|
+
nullDefault,
|
|
7983
|
+
schemaName,
|
|
7984
|
+
enums,
|
|
7985
|
+
errors
|
|
7986
|
+
}) {
|
|
7987
|
+
const cast = `CAST(src.${quoteIdentifier(source)} AS ${pgType})`;
|
|
7988
|
+
if (nullDefault === void 0) return cast;
|
|
7989
|
+
const literal2 = literalSql({ column, value: nullDefault, schemaName, enums });
|
|
7990
|
+
if (!literal2.ok) {
|
|
7991
|
+
errors.push(literal2.error);
|
|
7992
|
+
return null;
|
|
7993
|
+
}
|
|
7994
|
+
return `COALESCE(${cast}, ${literal2.sql})`;
|
|
7995
|
+
}
|
|
7996
|
+
function collectUnmapped({
|
|
7997
|
+
table,
|
|
7998
|
+
mapped,
|
|
7999
|
+
errors
|
|
8000
|
+
}) {
|
|
8001
|
+
const mappedNames = new Set(mapped.map((column) => column.column.columnName));
|
|
8002
|
+
const primaryKey2 = new Set(table.primaryKey ?? []);
|
|
8003
|
+
for (const column of table.columns) {
|
|
8004
|
+
if (mappedNames.has(column.columnName) || column.cell !== void 0) continue;
|
|
8005
|
+
const hasDefault = column.default !== void 0 && column.default !== null || column.defaultFn !== void 0;
|
|
8006
|
+
const required = column.nullable === false && !hasDefault && !primaryKey2.has(column.columnName);
|
|
8007
|
+
if (required) {
|
|
8008
|
+
errors.push(`column '${column.name}' is required and has no default \u2014 map it or set a database default before importing`);
|
|
8009
|
+
}
|
|
8010
|
+
}
|
|
8011
|
+
}
|
|
8012
|
+
function omittedColumns(table, mapped) {
|
|
8013
|
+
const mappedNames = new Set(mapped.map((column) => column.column.columnName));
|
|
8014
|
+
return table.columns.filter((column) => column.cell === void 0 && !mappedNames.has(column.columnName)).map((column) => column.columnName);
|
|
8015
|
+
}
|
|
8016
|
+
function resumeArguments({
|
|
8017
|
+
mapped,
|
|
8018
|
+
primaryKey: primaryKey2,
|
|
8019
|
+
after,
|
|
8020
|
+
schemaName,
|
|
8021
|
+
enums,
|
|
8022
|
+
errors
|
|
8023
|
+
}) {
|
|
8024
|
+
if (after === void 0) return primaryKey2.map(() => "NULL");
|
|
8025
|
+
if (primaryKey2.length !== 1) {
|
|
8026
|
+
errors.push("after resumes a single-column primary key; re-run from the start for a composite key (skip and update are idempotent)");
|
|
8027
|
+
return [];
|
|
8028
|
+
}
|
|
8029
|
+
const key = mapped.find((column) => column.column.columnName === primaryKey2[0]);
|
|
8030
|
+
if (!key) return [];
|
|
8031
|
+
const literal2 = resumeLiteralSql({ column: key.column, after, schemaName, enums });
|
|
8032
|
+
if (!literal2.ok) {
|
|
8033
|
+
errors.push(`after: ${literal2.error}`);
|
|
8034
|
+
return [];
|
|
8035
|
+
}
|
|
8036
|
+
return [literal2.sql];
|
|
8037
|
+
}
|
|
5836
8038
|
|
|
5837
|
-
export { ACTION_REQUIRES_DENIED_SQLSTATE, ACTION_VALIDATES_FAILED_SQLSTATE, AUDIT_ACTIONS, CANONICAL_FORMAT, CAPABILITIES_CLAIM_KEY, CAPABILITY_KEY_PATTERN, CLAIMS_CONTRACT, CLAIMS_SETTING, CLAIM_KEY_PATTERN, CMP_OPS, COLUMN_BRAND, COLUMN_DEFAULT_FNS, COLUMN_IDENTIFIER_PATTERN, COLUMN_TYPES2 as COLUMN_TYPES, COMPARE_OPS, CONSUMER_ROLE_SUFFIX, ColumnBuilder, DATE_BUCKETS, DEFINITION_FILE_EXTENSIONS, ENUM_BRAND, HASH_DOMAIN, IDENTIFIER_PATTERN, IDENTIFIER_RE, INPUT_REF_KINDS, IR_VERSION, IrVersionError, LEGACY_IR_VERSION, LINK_BRAND, MAX_DEFINITION_FILE_BYTES, MAX_GENERATED_SDL_BYTES, MAX_IDENTIFIER_LENGTH, MIGRATIONS_DIRNAME, NAMESPACE_CONFIG_BRAND, NAMESPACE_CONFIG_FILE, NAMESPACE_CONFIG_FILENAMES, NAMESPACE_PATTERN, ORM_DATA_TOKEN_HEADER, ORM_MIGRATION_EXPORT_FILE, ORM_MIGRATION_EXPORT_MAX_BYTES, ORM_REGISTRATION_MAX_BYTES, ORM_SCHEMA_PREFIX, ORM_SERVICE_DATA_TOKEN_HEADER, ORM_TO_PIPELINE_TYPE, OrmDefinitionError, PredicateParseError, QUALIFIED_TABLE_PATTERN, RESERVED_COLUMN_NAMES, RESERVED_TABLE_NAMES, ROLES_CLAIM_KEY, SCHEMA_DIR, STATEMENT_KINDS, SYNCED_CONTRACT_HASH_DOMAIN, SYNCED_SOURCE_PATTERN, SYNCED_VIEW_EXECUTION_OWNER_ROLE, TABLE_BRAND, VIEW_BRAND, actionFunctionName, actionFunctionStatements, actionParamArg, actionParamOrder, actionParamSchema, actionPrepareChecks, actionSchema, actionWriteTargets, actionsInterfaceName, activeAuthoringContext, applyMigrationHints, applyMigrationHintsV2, beginAuthoringContext, buildActionParamsSchema, buildDrizzleEntities, buildDrizzleEntitiesV2, buildNamespaceDefinition, buildRebaseJournalEntry, canonicalActionSchema, canonicalColumnSchema, canonicalDefinitionSchema, canonicalEnumSchema, canonicalIndexSchema, canonicalLinkSchema, canonicalPoliciesSchema, canonicalRelations, canonicalTableSchema, canonicalViewSchema, canonicalizeDefinition, capabilityExpression, castParts, checksumOf, claimExpression, classifyDestructive, columnSchema, columnTypeSchema2 as columnTypeSchema, compileActionRequires, compileActionStatements, compileNamespace, compilePredIrToSql, compilePredicate, compiledArtifactSchema, compiledOperationSchema, computeCanonicalHash, computeDefinitionHash, computeSyncedContractHash, consumerRoleName, convertV1DefinitionToCanonical, createOrmMigrationExport, declaredCapabilityKeys, defineEnum, defineNamespace, defineSyncedTable, defineTable, defineView, deriveColumnName, diffCommand, diffStatements, emptyJournal, emptySnapshot, endAuthoringContext, extractNamedParams, extractOrmSyncedTableContract, generateNamespaceTypes, generateSnapshot, generateSnapshotV2, identifierSchema, indexByHash, isColumnBuilder, isEnumRef, isFunctionDefault, isOwnedStatement, isOwnedStatementV2, isQualifiedReference, isSeqLink, isSeqNamespaceConfig, isSeqTable, isSeqView, limitIrSchema, loadCommittedMigrations, loadCommittedTipMigration, loadNamespaceDefinition, loadNamespaceFromDir, migrationName, namespaceConfigSchema, namespaceSchema, normalizeCanonical, normalizeIr, normalizeSnapshot, orderIrSchema, ormMigrationExportSchema, ormSchemaName, overlayWinsUnion, overrideRowMutation, parseCanonicalDefinition, parseDefinitionSources, parseOrmMigrationExport, parsePredicate, parseTableFile, parsedPredicateSchema, pendingKitStatements, pendingKitStatementsV2, planMigration, planMigrationV2, policyExpressionSchema, policyVariantSchema, predIrSchema, predicateOperands, quoteIdentifier, quoteLiteral, readMigrationSql, readMigrationWorkspace, readSnapshot, rebaseViolations, referencedCapabilities, referencedColumns, renderMigrationSql, renderOrmMigrationExport, requiredArtifactIrVersion, revertOverlayMutation, rewriteSyncedSourceRelationsInViewQuery, roleExpression, rootIrSchema, securityStatements, selectionIrSchema, serializeNamespace, sqlTypeName, stableJson, syncedBackingRevokeStatements, syncedContractSurface, syncedTables, syncedViewDropStatements, syncedViewStatements, t, tableFileSchema, tableMetaIrSchema, tableReferenceSchema, toMany, toOne, toPlatformUser, typesInterfaceName, validateViewCatalogStatement, valueIrSchema, variableDefaultIrSchema, variableIrSchema, verifyRebaseIsNoop, walkPredicate, writeMigration };
|
|
8039
|
+
export { ACTION_REQUIRES_DENIED_SQLSTATE, ACTION_VALIDATES_FAILED_SQLSTATE, AUDIT_ACTIONS, CANONICAL_FORMAT, CAPABILITIES_CLAIM_KEY, CAPABILITY_KEY_PATTERN, CELLS_FIELD, CELL_PARTS, CELL_PART_SUFFIX, CLAIMS_CONTRACT, CLAIMS_SETTING, CLAIM_KEY_PATTERN, CMP_OPS, COLUMN_BRAND, COLUMN_DEFAULT_FNS, COLUMN_IDENTIFIER_PATTERN, COLUMN_TYPES2 as COLUMN_TYPES, COMPARE_OPS, COMPOSED_IR_VERSION, CONSUMER_ROLE_SUFFIX, ColumnBuilder, DATE_BUCKETS, DEFINITION_FILE_EXTENSIONS, ENUM_BRAND, GUARDED_IR_VERSION, HASH_DOMAIN, IDENTIFIER_PATTERN, IDENTIFIER_RE, IMPORT_CONFLICT_MODES, INPUT_REF_KINDS, IR_VERSION, IrVersionError, LEGACY_IR_VERSION, LINK_BRAND, MATERIALIZE_ACTOR, MATERIALIZE_CLAIMS_JSON, MAX_DEFINITION_FILE_BYTES, MAX_GENERATED_SDL_BYTES, MAX_IDENTIFIER_LENGTH, MAX_TRACKED_COLUMN_NAME, MIGRATIONS_DIRNAME, NAMESPACE_CONFIG_BRAND, NAMESPACE_CONFIG_FILE, NAMESPACE_CONFIG_FILENAMES, NAMESPACE_PATTERN, ORM_DATA_TOKEN_HEADER, ORM_MIGRATION_EXPORT_FILE, ORM_MIGRATION_EXPORT_MAX_BYTES, ORM_REGISTRATION_MAX_BYTES, ORM_SCHEMA_PREFIX, ORM_SERVICE_DATA_TOKEN_HEADER, ORM_TO_PIPELINE_TYPE, OrmDefinitionError, PredicateParseError, QUALIFIED_TABLE_PATTERN, RESERVED_COLUMN_NAMES, RESERVED_TABLE_NAMES, ROLES_CLAIM_KEY, RUNTIME_SQL_IR_VERSION, SCHEMA_DIR, SQL_IR_VERSION, STATEMENT_KINDS, SYNCED_CONTRACT_HASH_DOMAIN, SYNCED_SOURCE_PATTERN, SYNCED_VIEW_EXECUTION_OWNER_ROLE, TABLE_BRAND, VIEW_BRAND, actionFunctionName, actionFunctionStatements, actionParamArg, actionParamOrder, actionParamSchema, actionPrepareChecks, actionSchema, actionWriteTargets, actionsInterfaceName, activeAuthoringContext, applyMigrationHints, applyMigrationHintsV2, beginAuthoringContext, buildActionParamsSchema, buildDrizzleEntities, buildDrizzleEntitiesV2, buildNamespaceDefinition, buildRebaseJournalEntry, canonicalActionSchema, canonicalColumnSchema, canonicalDefinitionSchema, canonicalEnumSchema, canonicalIndexSchema, canonicalLinkSchema, canonicalPoliciesSchema, canonicalRelations, canonicalTableSchema, canonicalViewSchema, canonicalizeDefinition, capabilityExpression, castParts, cellColumnName, checksumOf, claimExpression, classifyDestructive, columnSchema, columnTypeSchema2 as columnTypeSchema, compileActionRequires, compileActionStatements, compileNamespace, compilePredIrToSql, compilePredicate, compiledArtifactSchema, compiledOperationSchema, computeCanonicalHash, computeDefinitionHash, computeSyncedContractHash, consumerRoleName, convertV1DefinitionToCanonical, createOrmMigrationExport, custom, declaredCapabilityKeys, defineEnum, defineNamespace, defineOperation, defineResolvedTable, defineSyncedTable, defineTable, defineView, deriveColumnName, diffCommand, diffStatements, emptyJournal, emptySnapshot, endAuthoringContext, etlSource, extractNamedParams, extractOrmSyncedTableContract, generateNamespaceTypes, generateSnapshot, generateSnapshotV2, highest, identifierSchema, importSpecSchema, indexByHash, isColumnBuilder, isEnumRef, isFunctionDefault, isOwnedStatement, isOwnedStatementV2, isQualifiedReference, isSeqLink, isSeqNamespaceConfig, isSeqTable, isSeqView, latest, limitIrSchema, loadCommittedMigrations, loadCommittedTipMigration, loadNamespaceDefinition, loadNamespaceFromDir, lowest, managedFn, manualSource, materializeLockKey, materializedRefreshFunctionName, migrationName, namespaceConfigSchema, namespaceSchema, nonNullFirst, normalizeCanonical, normalizeIr, normalizeSnapshot, objectRef, ontologyContractSchema, orderIrSchema, ormMigrationExportSchema, ormSchemaName, overlayWinsUnion, overrideRowMutation, parseCanonicalDefinition, parseDefinitionSources, parseOrmMigrationExport, parsePredicate, parseTableFile, parsedPredicateSchema, pendingKitStatements, pendingKitStatementsV2, planCommittedMigrationV2, planMigration, planMigrationV2, planTableImport, policyExpressionSchema, policyVariantSchema, predIrSchema, predicateOperands, prefetchPlanIrSchema, prefetchSource, priority, quoteIdentifier, quoteLiteral, readMigrationSql, readMigrationWorkspace, readOntologyContract, readSnapshot, rebaseViolations, referencedCapabilities, referencedColumns, renderMigrationSql, renderOrmMigrationExport, requiredArtifactIrVersion, resolvedMetaIrSchema, resolvedSourceIrSchema, revertOverlayMutation, rewriteSyncedSourceRelationsInViewQuery, roleExpression, rootIrSchema, securityStatements, selectionIrSchema, serializeNamespace, sqlTypeName, stableJson, syncedBackingRevokeStatements, syncedContractSurface, syncedTables, syncedViewDropStatements, syncedViewStatements, t, tableFileSchema, tableMetaIrSchema, tableReferenceSchema, toMany, toOne, toPlatformUser, typesInterfaceName, validateNamespaceCatalog, validateViewCatalogStatement, valueIrSchema, variableDefaultIrSchema, variableIrSchema, verifyRebaseIsNoop, walkPredicate, writeMigration };
|
|
5838
8040
|
//# sourceMappingURL=index.js.map
|
|
5839
8041
|
//# sourceMappingURL=index.js.map
|