@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.
Files changed (77) hide show
  1. package/README.md +40 -1
  2. package/dist/authoring.cjs +1619 -121
  3. package/dist/authoring.cjs.map +1 -1
  4. package/dist/authoring.d.cts +252 -8
  5. package/dist/authoring.d.ts +252 -8
  6. package/dist/authoring.js +1607 -123
  7. package/dist/authoring.js.map +1 -1
  8. package/dist/canonical.cjs +713 -41
  9. package/dist/canonical.cjs.map +1 -1
  10. package/dist/canonical.d.cts +1520 -51
  11. package/dist/canonical.d.ts +1520 -51
  12. package/dist/canonical.js +693 -42
  13. package/dist/canonical.js.map +1 -1
  14. package/dist/cli.cjs +7103 -3901
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.js +7104 -3902
  17. package/dist/cli.js.map +1 -1
  18. package/dist/client.cjs +122 -40
  19. package/dist/client.cjs.map +1 -1
  20. package/dist/client.d.cts +85 -25
  21. package/dist/client.d.ts +85 -25
  22. package/dist/client.js +119 -41
  23. package/dist/client.js.map +1 -1
  24. package/dist/compiler.cjs +2464 -450
  25. package/dist/compiler.cjs.map +1 -1
  26. package/dist/compiler.d.cts +79 -4
  27. package/dist/compiler.d.ts +79 -4
  28. package/dist/compiler.js +2457 -451
  29. package/dist/compiler.js.map +1 -1
  30. package/dist/constants-C6EG6Wq7.d.cts +99 -0
  31. package/dist/constants-C6EG6Wq7.d.ts +99 -0
  32. package/dist/definition-DjfC7Lll.d.cts +91 -0
  33. package/dist/definition-DjfC7Lll.d.ts +91 -0
  34. package/dist/dynamic-input.cjs +2 -1
  35. package/dist/dynamic-input.cjs.map +1 -1
  36. package/dist/dynamic-input.d.cts +9 -1
  37. package/dist/dynamic-input.d.ts +9 -1
  38. package/dist/dynamic-input.js +2 -1
  39. package/dist/dynamic-input.js.map +1 -1
  40. package/dist/index.cjs +2656 -418
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/index.d.cts +693 -116
  43. package/dist/index.d.ts +693 -116
  44. package/dist/index.js +2622 -420
  45. package/dist/index.js.map +1 -1
  46. package/dist/ir.cjs +811 -48
  47. package/dist/ir.cjs.map +1 -1
  48. package/dist/ir.d.cts +670 -27
  49. package/dist/ir.d.ts +670 -27
  50. package/dist/ir.js +802 -49
  51. package/dist/ir.js.map +1 -1
  52. package/dist/{naming-DCn94LJ4.d.ts → naming-CPfx7EfF.d.ts} +14 -1
  53. package/dist/{naming-DPglwzkB.d.cts → naming-CU9ThP_E.d.cts} +14 -1
  54. package/dist/policy-1iZbGnZZ.d.cts +24 -0
  55. package/dist/policy-1iZbGnZZ.d.ts +24 -0
  56. package/dist/predicate-v2.cjs +804 -51
  57. package/dist/predicate-v2.cjs.map +1 -1
  58. package/dist/predicate-v2.d.cts +2 -1
  59. package/dist/predicate-v2.d.ts +2 -1
  60. package/dist/predicate-v2.js +804 -51
  61. package/dist/predicate-v2.js.map +1 -1
  62. package/dist/sdl.cjs +1014 -75
  63. package/dist/sdl.cjs.map +1 -1
  64. package/dist/sdl.d.cts +14 -3
  65. package/dist/sdl.d.ts +14 -3
  66. package/dist/sdl.js +1012 -76
  67. package/dist/sdl.js.map +1 -1
  68. package/package.json +13 -7
  69. package/src/codegen-gql/server-fns.cjs +2 -2
  70. package/templates/orm-namespace/AGENTS.md +10 -0
  71. package/templates/orm-namespace/CLAUDE.md +1 -82
  72. package/templates/orm-namespace/codegen.preset.d.cts +7 -0
  73. package/templates/orm-namespace/codegen.ts +2 -1
  74. package/templates/orm-namespace/graphql/example/queries.ts +4 -3
  75. package/templates/orm-namespace/package.json +2 -1
  76. package/templates/orm-namespace/schema/tables.ts +3 -2
  77. package/templates/orm-namespace/sequence.config.ts +7 -11
@@ -2,8 +2,8 @@
2
2
 
3
3
  var drizzleOrm = require('drizzle-orm');
4
4
  var pgCore = require('drizzle-orm/pg-core');
5
- var crypto = require('crypto');
6
5
  var zod = require('zod');
6
+ var crypto = require('crypto');
7
7
  var jiti = require('jiti');
8
8
  var promises = require('fs/promises');
9
9
  var module$1 = require('module');
@@ -37,7 +37,8 @@ var ColumnBuilder = class {
37
37
  description: void 0,
38
38
  renamedFrom: void 0,
39
39
  cast: void 0,
40
- deprecated: false
40
+ deprecated: false,
41
+ cell: void 0
41
42
  };
42
43
  }
43
44
  /** Marks this column as (part of) the primary key. Implies NOT NULL. */
@@ -97,6 +98,11 @@ var ColumnBuilder = class {
97
98
  this.state.deprecated = true;
98
99
  return this;
99
100
  }
101
+ /** @internal System cell-metadata part of logical column `of` (generated resolved views only). */
102
+ cellPart(of, part) {
103
+ this.state.cell = { of, part };
104
+ return this;
105
+ }
100
106
  /** Whether `.primaryKey()` was called — collected into the table-level PK. */
101
107
  get isPrimaryKey() {
102
108
  return this.state.primaryKey;
@@ -125,6 +131,7 @@ var ColumnBuilder = class {
125
131
  if (this.state.description !== void 0) column.description = this.state.description;
126
132
  if (this.state.renamedFrom !== void 0) column.renamedFrom = this.state.renamedFrom;
127
133
  if (this.state.cast !== void 0) column.cast = this.state.cast;
134
+ if (this.state.cell !== void 0) column.cell = { ...this.state.cell };
128
135
  return column;
129
136
  }
130
137
  };
@@ -151,8 +158,24 @@ var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
151
158
  var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
152
159
  var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
153
160
  var MAX_IDENTIFIER_LENGTH = 63;
154
- var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
161
+ var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]{0,62}\.[a-z][a-z0-9_]{0,62}$/;
162
+ var SYNCED_SOURCE_PATTERN = QUALIFIED_TABLE_PATTERN;
155
163
  var HASH_DOMAIN = "sequence-orm-definitions\0";
164
+ var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
165
+ var CELL_PART_SUFFIX = {
166
+ at: "__at",
167
+ by: "__by",
168
+ rev: "__rev",
169
+ source: "__src",
170
+ conflicts: "__lost"
171
+ };
172
+ var MAX_TRACKED_COLUMN_NAME = MAX_IDENTIFIER_LENGTH - 6;
173
+ var CELLS_FIELD = "cells";
174
+ function cellColumnName(column, part) {
175
+ return `${column}${CELL_PART_SUFFIX[part]}`;
176
+ }
177
+ var MATERIALIZE_ACTOR = "orm:materialize";
178
+ JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
156
179
 
157
180
  // src/errors.ts
158
181
  var OrmDefinitionError = class extends Error {
@@ -213,6 +236,9 @@ function registerNamespaceConfig(value) {
213
236
  // src/authoring/table.ts
214
237
  var TABLE_BRAND = "seq-table";
215
238
  var LINK_BRAND = "seq-link";
239
+ function objectRef(namespace, table) {
240
+ return { kind: "seq-external-object", namespace, table };
241
+ }
216
242
  function toOne(target, options) {
217
243
  return {
218
244
  kind: LINK_BRAND,
@@ -221,6 +247,8 @@ function toOne(target, options) {
221
247
  on: options.on,
222
248
  references: options.references,
223
249
  enforce: options.enforce ?? false,
250
+ onDelete: options.onDelete,
251
+ onUpdate: options.onUpdate,
224
252
  description: options.description
225
253
  };
226
254
  }
@@ -232,6 +260,8 @@ function toMany(target, options) {
232
260
  on: options.on,
233
261
  references: options.references,
234
262
  enforce: false,
263
+ onDelete: void 0,
264
+ onUpdate: void 0,
235
265
  description: options.description
236
266
  };
237
267
  }
@@ -243,6 +273,8 @@ function toPlatformUser(options) {
243
273
  on: options.on,
244
274
  references: options.references,
245
275
  enforce: false,
276
+ onDelete: void 0,
277
+ onUpdate: void 0,
246
278
  description: options.description
247
279
  };
248
280
  }
@@ -260,45 +292,51 @@ function assertIdentifier({ value, what, file, pattern = IDENTIFIER_PATTERN }) {
260
292
  );
261
293
  }
262
294
  }
263
- function defineTable(name, columns, config = {}) {
295
+ function defineTable(name2, columns, config = {}) {
264
296
  const file = activeAuthoringContext()?.currentFile;
265
- assertIdentifier({ value: name, what: "table name", file });
297
+ assertIdentifier({ value: name2, what: "table name", file });
266
298
  const entries = Object.entries(columns);
267
299
  if (entries.length === 0) {
268
- throw new OrmDefinitionError(`table "${name}" declares no columns`, file);
300
+ throw new OrmDefinitionError(`table "${name2}" declares no columns`, file);
269
301
  }
270
302
  const seenColumnNames = /* @__PURE__ */ new Map();
271
303
  for (const [propertyName, builder] of entries) {
272
304
  if (!isColumnBuilder(builder)) {
273
305
  throw new OrmDefinitionError(
274
- `table "${name}": column "${propertyName}" is not a column builder \u2014 use the t.* factories (t.uuid(), t.text(), \u2026)`,
306
+ `table "${name2}": column "${propertyName}" is not a column builder \u2014 use the t.* factories (t.uuid(), t.text(), \u2026)`,
275
307
  file
276
308
  );
277
309
  }
278
310
  const columnName = builder.columnNameFor(propertyName);
279
- assertIdentifier({ value: columnName, what: `table "${name}": column "${propertyName}" column name`, file, pattern: COLUMN_IDENTIFIER_PATTERN });
311
+ assertIdentifier({ value: columnName, what: `table "${name2}": column "${propertyName}" column name`, file, pattern: COLUMN_IDENTIFIER_PATTERN });
312
+ if (config.materializes === void 0 && (propertyName === CELLS_FIELD || Object.values(CELL_PART_SUFFIX).some((suffix) => columnName.endsWith(suffix) || propertyName.endsWith(suffix)))) {
313
+ throw new OrmDefinitionError(
314
+ `table "${name2}": "${propertyName}" collides with the reserved cell-metadata names ('${CELLS_FIELD}', or a ${Object.values(CELL_PART_SUFFIX).join("/")} suffix)`,
315
+ file
316
+ );
317
+ }
280
318
  const existing = seenColumnNames.get(columnName);
281
319
  if (existing !== void 0) {
282
320
  throw new OrmDefinitionError(
283
- `table "${name}": columns "${existing}" and "${propertyName}" both map to Postgres column "${columnName}"`,
321
+ `table "${name2}": columns "${existing}" and "${propertyName}" both map to Postgres column "${columnName}"`,
284
322
  file
285
323
  );
286
324
  }
287
325
  seenColumnNames.set(columnName, propertyName);
288
326
  }
289
- const table = { kind: TABLE_BRAND, tableName: name, columns, config, source: void 0, file };
327
+ const table = { kind: TABLE_BRAND, tableName: name2, columns, config, source: void 0, file };
290
328
  registerTable(table);
291
329
  return table;
292
330
  }
293
- function defineSyncedTable(name, columnsOrConfig, maybeConfig) {
331
+ function defineSyncedTable(name2, columnsOrConfig, maybeConfig) {
294
332
  const file = activeAuthoringContext()?.currentFile;
295
- assertIdentifier({ value: name, what: "table name", file });
333
+ assertIdentifier({ value: name2, what: "table name", file });
296
334
  const isTyped = maybeConfig !== void 0;
297
335
  const columns = isTyped ? columnsOrConfig : {};
298
336
  const config = isTyped ? maybeConfig : columnsOrConfig;
299
337
  if (!SYNCED_SOURCE_PATTERN.test(config.source)) {
300
338
  throw new OrmDefinitionError(
301
- `synced table "${name}": source "${config.source}" must match ${SYNCED_SOURCE_PATTERN} (gold layer only)`,
339
+ `synced table "${name2}": source "${config.source}" must be a qualified lowercase schema.table identifier`,
302
340
  file
303
341
  );
304
342
  }
@@ -308,21 +346,21 @@ function defineSyncedTable(name, columnsOrConfig, maybeConfig) {
308
346
  for (const [propertyName, builder] of entries) {
309
347
  if (!isColumnBuilder(builder)) {
310
348
  throw new OrmDefinitionError(
311
- `synced table "${name}": column "${propertyName}" is not a column builder \u2014 use the t.* factories (t.uuid(), t.text(), \u2026)`,
349
+ `synced table "${name2}": column "${propertyName}" is not a column builder \u2014 use the t.* factories (t.uuid(), t.text(), \u2026)`,
312
350
  file
313
351
  );
314
352
  }
315
353
  const columnName = builder.columnNameFor(propertyName);
316
354
  assertIdentifier({
317
355
  value: columnName,
318
- what: `synced table "${name}": column "${propertyName}" column name`,
356
+ what: `synced table "${name2}": column "${propertyName}" column name`,
319
357
  file,
320
358
  pattern: COLUMN_IDENTIFIER_PATTERN
321
359
  });
322
360
  const existing = seenColumnNames.get(columnName);
323
361
  if (existing !== void 0) {
324
362
  throw new OrmDefinitionError(
325
- `synced table "${name}": columns "${existing}" and "${propertyName}" both map to Postgres column "${columnName}"`,
363
+ `synced table "${name2}": columns "${existing}" and "${propertyName}" both map to Postgres column "${columnName}"`,
326
364
  file
327
365
  );
328
366
  }
@@ -333,13 +371,13 @@ function defineSyncedTable(name, columnsOrConfig, maybeConfig) {
333
371
  const hasConfigPk = config.primaryKey !== void 0 && config.primaryKey.length > 0;
334
372
  if (columnLevelPk.length === 0 && !hasConfigPk) {
335
373
  throw new OrmDefinitionError(
336
- `synced table "${name}" declares columns but no primary key \u2014 use .primaryKey() or config.primaryKey`,
374
+ `synced table "${name2}" declares columns but no primary key \u2014 use .primaryKey() or config.primaryKey`,
337
375
  file
338
376
  );
339
377
  }
340
378
  if (columnLevelPk.length > 0 && hasConfigPk) {
341
379
  throw new OrmDefinitionError(
342
- `synced table "${name}": declare the primary key either with .primaryKey() on columns or with config.primaryKey \u2014 not both`,
380
+ `synced table "${name2}": declare the primary key either with .primaryKey() on columns or with config.primaryKey \u2014 not both`,
343
381
  file
344
382
  );
345
383
  }
@@ -351,7 +389,7 @@ function defineSyncedTable(name, columnsOrConfig, maybeConfig) {
351
389
  if (config.policies !== void 0) tableConfig.policies = { read: config.policies.read };
352
390
  const table = {
353
391
  kind: TABLE_BRAND,
354
- tableName: name,
392
+ tableName: name2,
355
393
  columns,
356
394
  config: tableConfig,
357
395
  source: config.source,
@@ -360,26 +398,26 @@ function defineSyncedTable(name, columnsOrConfig, maybeConfig) {
360
398
  registerTable(table);
361
399
  return table;
362
400
  }
363
- function defineEnum(name, values) {
401
+ function defineEnum(name2, values) {
364
402
  const file = activeAuthoringContext()?.currentFile;
365
- assertIdentifier({ value: name, what: "enum name", file });
403
+ assertIdentifier({ value: name2, what: "enum name", file });
366
404
  if (values.length === 0 || values.length > 100) {
367
- throw new OrmDefinitionError(`enum "${name}" must declare between 1 and 100 values`, file);
405
+ throw new OrmDefinitionError(`enum "${name2}" must declare between 1 and 100 values`, file);
368
406
  }
369
407
  const seen = /* @__PURE__ */ new Set();
370
408
  for (const value of values) {
371
409
  if (value.length === 0 || value.length > MAX_IDENTIFIER_LENGTH) {
372
410
  throw new OrmDefinitionError(
373
- `enum "${name}": value ${JSON.stringify(value)} must be 1-${MAX_IDENTIFIER_LENGTH} characters`,
411
+ `enum "${name2}": value ${JSON.stringify(value)} must be 1-${MAX_IDENTIFIER_LENGTH} characters`,
374
412
  file
375
413
  );
376
414
  }
377
415
  if (seen.has(value)) {
378
- throw new OrmDefinitionError(`enum "${name}" declares duplicate value "${value}"`, file);
416
+ throw new OrmDefinitionError(`enum "${name2}" declares duplicate value "${value}"`, file);
379
417
  }
380
418
  seen.add(value);
381
419
  }
382
- const ref = { kind: ENUM_BRAND, name, values: [...values] };
420
+ const ref = { kind: ENUM_BRAND, name: name2, values: [...values] };
383
421
  registerEnum(ref);
384
422
  return ref;
385
423
  }
@@ -387,52 +425,52 @@ var VIEW_BRAND = "seq-view";
387
425
  function isSeqView(value) {
388
426
  return typeof value === "object" && value !== null && value.kind === VIEW_BRAND;
389
427
  }
390
- function defineView(name, columns, config) {
428
+ function defineView(name2, columns, config) {
391
429
  const file = activeAuthoringContext()?.currentFile;
392
- if (!IDENTIFIER_PATTERN.test(name) || name.length > MAX_IDENTIFIER_LENGTH) {
430
+ if (!IDENTIFIER_PATTERN.test(name2) || name2.length > MAX_IDENTIFIER_LENGTH) {
393
431
  throw new OrmDefinitionError(
394
- `view name "${name}" must match ${IDENTIFIER_PATTERN} and be at most ${MAX_IDENTIFIER_LENGTH} characters`,
432
+ `view name "${name2}" must match ${IDENTIFIER_PATTERN} and be at most ${MAX_IDENTIFIER_LENGTH} characters`,
395
433
  file
396
434
  );
397
435
  }
398
436
  const entries = Object.entries(columns);
399
- if (entries.length === 0) throw new OrmDefinitionError(`view "${name}" declares no columns`, file);
437
+ if (entries.length === 0) throw new OrmDefinitionError(`view "${name2}" declares no columns`, file);
400
438
  const seenColumnNames = /* @__PURE__ */ new Map();
401
439
  for (const [propertyName, builder] of entries) {
402
440
  if (!isColumnBuilder(builder)) {
403
441
  throw new OrmDefinitionError(
404
- `view "${name}": column "${propertyName}" is not a column builder \u2014 use the t.* factories`,
442
+ `view "${name2}": column "${propertyName}" is not a column builder \u2014 use the t.* factories`,
405
443
  file
406
444
  );
407
445
  }
408
446
  const columnName = builder.columnNameFor(propertyName);
409
447
  if (!COLUMN_IDENTIFIER_PATTERN.test(columnName) || columnName.length > MAX_IDENTIFIER_LENGTH) {
410
- throw new OrmDefinitionError(`view "${name}": column "${propertyName}" has invalid column name "${columnName}"`, file);
448
+ throw new OrmDefinitionError(`view "${name2}": column "${propertyName}" has invalid column name "${columnName}"`, file);
411
449
  }
412
450
  const existing = seenColumnNames.get(columnName);
413
451
  if (existing !== void 0) {
414
452
  throw new OrmDefinitionError(
415
- `view "${name}": columns "${existing}" and "${propertyName}" both map to Postgres column "${columnName}"`,
453
+ `view "${name2}": columns "${existing}" and "${propertyName}" both map to Postgres column "${columnName}"`,
416
454
  file
417
455
  );
418
456
  }
419
457
  seenColumnNames.set(columnName, propertyName);
420
458
  }
421
- const view = { kind: VIEW_BRAND, viewName: name, columns, config, file };
459
+ const view = { kind: VIEW_BRAND, viewName: name2, columns, config, file };
422
460
  registerView(view);
423
461
  return view;
424
462
  }
425
463
  var QUERY_COLUMN_FACTORIES = {
426
- uuid: (name) => pgCore.uuid(name),
427
- text: (name) => pgCore.text(name),
428
- integer: (name) => pgCore.integer(name),
429
- bigint: (name) => pgCore.bigint(name, { mode: "bigint" }),
430
- numeric: (name) => pgCore.numeric(name),
431
- double: (name) => pgCore.doublePrecision(name),
432
- boolean: (name) => pgCore.boolean(name),
433
- timestamptz: (name) => pgCore.timestamp(name, { withTimezone: true }),
434
- date: (name) => pgCore.date(name),
435
- jsonb: (name) => pgCore.jsonb(name)
464
+ uuid: (name2) => pgCore.uuid(name2),
465
+ text: (name2) => pgCore.text(name2),
466
+ integer: (name2) => pgCore.integer(name2),
467
+ bigint: (name2) => pgCore.bigint(name2, { mode: "bigint" }),
468
+ numeric: (name2) => pgCore.numeric(name2),
469
+ double: (name2) => pgCore.doublePrecision(name2),
470
+ boolean: (name2) => pgCore.boolean(name2),
471
+ timestamptz: (name2) => pgCore.timestamp(name2, { withTimezone: true }),
472
+ date: (name2) => pgCore.date(name2),
473
+ jsonb: (name2) => pgCore.jsonb(name2)
436
474
  };
437
475
  function compileViewQueries(input) {
438
476
  const schemaName = `orm_${input.namespace}`;
@@ -488,9 +526,9 @@ function compileViewQueries(input) {
488
526
  }
489
527
  const handle = handles.get(resolved);
490
528
  if (!handle) {
491
- const name = isSeqTable(resolved) ? resolved.tableName : resolved.viewName;
529
+ const name2 = isSeqTable(resolved) ? resolved.tableName : resolved.viewName;
492
530
  throw new OrmDefinitionError(
493
- `view "${authored.viewName}" references relation "${name}" which is not part of this namespace`,
531
+ `view "${authored.viewName}" references relation "${name2}" which is not part of this namespace`,
494
532
  authored.file
495
533
  );
496
534
  }
@@ -524,11 +562,11 @@ function compileViewQueries(input) {
524
562
  );
525
563
  }
526
564
  if (!config.query) throw new OrmDefinitionError(`view "${authored.viewName}" produced no query`, authored.file);
527
- const tableDependencies = pgCore.extractUsedTable(config.query).map((physical) => {
528
- const logical = physicalToLogical.get(physical);
565
+ const tableDependencies = pgCore.extractUsedTable(config.query).map((physical2) => {
566
+ const logical = physicalToLogical.get(physical2);
529
567
  if (!logical) {
530
568
  throw new OrmDefinitionError(
531
- `view "${authored.viewName}" query references unmanaged relation "${physical}" \u2014 use relation(...) with a namespace table or view`,
569
+ `view "${authored.viewName}" query references unmanaged relation "${physical2}" \u2014 use relation(...) with a namespace table or view`,
532
570
  authored.file
533
571
  );
534
572
  }
@@ -681,8 +719,8 @@ function overlayWinsUnion(input) {
681
719
  // src/authoring/cow-mutations.ts
682
720
  function overrideRowMutation(input) {
683
721
  const { overlay, synced, pk } = input;
684
- const name = input.name ?? `override_${overlay.tableName}`;
685
- assertMutationName(name, "overrideRowMutation");
722
+ const name2 = input.name ?? `override_${overlay.tableName}`;
723
+ assertMutationName(name2, "overrideRowMutation");
686
724
  assertOverlayPairPrimaryKey({ overlay, synced, pk, helper: "overrideRowMutation" });
687
725
  const sharedProperties = sharedNonPkProperties({ overlay, synced, pk });
688
726
  const setProperties = resolveSetProperties({
@@ -701,7 +739,7 @@ function overrideRowMutation(input) {
701
739
  });
702
740
  return {
703
741
  kind: "override",
704
- name,
742
+ name: name2,
705
743
  synced: synced.tableName,
706
744
  pk,
707
745
  copy: [pk, ...sharedProperties],
@@ -710,23 +748,23 @@ function overrideRowMutation(input) {
710
748
  }
711
749
  function revertOverlayMutation(input) {
712
750
  const { overlay, synced, pk } = input;
713
- const name = input.name ?? `revert_${overlay.tableName}`;
714
- assertMutationName(name, "revertOverlayMutation");
751
+ const name2 = input.name ?? `revert_${overlay.tableName}`;
752
+ assertMutationName(name2, "revertOverlayMutation");
715
753
  assertOverlayPairPrimaryKey({ overlay, synced, pk, helper: "revertOverlayMutation" });
716
754
  assertMatchingColumns({ overlay, synced, properties: [pk], helper: "revertOverlayMutation" });
717
755
  return {
718
756
  kind: "revert",
719
- name,
757
+ name: name2,
720
758
  synced: synced.tableName,
721
759
  pk,
722
760
  copy: [],
723
761
  set: []
724
762
  };
725
763
  }
726
- function assertMutationName(name, helper) {
727
- if (!IDENTIFIER_PATTERN.test(name) || name.length > MAX_IDENTIFIER_LENGTH) {
764
+ function assertMutationName(name2, helper) {
765
+ if (!IDENTIFIER_PATTERN.test(name2) || name2.length > MAX_IDENTIFIER_LENGTH) {
728
766
  throw new OrmDefinitionError(
729
- `${helper}: mutation name "${name}" must match ${IDENTIFIER_PATTERN} and be at most ${MAX_IDENTIFIER_LENGTH} characters`
767
+ `${helper}: mutation name "${name2}" must match ${IDENTIFIER_PATTERN} and be at most ${MAX_IDENTIFIER_LENGTH} characters`
730
768
  );
731
769
  }
732
770
  }
@@ -852,9 +890,468 @@ function scalarColumnType(table, property, helper) {
852
890
  }
853
891
  return type;
854
892
  }
893
+ var quoteIdentifier = (name2) => `"${name2.replaceAll('"', '""')}"`;
894
+ var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
895
+
896
+ // src/checks/compile.ts
897
+ function tokenize(source) {
898
+ const tokens = [];
899
+ for (let position = 0; position < source.length; ) {
900
+ const char = source[position];
901
+ if (/\s/.test(char)) {
902
+ position++;
903
+ continue;
904
+ }
905
+ if (char === "(" || char === ")" || char === ",") {
906
+ tokens.push({ kind: char === "(" ? "lparen" : char === ")" ? "rparen" : "comma", value: char, position });
907
+ position++;
908
+ continue;
909
+ }
910
+ if (char === "'") {
911
+ const start = position++;
912
+ let value = "";
913
+ let closed = false;
914
+ while (position < source.length) {
915
+ if (source[position] !== "'") {
916
+ value += source[position++];
917
+ continue;
918
+ }
919
+ if (source[position + 1] === "'") {
920
+ value += "'";
921
+ position += 2;
922
+ continue;
923
+ }
924
+ position++;
925
+ closed = true;
926
+ break;
927
+ }
928
+ if (!closed) throw new Error(`unterminated string at position ${start}`);
929
+ if (/[\x00-\x1f\x7f]/.test(value)) throw new Error(`control character in string at position ${start}`);
930
+ tokens.push({ kind: "string", value, position: start });
931
+ continue;
932
+ }
933
+ const operator = /^(<=|>=|<>|!=|!~|=|<|>|~)/.exec(source.slice(position));
934
+ if (operator) {
935
+ tokens.push({ kind: "operator", value: operator[0] === "<>" ? "!=" : operator[0], position });
936
+ position += operator[0].length;
937
+ continue;
938
+ }
939
+ const number = /^-?\d+(?:\.\d+)?/.exec(source.slice(position));
940
+ if (number) {
941
+ tokens.push({ kind: "number", value: number[0], position });
942
+ position += number[0].length;
943
+ continue;
944
+ }
945
+ const identifier3 = /^[a-zA-Z_][a-zA-Z_0-9]*/.exec(source.slice(position));
946
+ if (identifier3) {
947
+ tokens.push({ kind: "identifier", value: identifier3[0], position });
948
+ position += identifier3[0].length;
949
+ continue;
950
+ }
951
+ throw new Error(`unexpected character '${char}' at position ${position}`);
952
+ }
953
+ tokens.push({ kind: "end", value: "", position: source.length });
954
+ if (tokens.length > 256) throw new Error("check expression exceeds 256 tokens");
955
+ return tokens;
956
+ }
957
+ var Parser = class {
958
+ constructor(tokens, columns) {
959
+ this.tokens = tokens;
960
+ this.columns = /* @__PURE__ */ new Map();
961
+ for (const column of columns) {
962
+ for (const alias of [column.name, column.columnName]) {
963
+ const previous = this.columns.get(alias);
964
+ if (previous && previous.columnName !== column.columnName) {
965
+ throw new Error(`ambiguous check column '${alias}'`);
966
+ }
967
+ this.columns.set(alias, column);
968
+ }
969
+ }
970
+ }
971
+ index = 0;
972
+ depth = 0;
973
+ columns;
974
+ parse() {
975
+ const expression = this.parseOr();
976
+ this.expect("end", "end of expression");
977
+ return expression;
978
+ }
979
+ parseOr() {
980
+ let left = this.parseAnd();
981
+ while (this.matchKeyword("or")) left = `(${left} OR ${this.parseAnd()})`;
982
+ return left;
983
+ }
984
+ parseAnd() {
985
+ let left = this.parseNot();
986
+ while (this.matchKeyword("and")) left = `(${left} AND ${this.parseNot()})`;
987
+ return left;
988
+ }
989
+ parseNot() {
990
+ if (this.matchKeyword("not")) return `(NOT ${this.parseNot()})`;
991
+ return this.parsePrimary();
992
+ }
993
+ parsePrimary() {
994
+ if (this.peek().kind === "lparen") {
995
+ if (++this.depth > 32) throw new Error("check expression nesting exceeds 32 levels");
996
+ this.advance();
997
+ const expression = this.parseOr();
998
+ this.expect("rparen", "')'");
999
+ this.depth--;
1000
+ return `(${expression})`;
1001
+ }
1002
+ return this.parseComparison();
1003
+ }
1004
+ parseComparison() {
1005
+ const left = this.parseValue();
1006
+ if (this.matchKeyword("is")) {
1007
+ const negate = this.matchKeyword("not");
1008
+ this.expectKeyword("null");
1009
+ return `${left.sql} IS ${negate ? "NOT " : ""}NULL`;
1010
+ }
1011
+ if (this.matchKeyword("between")) {
1012
+ const lower = this.parseValue();
1013
+ this.expectKeyword("and");
1014
+ const upper = this.parseValue();
1015
+ return `${left.sql} BETWEEN ${lower.sql} AND ${upper.sql}`;
1016
+ }
1017
+ if (this.matchKeyword("in")) {
1018
+ this.expect("lparen", "'(' after IN");
1019
+ const values = [this.parseValue()];
1020
+ while (this.peek().kind === "comma") {
1021
+ this.advance();
1022
+ values.push(this.parseValue());
1023
+ }
1024
+ this.expect("rparen", "')' after IN values");
1025
+ if (values.some((value) => !value.literal)) throw new Error("IN values must be literals");
1026
+ return `${left.sql} IN (${values.map((value) => value.sql).join(", ")})`;
1027
+ }
1028
+ const operator = this.expect("operator", "a comparison operator");
1029
+ const right = this.parseValue();
1030
+ if (operator.value === "~" || operator.value === "!~") {
1031
+ if (left.column && left.column.type !== "text") throw new Error("regex checks require a text column");
1032
+ if (!right.literal) throw new Error("regex pattern must be a literal");
1033
+ }
1034
+ return `${left.sql} ${operator.value} ${right.sql}`;
1035
+ }
1036
+ parseValue() {
1037
+ const token = this.advance();
1038
+ if (token.kind === "string") return { sql: quoteLiteral(token.value), literal: true };
1039
+ if (token.kind === "number") {
1040
+ const number = Number(token.value);
1041
+ if (!Number.isFinite(number) || Number.isInteger(number) && !Number.isSafeInteger(number)) {
1042
+ throw new Error(`unsafe numeric literal at position ${token.position}`);
1043
+ }
1044
+ return { sql: token.value, literal: true };
1045
+ }
1046
+ if (token.kind !== "identifier") throw new Error(`expected a column or literal at position ${token.position}`);
1047
+ const keyword = token.value.toLowerCase();
1048
+ if (keyword === "true" || keyword === "false" || keyword === "null") return { sql: keyword.toUpperCase(), literal: true };
1049
+ if (keyword === "btrim") {
1050
+ this.expect("lparen", "'(' after btrim");
1051
+ const column2 = this.parseColumn();
1052
+ this.expect("rparen", "')' after btrim column");
1053
+ if (column2.type !== "text") throw new Error("btrim requires a text column");
1054
+ return { sql: `btrim(${quoteIdentifier(column2.columnName)})`, column: column2 };
1055
+ }
1056
+ const column = this.columns.get(token.value);
1057
+ if (!column) throw new Error(`unknown check column '${token.value}'`);
1058
+ return { sql: quoteIdentifier(column.columnName), column };
1059
+ }
1060
+ parseColumn() {
1061
+ const token = this.expect("identifier", "a column name");
1062
+ const column = this.columns.get(token.value);
1063
+ if (!column) throw new Error(`unknown check column '${token.value}'`);
1064
+ return column;
1065
+ }
1066
+ peek() {
1067
+ return this.tokens[this.index];
1068
+ }
1069
+ advance() {
1070
+ const token = this.peek();
1071
+ if (token.kind !== "end") this.index++;
1072
+ return token;
1073
+ }
1074
+ matchKeyword(keyword) {
1075
+ if (this.peek().kind !== "identifier" || this.peek().value.toLowerCase() !== keyword) return false;
1076
+ this.advance();
1077
+ return true;
1078
+ }
1079
+ expectKeyword(keyword) {
1080
+ if (!this.matchKeyword(keyword)) throw new Error(`expected '${keyword}' at position ${this.peek().position}`);
1081
+ }
1082
+ expect(kind, description) {
1083
+ const token = this.peek();
1084
+ if (token.kind !== kind) throw new Error(`expected ${description} at position ${token.position}`);
1085
+ return this.advance();
1086
+ }
1087
+ };
1088
+ function compileCheckExpression(source, columns) {
1089
+ return new Parser(tokenize(source), columns).parse();
1090
+ }
1091
+ var policyVariantSchema = zod.z.object({
1092
+ /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
1093
+ when: zod.z.string().min(1).max(1e3),
1094
+ /** Exactly one of: allow-all bypass, or a row predicate. */
1095
+ allow: zod.z.literal(true).optional(),
1096
+ policy: zod.z.string().min(1).max(2e3).optional()
1097
+ }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
1098
+ message: "A variant declares exactly one of allow: true or policy"
1099
+ });
1100
+ var policyExpressionSchema = zod.z.union([
1101
+ zod.z.string().min(1).max(2e3),
1102
+ zod.z.object({
1103
+ default: zod.z.string().min(1).max(2e3),
1104
+ variants: zod.z.array(policyVariantSchema).max(10).default([])
1105
+ })
1106
+ ]);
1107
+ function parseDurationMs(input) {
1108
+ if (typeof input === "number") return Number.isInteger(input) && input > 0 ? input : void 0;
1109
+ const match = /^(\d+(?:\.\d+)?)(ms|s|m)$/.exec(input);
1110
+ if (!match) return void 0;
1111
+ const factor = match[2] === "ms" ? 1 : match[2] === "s" ? 1e3 : 6e4;
1112
+ const value = Math.round(Number(match[1]) * factor);
1113
+ return value > 0 ? value : void 0;
1114
+ }
1115
+ var identifier = zod.z.string().min(1).max(63).regex(/^[a-z][a-z0-9_]*$/);
1116
+ var durationMs = zod.z.number().int().positive().max(24 * 60 * 60 * 1e3);
1117
+ var RESOLVED_VIEW_SYSTEM_COLUMNS = {
1118
+ writebackStatus: { name: "writebackStatus", columnName: "_writeback_status", type: "text" },
1119
+ writebackError: { name: "writebackError", columnName: "_writeback_error", type: "text" }
1120
+ };
1121
+ var RESOLVED_SOURCE_SYSTEM_COLUMNS = {
1122
+ tombstoned: { name: "sysTombstoned", columnName: "_tombstoned" },
1123
+ fetchedAt: { name: "sysFetchedAt", columnName: "_fetched_at" },
1124
+ writebackStatus: { name: "sysWritebackStatus", columnName: "_writeback_status" },
1125
+ writebackError: { name: "sysWritebackError", columnName: "_writeback_error" },
1126
+ writebackAt: { name: "sysWritebackAt", columnName: "_writeback_at" }
1127
+ };
1128
+ var RESOLVED_VIEW_CELL_PARTS = ["source", "at", "by", "conflicts"];
1129
+ var RESERVED_RESOLVED_COLUMN_NAMES = /* @__PURE__ */ new Set([
1130
+ ...Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName]),
1131
+ ...Object.values(RESOLVED_SOURCE_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName])
1132
+ ]);
1133
+ var COMPARATORS = ["latest", "highest", "lowest", "priority", "nonNullFirst", "custom"];
1134
+ var canonicalFunctionRefSchema = zod.z.object({
1135
+ /** `managed` = a Managed Function id/slug; `native` = an in-process Atlas function (reserved). */
1136
+ kind: zod.z.enum(["managed", "native"]),
1137
+ id: zod.z.string().min(1).max(200)
1138
+ }).strict();
1139
+ var canonicalFetchShapeSchema = zod.z.union([
1140
+ zod.z.object({ shape: zod.z.literal("single") }).strict(),
1141
+ zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().min(2).max(500) }).strict()
1142
+ ]);
1143
+ var canonicalResolvedSourceSchema = zod.z.object({
1144
+ name: identifier,
1145
+ kind: zod.z.enum(["manual", "etl", "prefetch"]),
1146
+ /** Backing relation in this namespace: an internal table (manual/prefetch) or a synced table (etl). */
1147
+ table: identifier,
1148
+ /** Resolved columns (logical names) this source supplies — the primary key is implicit. */
1149
+ columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
1150
+ /** etl: logical timestamptz/date column on the synced table used as every cell's updated_at. */
1151
+ updatedAt: zod.z.string().min(1).max(63).optional(),
1152
+ /** prefetch: resolved column holding the source system's key (may be the primary key). */
1153
+ key: zod.z.string().min(1).max(63).optional(),
1154
+ fetch: canonicalFetchShapeSchema.optional(),
1155
+ function: canonicalFunctionRefSchema.optional(),
1156
+ prefetchDefaults: zod.z.object({
1157
+ maxStalenessMs: durationMs,
1158
+ timeoutMs: durationMs,
1159
+ onError: zod.z.enum(["fallback", "fail"])
1160
+ }).strict().optional(),
1161
+ writeback: zod.z.object({
1162
+ columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
1163
+ defaultMode: zod.z.enum(["sync", "async"]),
1164
+ timeoutMs: durationMs,
1165
+ /** Remove confirmed cells from the manual source so a stale edit cannot shadow the source. */
1166
+ clearManualOnConfirm: zod.z.boolean()
1167
+ }).strict().optional()
1168
+ }).strict();
1169
+ var canonicalResolveRuleSchema = zod.z.object({
1170
+ column: zod.z.string().min(1).max(63),
1171
+ comparator: zod.z.enum(COMPARATORS),
1172
+ /** Source names, in tie-break order. */
1173
+ candidates: zod.z.array(identifier).min(1).max(16),
1174
+ /** custom only: SQL ORDER BY key over value / updated_at / source / actor / ord. */
1175
+ orderBy: zod.z.string().min(1).max(2e3).optional()
1176
+ }).strict();
1177
+ var canonicalResolvedSchema = zod.z.object({
1178
+ sources: zod.z.array(canonicalResolvedSourceSchema).min(1).max(16),
1179
+ /** One rule per resolved (non-pk) column; documents the generated view for tooling. */
1180
+ resolve: zod.z.array(canonicalResolveRuleSchema).max(500),
1181
+ /** Sources whose tombstone hides the resolved row (v1: the manual source only). */
1182
+ deletedBy: zod.z.array(identifier).max(1).default([]),
1183
+ /**
1184
+ * Materialized: the view reads an indexed internal table (`table`) that the
1185
+ * data plane keeps in step with `compute`, the internal view holding the
1186
+ * resolution query. Omitted when the view computes on read.
1187
+ */
1188
+ materialized: zod.z.object({ table: identifier, compute: identifier }).strict().optional(),
1189
+ /** Write policies evaluated against the RESOLVED row (reads use the view's read policy). */
1190
+ policies: zod.z.object({
1191
+ write: policyExpressionSchema.optional(),
1192
+ insert: policyExpressionSchema.optional(),
1193
+ update: policyExpressionSchema.optional()
1194
+ }).strict().default({})
1195
+ }).strict();
1196
+ function validateResolvedView({ view, tables, views = [] }) {
1197
+ const resolved = view.resolved;
1198
+ if (!resolved) return [];
1199
+ const errors = [];
1200
+ const fail = (message) => errors.push(`resolved table '${view.view}': ${message}`);
1201
+ const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
1202
+ if (!pkColumn) {
1203
+ fail("requires exactly one primary-key column");
1204
+ return errors;
1205
+ }
1206
+ const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
1207
+ const scope = {
1208
+ view,
1209
+ resolved,
1210
+ pkColumn,
1211
+ dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
1212
+ sourceByName: /* @__PURE__ */ new Map(),
1213
+ fail
1214
+ };
1215
+ if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail("requires exactly one manual source");
1216
+ const tableByName = new Map(tables.map((table) => [table.table, table]));
1217
+ for (const source of resolved.sources) {
1218
+ if (scope.sourceByName.has(source.name)) fail(`duplicate source '${source.name}'`);
1219
+ scope.sourceByName.set(source.name, source);
1220
+ const table = tableByName.get(source.table);
1221
+ if (!table) fail(`source '${source.name}' references unknown table '${source.table}'`);
1222
+ else validateSource({ scope, source, table });
1223
+ }
1224
+ validateRules(scope);
1225
+ validateViewCells(scope);
1226
+ if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
1227
+ for (const name2 of resolved.deletedBy) {
1228
+ if (scope.sourceByName.get(name2)?.kind !== "manual") fail(`deletedBy '${name2}' must be the manual source`);
1229
+ }
1230
+ return errors;
1231
+ }
1232
+ function validateSource({ scope, source, table }) {
1233
+ const { fail } = scope;
1234
+ if (source.kind === "etl" && table.source === void 0) fail(`etl source '${source.name}' must reference a synced table`);
1235
+ if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
1236
+ fail(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
1237
+ }
1238
+ const hasRev = table.columns.some((column) => column.cell?.part === "rev");
1239
+ if (source.kind === "manual" && !hasRev) fail(`manual source '${source.name}' must carry '__rev' cell columns`);
1240
+ if (source.kind === "prefetch" && hasRev) fail(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
1241
+ const byName = new Map(table.columns.map((c) => [c.name, c]));
1242
+ validateSourceColumns({ scope, source, byName });
1243
+ if (source.kind === "etl") {
1244
+ const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
1245
+ if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
1246
+ fail(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
1247
+ }
1248
+ }
1249
+ validatePrefetchFields({ scope, source });
1250
+ }
1251
+ function validatePrefetchFields({ scope, source }) {
1252
+ const { fail, pkColumn } = scope;
1253
+ if (source.kind !== "prefetch") {
1254
+ if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
1255
+ fail(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
1256
+ }
1257
+ return;
1258
+ }
1259
+ if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
1260
+ fail(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
1261
+ } else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
1262
+ fail(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
1263
+ }
1264
+ for (const column of source.writeback?.columns ?? []) {
1265
+ if (!source.columns.includes(column)) fail(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
1266
+ }
1267
+ }
1268
+ function validateSourceColumns({ scope, source, byName }) {
1269
+ const { fail, pkColumn, dataColumns } = scope;
1270
+ for (const column of [pkColumn.name, ...source.columns]) {
1271
+ const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
1272
+ const theirs = byName.get(column);
1273
+ if (!own) fail(`source '${source.name}' supplies unknown column '${column}'`);
1274
+ else if (!theirs) fail(`source '${source.name}' table '${source.table}' has no column '${column}'`);
1275
+ else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail(`column '${column}' has a different type on source '${source.name}'`);
1276
+ }
1277
+ }
1278
+ function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail }) {
1279
+ const ruled = /* @__PURE__ */ new Set();
1280
+ for (const rule of resolved.resolve) {
1281
+ if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
1282
+ fail(`resolve rule targets unknown column '${rule.column}'`);
1283
+ continue;
1284
+ }
1285
+ if (ruled.has(rule.column)) fail(`column '${rule.column}' has more than one resolve rule`);
1286
+ ruled.add(rule.column);
1287
+ if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
1288
+ fail(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
1289
+ }
1290
+ if (new Set(rule.candidates).size !== rule.candidates.length) fail(`column '${rule.column}' repeats a candidate`);
1291
+ for (const candidate of rule.candidates) {
1292
+ const source = sourceByName.get(candidate);
1293
+ if (!source) fail(`column '${rule.column}' names unknown candidate source '${candidate}'`);
1294
+ else if (!source.columns.includes(rule.column)) fail(`column '${rule.column}' candidate '${candidate}' does not supply it`);
1295
+ }
1296
+ }
1297
+ for (const column of dataColumns.keys()) {
1298
+ if (column !== pkColumn.name && !ruled.has(column)) fail(`column '${column}' has no resolve rule`);
1299
+ }
1300
+ }
1301
+ var VIEW_CELL_TYPES = {
1302
+ source: "text",
1303
+ at: "timestamptz",
1304
+ by: "text",
1305
+ conflicts: "jsonb"
1306
+ };
1307
+ function validateViewCells({ view, dataColumns, pkColumn, fail }) {
1308
+ const expected = /* @__PURE__ */ new Map();
1309
+ for (const column of dataColumns.values()) {
1310
+ if (column.name === pkColumn.name) continue;
1311
+ for (const part of RESOLVED_VIEW_CELL_PARTS) {
1312
+ expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
1313
+ }
1314
+ }
1315
+ for (const column of view.columns) {
1316
+ if (!column.cell) continue;
1317
+ const want = expected.get(column.name);
1318
+ if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
1319
+ fail(`unexpected cell column '${column.name}'`);
1320
+ continue;
1321
+ }
1322
+ if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
1323
+ expected.delete(column.name);
1324
+ }
1325
+ for (const name2 of expected.keys()) fail(`missing cell column '${name2}'`);
1326
+ }
1327
+ var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
1328
+ function validateMaterialized({ scope, tableByName, views }) {
1329
+ const { view, resolved, fail } = scope;
1330
+ const { table: tableName, compute: computeName } = resolved.materialized;
1331
+ const table = tableByName.get(tableName);
1332
+ if (!table || table.internal !== true || table.materializes !== view.view) {
1333
+ fail(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
1334
+ } else {
1335
+ if (columnSignature(table.columns) !== columnSignature(view.columns)) fail(`materialized table '${tableName}' must have exactly the view's columns`);
1336
+ if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail(`materialized table '${tableName}' must share the view's primary key`);
1337
+ }
1338
+ const compute = views.find((candidate) => candidate.view === computeName);
1339
+ if (!compute || compute.internal !== true) fail(`compute view '${computeName}' must be an internal view`);
1340
+ else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail(`compute view '${computeName}' must have exactly the view's columns`);
1341
+ for (const source of resolved.sources) {
1342
+ if (source.kind !== "etl") continue;
1343
+ const read = tableByName.get(source.table)?.policies?.read;
1344
+ const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
1345
+ if (!unconditional) {
1346
+ fail(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
1347
+ }
1348
+ }
1349
+ }
1350
+
1351
+ // src/canonical/v2.ts
855
1352
  var CANONICAL_FORMAT = 2;
856
1353
  var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
857
- var identifier = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
1354
+ var identifier2 = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
858
1355
  var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
859
1356
  var COLUMN_TYPES = [
860
1357
  "uuid",
@@ -870,14 +1367,14 @@ var COLUMN_TYPES = [
870
1367
  ];
871
1368
  var columnTypeSchema = zod.z.enum(COLUMN_TYPES);
872
1369
  var canonicalEnumSchema = zod.z.object({
873
- name: identifier,
1370
+ name: identifier2,
874
1371
  values: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(100)
875
1372
  });
876
1373
  var canonicalColumnSchema = zod.z.object({
877
1374
  /** Property name in TS (camelCase); columnName is the Postgres name. */
878
1375
  name: zod.z.string().min(1).max(63),
879
1376
  columnName: columnIdentifier,
880
- type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier })]),
1377
+ type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier2 })]),
881
1378
  nullable: zod.z.boolean().default(true),
882
1379
  unique: zod.z.boolean().default(false),
883
1380
  /** Literal default, or one of the allowlisted SQL functions. */
@@ -893,7 +1390,14 @@ var canonicalColumnSchema = zod.z.object({
893
1390
  /** Migration hints — same semantics as v1. */
894
1391
  renamedFrom: columnIdentifier.optional(),
895
1392
  cast: zod.z.string().max(200).optional(),
896
- deprecated: zod.z.boolean().default(false)
1393
+ deprecated: zod.z.boolean().default(false),
1394
+ /**
1395
+ * Marks a system cell-metadata column (see CELL_PARTS in constants): part
1396
+ * `part` of the cell of logical column `of`. Synthesized by authoring,
1397
+ * never client-writable, absent from GraphQL scalar fields. Omitted on
1398
+ * ordinary columns so their hashes are unchanged.
1399
+ */
1400
+ cell: zod.z.object({ of: zod.z.string().min(1).max(63), part: zod.z.enum(CELL_PARTS) }).strict().optional()
897
1401
  }).superRefine((column, ctx) => {
898
1402
  if (column.onUpdateFn === void 0) return;
899
1403
  if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
@@ -908,36 +1412,28 @@ var canonicalLinkSchema = zod.z.object({
908
1412
  name: zod.z.string().min(1).max(63),
909
1413
  kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
910
1414
  /** Target table name (namespace-local). Absent for toPlatformUser. */
911
- target: identifier.optional(),
1415
+ target: identifier2.optional(),
1416
+ /** Owning namespace of an imported target; omitted for local links. */
1417
+ namespace: identifier2.max(41).optional(),
912
1418
  /** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
913
1419
  on: columnIdentifier,
914
1420
  /** Referenced column on the target (default: target's PK / platform user `sub`). */
915
1421
  references: columnIdentifier.optional(),
916
1422
  /** enforce: true emits a real FK constraint (same-namespace toOne only). */
917
1423
  enforce: zod.z.boolean().default(false),
1424
+ onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
1425
+ onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
918
1426
  description: zod.z.string().max(500).optional()
919
1427
  });
920
1428
  var canonicalIndexSchema = zod.z.object({
921
1429
  columns: zod.z.array(columnIdentifier).min(1),
922
1430
  unique: zod.z.boolean().default(false),
923
- name: identifier.optional()
1431
+ name: identifier2.optional()
924
1432
  });
925
- var policyVariantSchema = zod.z.object({
926
- /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
927
- when: zod.z.string().min(1).max(1e3),
928
- /** Exactly one of: allow-all bypass, or a row predicate. */
929
- allow: zod.z.literal(true).optional(),
930
- policy: zod.z.string().min(1).max(2e3).optional()
931
- }).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
932
- message: "A variant declares exactly one of allow: true or policy"
933
- });
934
- var policyExpressionSchema = zod.z.union([
935
- zod.z.string().min(1).max(2e3),
936
- zod.z.object({
937
- default: zod.z.string().min(1).max(2e3),
938
- variants: zod.z.array(policyVariantSchema).max(10).default([])
939
- })
940
- ]);
1433
+ var canonicalCheckSchema = zod.z.object({
1434
+ name: identifier2,
1435
+ expression: zod.z.string().min(1).max(2e3)
1436
+ }).strict();
941
1437
  var canonicalPoliciesSchema = zod.z.object({
942
1438
  read: policyExpressionSchema.optional(),
943
1439
  write: policyExpressionSchema.optional(),
@@ -967,8 +1463,165 @@ function isSingleViewQuerySql(value) {
967
1463
  }
968
1464
  return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
969
1465
  }
1466
+ var VIEW_QUERY_FUNCTION_ALLOWLIST = [
1467
+ "abs",
1468
+ "age",
1469
+ "array_agg",
1470
+ "array_append",
1471
+ "array_cat",
1472
+ "array_dims",
1473
+ "array_length",
1474
+ "array_lower",
1475
+ "array_ndims",
1476
+ "array_position",
1477
+ "array_positions",
1478
+ "array_prepend",
1479
+ "array_remove",
1480
+ "array_replace",
1481
+ "array_to_string",
1482
+ "array_trim",
1483
+ "array_upper",
1484
+ "ascii",
1485
+ "avg",
1486
+ "bit_and",
1487
+ "bit_or",
1488
+ "bit_xor",
1489
+ "bool_and",
1490
+ "bool_or",
1491
+ "btrim",
1492
+ "cardinality",
1493
+ "cbrt",
1494
+ "ceil",
1495
+ "ceiling",
1496
+ "char_length",
1497
+ "character_length",
1498
+ "chr",
1499
+ "concat",
1500
+ "concat_ws",
1501
+ "count",
1502
+ "cume_dist",
1503
+ "date_bin",
1504
+ "date_part",
1505
+ "date_trunc",
1506
+ "decode",
1507
+ "degrees",
1508
+ "dense_rank",
1509
+ "encode",
1510
+ "enum_first",
1511
+ "enum_last",
1512
+ "enum_range",
1513
+ "every",
1514
+ "exp",
1515
+ "first_value",
1516
+ "floor",
1517
+ "format",
1518
+ "grouping",
1519
+ "initcap",
1520
+ "json_agg",
1521
+ "json_array_length",
1522
+ "json_build_array",
1523
+ "json_build_object",
1524
+ "json_extract_path",
1525
+ "json_extract_path_text",
1526
+ "json_object_agg",
1527
+ "json_object_keys",
1528
+ "json_strip_nulls",
1529
+ "json_typeof",
1530
+ "jsonb_agg",
1531
+ "jsonb_array_length",
1532
+ "jsonb_build_array",
1533
+ "jsonb_build_object",
1534
+ "jsonb_extract_path",
1535
+ "jsonb_extract_path_text",
1536
+ "jsonb_insert",
1537
+ "jsonb_object_agg",
1538
+ "jsonb_object_keys",
1539
+ "jsonb_path_exists",
1540
+ "jsonb_path_match",
1541
+ "jsonb_path_query",
1542
+ "jsonb_path_query_array",
1543
+ "jsonb_path_query_first",
1544
+ "jsonb_pretty",
1545
+ "jsonb_set",
1546
+ "jsonb_set_lax",
1547
+ "jsonb_strip_nulls",
1548
+ "jsonb_typeof",
1549
+ "justify_days",
1550
+ "justify_hours",
1551
+ "justify_interval",
1552
+ "lag",
1553
+ "last_value",
1554
+ "lead",
1555
+ "left",
1556
+ "length",
1557
+ "ln",
1558
+ "log",
1559
+ "lower",
1560
+ "lpad",
1561
+ "ltrim",
1562
+ "make_date",
1563
+ "make_interval",
1564
+ "make_time",
1565
+ "make_timestamp",
1566
+ "make_timestamptz",
1567
+ "max",
1568
+ "md5",
1569
+ "min",
1570
+ "mod",
1571
+ "nth_value",
1572
+ "ntile",
1573
+ "octet_length",
1574
+ "percent_rank",
1575
+ "pi",
1576
+ "power",
1577
+ "quote_ident",
1578
+ "quote_literal",
1579
+ "quote_nullable",
1580
+ "radians",
1581
+ "rank",
1582
+ "regexp_count",
1583
+ "regexp_instr",
1584
+ "regexp_like",
1585
+ "regexp_match",
1586
+ "regexp_matches",
1587
+ "regexp_replace",
1588
+ "regexp_split_to_array",
1589
+ "regexp_split_to_table",
1590
+ "repeat",
1591
+ "replace",
1592
+ "reverse",
1593
+ "right",
1594
+ "round",
1595
+ "row_number",
1596
+ "rpad",
1597
+ "rtrim",
1598
+ "sign",
1599
+ "split_part",
1600
+ "sqrt",
1601
+ "starts_with",
1602
+ "string_agg",
1603
+ "string_to_array",
1604
+ "strpos",
1605
+ "substr",
1606
+ "substring",
1607
+ "sum",
1608
+ "timezone",
1609
+ "to_ascii",
1610
+ "to_char",
1611
+ "to_date",
1612
+ "to_hex",
1613
+ "to_json",
1614
+ "to_jsonb",
1615
+ "to_number",
1616
+ "to_timestamp",
1617
+ "translate",
1618
+ "trunc",
1619
+ "unnest",
1620
+ "upper",
1621
+ "width_bucket"
1622
+ ];
970
1623
  var canonicalActionSchema = zod.z.object({
971
- name: identifier,
1624
+ name: identifier2,
972
1625
  requires: zod.z.string().max(1e3).optional(),
973
1626
  params: zod.z.record(zod.z.string(), zod.z.object({
974
1627
  type: columnTypeSchema,
@@ -980,9 +1633,9 @@ var canonicalActionSchema = zod.z.object({
980
1633
  });
981
1634
  var canonicalCowMutationSchema = zod.z.object({
982
1635
  kind: zod.z.enum(["override", "revert"]),
983
- name: identifier,
1636
+ name: identifier2,
984
1637
  /** Synced table (same namespace) the overlay copies from / falls back to. */
985
- synced: identifier,
1638
+ synced: identifier2,
986
1639
  /** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
987
1640
  pk: zod.z.string().min(1).max(63),
988
1641
  /** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
@@ -1010,23 +1663,28 @@ var canonicalCowMutationSchema = zod.z.object({
1010
1663
  }
1011
1664
  });
1012
1665
  var canonicalForeignKeySchema = zod.z.object({
1013
- name: identifier,
1666
+ name: identifier2,
1014
1667
  columns: zod.z.array(columnIdentifier).min(1).max(8),
1015
- target: identifier,
1668
+ target: identifier2,
1016
1669
  references: zod.z.array(columnIdentifier).min(1).max(8),
1017
- deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional()
1670
+ deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional(),
1671
+ onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
1672
+ onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional()
1018
1673
  }).strict();
1019
1674
  var canonicalTableSchema = zod.z.object({
1020
- table: identifier,
1675
+ table: identifier2,
1676
+ displayName: zod.z.string().trim().min(1).max(120).optional(),
1021
1677
  description: zod.z.string().max(2e3).optional(),
1022
1678
  /** Table-level rename hint (new in v2). */
1023
- renamedFrom: identifier.optional(),
1024
- /** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
1025
- source: zod.z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
1679
+ renamedFrom: identifier2.optional(),
1680
+ /** Synced/reflected read-only tables (`source: schema.table`) — v1 semantics. */
1681
+ source: zod.z.string().regex(QUALIFIED_TABLE_PATTERN).optional(),
1026
1682
  columns: zod.z.array(canonicalColumnSchema).default([]),
1027
1683
  primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
1028
1684
  links: zod.z.array(canonicalLinkSchema).default([]),
1029
1685
  indexes: zod.z.array(canonicalIndexSchema).default([]),
1686
+ /** Omitted by old definitions, preserving their existing content hashes. */
1687
+ checks: zod.z.array(canonicalCheckSchema).max(100).optional(),
1030
1688
  policies: canonicalPoliciesSchema.default({}),
1031
1689
  /**
1032
1690
  * v1-conversion carry-over only. v2 has no action surface — the runtime
@@ -1037,22 +1695,55 @@ var canonicalTableSchema = zod.z.object({
1037
1695
  /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
1038
1696
  cowMutations: zod.z.array(canonicalCowMutationSchema).optional(),
1039
1697
  /** Omitted by old definitions, preserving their existing content hashes. */
1040
- foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional()
1698
+ foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional(),
1699
+ /**
1700
+ * Per-cell edit metadata (`cellMetadata: true` in authoring): every tracked
1701
+ * column `c` carries typed `c__at` / `c__by` system columns (plus `c__rev`
1702
+ * on a resolved table's internal sources), listed explicitly in `columns`
1703
+ * with a `cell` marker so DDL and migrations treat them as ordinary columns.
1704
+ * Omitted when off so existing definitions hash unchanged.
1705
+ */
1706
+ cellMetadata: zod.z.literal(true).optional(),
1707
+ /**
1708
+ * System-owned backing table of a resolved table (its manual or prefetch
1709
+ * source). Internal tables get DDL and IR metadata but no GraphQL surface;
1710
+ * only the data plane writes them. Omitted when false.
1711
+ */
1712
+ internal: zod.z.literal(true).optional(),
1713
+ /**
1714
+ * The materialized copy of resolved table `materializes` (internal): the
1715
+ * same columns as that view, including its cell parts, maintained by the
1716
+ * data plane. Omitted otherwise.
1717
+ */
1718
+ materializes: identifier2.optional()
1041
1719
  });
1720
+ var MAX_VIEW_QUERY = 1e5;
1721
+ var MAX_RESOLVED_VIEW_QUERY = 5e6;
1042
1722
  var canonicalViewSchema = zod.z.object({
1043
- view: identifier,
1723
+ view: identifier2,
1044
1724
  description: zod.z.string().max(2e3).optional(),
1045
1725
  columns: zod.z.array(canonicalColumnSchema).min(1),
1046
1726
  /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
1047
1727
  primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
1048
1728
  links: zod.z.array(canonicalLinkSchema).default([]),
1049
1729
  policies: zod.z.object({ read: policyExpressionSchema }).strict(),
1050
- query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
1730
+ query: zod.z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
1051
1731
  message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
1052
1732
  }),
1053
1733
  /** Namespace-local logical relation names, sorted for deterministic hashing. */
1054
- dependencies: zod.z.array(identifier).max(500)
1734
+ dependencies: zod.z.array(identifier2).max(500),
1735
+ /**
1736
+ * Present when this view is a resolved table (defineResolvedTable): the
1737
+ * multi-source metadata behind its generated query. Omitted otherwise so
1738
+ * existing views hash unchanged.
1739
+ */
1740
+ resolved: canonicalResolvedSchema.optional(),
1741
+ /** System-owned view (a materialized resolved table's compute view): no GraphQL surface. */
1742
+ internal: zod.z.literal(true).optional()
1055
1743
  }).superRefine((view, ctx) => {
1744
+ if (view.resolved === void 0 && view.query.length > MAX_VIEW_QUERY) {
1745
+ ctx.addIssue({ code: "custom", path: ["query"], message: `view query must be at most ${MAX_VIEW_QUERY} characters` });
1746
+ }
1056
1747
  for (const [index, column] of view.columns.entries()) {
1057
1748
  if (column.default !== void 0 || column.defaultFn !== void 0) {
1058
1749
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
@@ -1067,20 +1758,91 @@ var canonicalViewSchema = zod.z.object({
1067
1758
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare migration hints` });
1068
1759
  }
1069
1760
  }
1761
+ if (view.resolved === void 0 && view.internal !== true) {
1762
+ for (const [index, column] of view.columns.entries()) {
1763
+ if (column.cell) ctx.addIssue({ code: "custom", path: ["columns", index], message: `only resolved tables carry cell columns ('${column.name}')` });
1764
+ }
1765
+ }
1070
1766
  for (const [index, link] of view.links.entries()) {
1071
1767
  if (link.enforce) {
1072
1768
  ctx.addIssue({ code: "custom", path: ["links", index, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
1073
1769
  }
1770
+ if (link.onDelete !== void 0 || link.onUpdate !== void 0) {
1771
+ ctx.addIssue({ code: "custom", path: ["links", index], message: `view link '${link.name}' cannot declare referential actions` });
1772
+ }
1074
1773
  }
1075
1774
  });
1076
- var canonicalDefinitionSchema = zod.z.object({
1775
+ var CELL_PART_TYPES = {
1776
+ at: "timestamptz",
1777
+ by: "text",
1778
+ rev: "uuid",
1779
+ source: "text",
1780
+ conflicts: "jsonb"
1781
+ };
1782
+ function trackedCellColumns(table) {
1783
+ const pk = new Set(table.primaryKey ?? []);
1784
+ return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
1785
+ }
1786
+ var SUFFIXES = Object.values(CELL_PART_SUFFIX);
1787
+ function validateCellMetadata(table, index, ctx) {
1788
+ const fail = (message, path = ["tables", index]) => ctx.addIssue({ code: "custom", path, message });
1789
+ const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
1790
+ if (table.materializes !== void 0) return;
1791
+ if (table.cellMetadata !== true) {
1792
+ for (const { position } of cells) fail(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index, "columns", position]);
1793
+ return;
1794
+ }
1795
+ if (table.source !== void 0) {
1796
+ fail(`synced table '${table.table}' cannot track cell metadata`);
1797
+ return;
1798
+ }
1799
+ const tracked = trackedCellColumns(table);
1800
+ const byName = new Map(tracked.map((column) => [column.name, column]));
1801
+ const withRev = cells.some(({ cell }) => cell.part === "rev");
1802
+ if (withRev && table.internal !== true) fail(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
1803
+ const seen = /* @__PURE__ */ new Set();
1804
+ for (const { column, position, cell } of cells) {
1805
+ const path = ["tables", index, "columns", position];
1806
+ const owner = byName.get(cell.of);
1807
+ if (!owner) {
1808
+ fail(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
1809
+ continue;
1810
+ }
1811
+ 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);
1812
+ if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
1813
+ fail(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
1814
+ }
1815
+ if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
1816
+ fail(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
1817
+ }
1818
+ const key = `${cell.of}:${cell.part}`;
1819
+ if (seen.has(key)) fail(`duplicate cell column '${column.name}'`, path);
1820
+ seen.add(key);
1821
+ }
1822
+ for (const column of tracked) {
1823
+ if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
1824
+ fail(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
1825
+ }
1826
+ if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
1827
+ fail(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
1828
+ }
1829
+ for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
1830
+ if (!seen.has(`${column.name}:${part}`)) fail(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
1831
+ }
1832
+ }
1833
+ }
1834
+ var exportGateSchema = zod.z.union([zod.z.literal(true), zod.z.object({ capability: zod.z.string().min(1) }).strict(), zod.z.object({ role: zod.z.string().min(1) }).strict()]);
1835
+ var relationExportSchema = zod.z.object({ read: exportGateSchema, write: exportGateSchema.optional() }).strict();
1836
+ var localDefinitionSchema = zod.z.object({
1077
1837
  definition_format: zod.z.literal(CANONICAL_FORMAT),
1078
- namespace: identifier.max(41),
1838
+ namespace: identifier2.max(41),
1079
1839
  description: zod.z.string().max(2e3).optional(),
1080
1840
  enums: zod.z.array(canonicalEnumSchema).default([]),
1081
1841
  tables: zod.z.array(canonicalTableSchema).min(0).max(500),
1082
1842
  /** Optional to preserve byte-identical hashes for pre-view canonical v2 documents. */
1083
1843
  views: zod.z.array(canonicalViewSchema).max(500).optional(),
1844
+ /** Target-owned cross-namespace access contract, using existing namespace grants. */
1845
+ exports: zod.z.record(identifier2, relationExportSchema).optional(),
1084
1846
  /** Verbatim custom-roles grammar (@sequenceholdings/custom-roles-manifest). */
1085
1847
  capabilities: zod.z.object({
1086
1848
  roles: zod.z.unknown().optional(),
@@ -1093,6 +1855,7 @@ var canonicalDefinitionSchema = zod.z.object({
1093
1855
  ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
1094
1856
  }
1095
1857
  seen.add(table.table);
1858
+ validateCellMetadata(table, index, ctx);
1096
1859
  if (table.source !== void 0) {
1097
1860
  for (const [columnIndex, column] of table.columns.entries()) {
1098
1861
  if (column.onUpdateFn !== void 0) {
@@ -1106,12 +1869,41 @@ var canonicalDefinitionSchema = zod.z.object({
1106
1869
  }
1107
1870
  }
1108
1871
  const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
1872
+ const relations = /* @__PURE__ */ new Set([...fkTables.keys(), ...(definition.views ?? []).map((view) => view.view)]);
1873
+ for (const name2 of Object.keys(definition.exports ?? {})) {
1874
+ if (!relations.has(name2)) ctx.addIssue({ code: "custom", path: ["exports", name2], message: `export '${name2}' is not a local object` });
1875
+ }
1109
1876
  for (const [tableIndex, table] of definition.tables.entries()) {
1877
+ for (const [linkIndex, link] of table.links.entries()) {
1878
+ if (link.enforce && link.namespace !== void 0 && link.namespace !== definition.namespace) {
1879
+ ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", linkIndex], message: "physical foreign keys require a local target; use an ORM link without enforce" });
1880
+ }
1881
+ }
1110
1882
  const names = /* @__PURE__ */ new Set([
1111
1883
  ...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
1112
1884
  ...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
1113
1885
  ...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
1114
1886
  ]);
1887
+ for (const [index, link] of table.links.entries()) {
1888
+ if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && (!link.enforce || link.kind !== "toOne")) {
1889
+ ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index], message: "referential actions require an enforced toOne link" });
1890
+ }
1891
+ if (link.onDelete === "set null" || link.onUpdate === "set null") {
1892
+ const source = table.columns.find((column) => column.columnName === link.on);
1893
+ if (source?.nullable === false || table.primaryKey?.includes(link.on)) ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index], message: "SET NULL requires a nullable source column" });
1894
+ }
1895
+ }
1896
+ for (const [index, check] of (table.checks ?? []).entries()) {
1897
+ const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index], message });
1898
+ if (table.source) fail("synced tables cannot declare checks");
1899
+ if (names.has(check.name)) fail(`duplicate constraint '${check.name}'`);
1900
+ names.add(check.name);
1901
+ try {
1902
+ compileCheckExpression(check.expression, table.columns);
1903
+ } catch (error) {
1904
+ fail(error instanceof Error ? error.message : String(error));
1905
+ }
1906
+ }
1115
1907
  for (const [index, fk] of (table.foreignKeys ?? []).entries()) {
1116
1908
  const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
1117
1909
  if (names.has(fk.name)) fail(`duplicate foreign key '${fk.name}'`);
@@ -1123,16 +1915,24 @@ var canonicalDefinitionSchema = zod.z.object({
1123
1915
  }
1124
1916
  if (fk.columns.length !== fk.references.length) fail("foreign key columns and references must have the same number of columns");
1125
1917
  if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail("foreign key columns must not repeat");
1126
- for (const [position, name] of fk.columns.entries()) {
1127
- const sourceColumn = table.columns.find((column) => column.columnName === name);
1918
+ for (const [position, name2] of fk.columns.entries()) {
1919
+ const sourceColumn = table.columns.find((column) => column.columnName === name2);
1128
1920
  const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
1129
1921
  if (!sourceColumn || !targetColumn) fail("foreign key references an unknown column");
1130
1922
  else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail("foreign key column types must match");
1131
1923
  }
1924
+ if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
1925
+ 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");
1926
+ }
1132
1927
  const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index2) => index2.unique).map((index2) => index2.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
1133
1928
  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");
1134
1929
  }
1135
1930
  }
1931
+ for (const [viewIndex, view] of (definition.views ?? []).entries()) {
1932
+ for (const message of validateResolvedView({ view, tables: definition.tables, views: definition.views ?? [] })) {
1933
+ ctx.addIssue({ code: "custom", path: ["views", viewIndex, "resolved"], message });
1934
+ }
1935
+ }
1136
1936
  const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
1137
1937
  for (const [tableIndex, table] of definition.tables.entries()) {
1138
1938
  const cowMutations = table.cowMutations ?? [];
@@ -1227,40 +2027,62 @@ var canonicalDefinitionSchema = zod.z.object({
1227
2027
  const visiting = /* @__PURE__ */ new Set();
1228
2028
  const visited = /* @__PURE__ */ new Set();
1229
2029
  let cycleReported = false;
1230
- const visit = (name, path) => {
1231
- if (visited.has(name) || cycleReported) return;
1232
- if (visiting.has(name)) {
2030
+ const visit = (name2, path) => {
2031
+ if (visited.has(name2) || cycleReported) return;
2032
+ if (visiting.has(name2)) {
1233
2033
  cycleReported = true;
1234
2034
  ctx.addIssue({
1235
2035
  code: "custom",
1236
2036
  path: ["views"],
1237
- message: `view dependencies form a cycle: ${[...path, name].join(" -> ")}`
2037
+ message: `view dependencies form a cycle: ${[...path, name2].join(" -> ")}`
1238
2038
  });
1239
2039
  return;
1240
2040
  }
1241
- visiting.add(name);
1242
- const view = viewByName.get(name);
2041
+ visiting.add(name2);
2042
+ const view = viewByName.get(name2);
1243
2043
  for (const dependency of view?.dependencies ?? []) {
1244
- if (viewByName.has(dependency)) visit(dependency, [...path, name]);
2044
+ if (viewByName.has(dependency)) visit(dependency, [...path, name2]);
1245
2045
  }
1246
- visiting.delete(name);
1247
- visited.add(name);
2046
+ visiting.delete(name2);
2047
+ visited.add(name2);
1248
2048
  };
1249
2049
  for (const view of views) visit(view.view, []);
1250
2050
  });
2051
+ var canonicalDefinitionSchema = localDefinitionSchema.safeExtend({
2052
+ /** Flattened, pinned snapshots. Imported relations never participate in this namespace's DDL. */
2053
+ imports: zod.z.array(localDefinitionSchema).max(32).optional()
2054
+ });
2055
+ function canonicalRelations(definition) {
2056
+ const views = (definition.views ?? []).map((view) => ({
2057
+ table: view.view,
2058
+ ...view.description !== void 0 ? { description: view.description } : {},
2059
+ columns: view.columns,
2060
+ ...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
2061
+ links: view.links,
2062
+ indexes: [],
2063
+ // A resolved table's write policies (evaluated against the resolved row)
2064
+ // ride alongside the view's read policy.
2065
+ policies: { read: view.policies.read, ...view.resolved?.policies ?? {} },
2066
+ actions: [],
2067
+ ...view.resolved !== void 0 ? { resolved: view.resolved } : {},
2068
+ ...view.internal ? { internal: true } : {}
2069
+ }));
2070
+ return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
2071
+ }
1251
2072
  function normalizeCanonical(definition) {
1252
2073
  const sortByName = (items) => [...items].sort((a, b) => a.name.localeCompare(b.name));
1253
2074
  const normalized = {
1254
2075
  ...definition,
1255
2076
  enums: sortByName(definition.enums),
1256
2077
  tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
1257
- const { cowMutations, foreignKeys, ...rest } = t2;
2078
+ const { cowMutations, foreignKeys, checks, ...rest } = t2;
1258
2079
  return {
1259
2080
  ...rest,
1260
2081
  links: sortByName(t2.links),
1261
2082
  indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
1262
2083
  actions: sortByName(t2.actions),
1263
2084
  ...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
2085
+ ...checks?.length ? { checks: sortByName(checks) } : {},
1264
2086
  // Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
1265
2087
  ...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
1266
2088
  };
@@ -1273,8 +2095,461 @@ function normalizeCanonical(definition) {
1273
2095
  dependencies: [...view.dependencies].sort()
1274
2096
  }));
1275
2097
  }
2098
+ if (definition.imports?.length) {
2099
+ normalized.imports = definition.imports.map(normalizeCanonical).sort((a, b) => a.namespace.localeCompare(b.namespace));
2100
+ } else {
2101
+ delete normalized.imports;
2102
+ }
1276
2103
  return normalized;
1277
2104
  }
2105
+
2106
+ // src/authoring/resolved-sources.ts
2107
+ function cloneColumn({ builder, property, notNull, primaryKey }) {
2108
+ const canonical = builder.toCanonicalColumn(property);
2109
+ let clone = new ColumnBuilder(canonical.type, canonical.columnName);
2110
+ if (primaryKey) clone = clone.primaryKey();
2111
+ if (notNull) clone = clone.notNull();
2112
+ if (canonical.description !== void 0) clone = clone.description(canonical.description);
2113
+ return clone;
2114
+ }
2115
+ function planSource(input) {
2116
+ const { sourceName, input: source, fail } = input;
2117
+ if (!IDENTIFIER_PATTERN.test(sourceName)) fail(`source name "${sourceName}" must match ${IDENTIFIER_PATTERN}`);
2118
+ switch (source.kind) {
2119
+ case "manual":
2120
+ return {
2121
+ name: sourceName,
2122
+ kind: "manual",
2123
+ columns: input.dataColumns,
2124
+ table: internalTable(input, input.dataColumns, {
2125
+ [RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.name]: tombstoned(),
2126
+ [RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackStatus.name]: t.text(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackStatus.columnName),
2127
+ [RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackError.name]: t.text(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackError.columnName),
2128
+ [RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackAt.name]: t.timestamptz(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackAt.columnName)
2129
+ }, { revisions: true })
2130
+ };
2131
+ case "etl":
2132
+ return planEtl(input, source);
2133
+ case "prefetch":
2134
+ return planPrefetch(input, source);
2135
+ }
2136
+ }
2137
+ var tombstoned = () => t.boolean(RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName).notNull().default(false);
2138
+ function internalTable(input, supplied, system, options = {}) {
2139
+ const { viewName, sourceName, columns, pk, pkBuilder } = input;
2140
+ const tableColumns = { [pk]: cloneColumn({ builder: pkBuilder, property: pk, notNull: true, primaryKey: true }) };
2141
+ for (const property of supplied) {
2142
+ tableColumns[property] = cloneColumn({ builder: columns[property], property, notNull: false, primaryKey: false });
2143
+ }
2144
+ return defineTable(`${viewName}__${sourceName}`, { ...tableColumns, ...system }, {
2145
+ description: `Internal ${input.input.kind} source "${sourceName}" of resolved table "${viewName}". System-maintained.`,
2146
+ cellMetadata: true,
2147
+ // The manual source identifies each edit (`c__rev`) so an async
2148
+ // confirmation clears only the exact edit it delivered.
2149
+ ...options.revisions ? { cellRevisions: true } : {},
2150
+ internal: true
2151
+ });
2152
+ }
2153
+ function assertColumns(input, list, what) {
2154
+ for (const column of list) if (!input.dataColumns.includes(column)) input.fail(`${what} names unknown column "${column}"`);
2155
+ }
2156
+ function planEtl(input, source) {
2157
+ const { sourceName, pk, dataColumns, fail } = input;
2158
+ const table = typeof source.table === "function" ? source.table() : source.table;
2159
+ if (!isSeqTable(table) || table.source === void 0) fail(`etl source "${sourceName}" must reference a defineSyncedTable`);
2160
+ const synced = table;
2161
+ if (!(pk in synced.columns)) fail(`etl source "${sourceName}" table "${synced.tableName}" has no "${pk}" column`);
2162
+ const supplied = source.columns ? [...source.columns] : dataColumns.filter((column) => column in synced.columns);
2163
+ assertColumns(input, supplied, `etl source "${sourceName}"`);
2164
+ if (source.updatedAt !== void 0 && !(source.updatedAt in synced.columns)) {
2165
+ fail(`etl source "${sourceName}" updatedAt "${source.updatedAt}" is not a column of "${synced.tableName}"`);
2166
+ }
2167
+ return { name: sourceName, kind: "etl", table: synced, columns: supplied, ...source.updatedAt ? { updatedAt: source.updatedAt } : {} };
2168
+ }
2169
+ function planPrefetch(input, source) {
2170
+ const { sourceName, pk, fail } = input;
2171
+ assertColumns(input, source.columns, `prefetch source "${sourceName}"`);
2172
+ if (source.key !== pk && !source.columns.includes(source.key)) {
2173
+ fail(`prefetch source "${sourceName}" key "${source.key}" must be the primary key or one of its columns`);
2174
+ }
2175
+ for (const column of source.writeback?.columns ?? []) {
2176
+ if (!source.columns.includes(column)) fail(`prefetch source "${sourceName}" writes back "${column}", which it does not supply`);
2177
+ }
2178
+ const writeback = source.writeback;
2179
+ return {
2180
+ name: sourceName,
2181
+ kind: "prefetch",
2182
+ columns: [...source.columns],
2183
+ key: source.key,
2184
+ function: source.function,
2185
+ fetch: source.fetch === "single" ? { shape: "single" } : { shape: "batch", maxKeys: source.fetch.batch.maxKeys },
2186
+ prefetchDefaults: {
2187
+ maxStalenessMs: parseDuration(source.prefetchDefaults?.maxStaleness ?? "30s"),
2188
+ timeoutMs: parseDuration(source.prefetchDefaults?.timeout ?? "1500ms"),
2189
+ onError: source.prefetchDefaults?.onError ?? "fallback"
2190
+ },
2191
+ ...writeback ? {
2192
+ writeback: {
2193
+ columns: [...writeback.columns],
2194
+ defaultMode: writeback.defaultMode ?? "sync",
2195
+ timeoutMs: parseDuration(writeback.timeout ?? "5s"),
2196
+ clearManualOnConfirm: writeback.clearManualOnConfirm ?? true
2197
+ }
2198
+ } : {},
2199
+ table: internalTable(input, source.columns, {
2200
+ [RESOLVED_SOURCE_SYSTEM_COLUMNS.fetchedAt.name]: t.timestamptz(RESOLVED_SOURCE_SYSTEM_COLUMNS.fetchedAt.columnName).notNull().default("now()"),
2201
+ [RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.name]: tombstoned()
2202
+ })
2203
+ };
2204
+ }
2205
+ function parseDuration(input) {
2206
+ const value = parseDurationMs(input);
2207
+ if (value === void 0) throw new OrmDefinitionError(`invalid duration "${input}" \u2014 use e.g. '1500ms', '30s', '5m'`);
2208
+ return value;
2209
+ }
2210
+ var PG_TYPE = {
2211
+ uuid: "uuid",
2212
+ text: "text",
2213
+ integer: "integer",
2214
+ bigint: "bigint",
2215
+ numeric: "numeric",
2216
+ double: "double precision",
2217
+ boolean: "boolean",
2218
+ timestamptz: "timestamptz",
2219
+ date: "date",
2220
+ jsonb: "jsonb"
2221
+ };
2222
+ var ident = (value) => `"${value.replace(/"/g, '""')}"`;
2223
+ var literal = (value) => `'${value.replace(/'/g, "''")}'`;
2224
+ var physical = ({ source, property }) => source.table.columns[property].columnNameFor(property);
2225
+ function candidateSql({ source, column, pk, pgType }) {
2226
+ const a = ident(`s_${source.name}`);
2227
+ const name2 = physical({ source, property: column });
2228
+ const value = `CAST(${a}.${ident(name2)} AS ${pgType})`;
2229
+ if (source.kind === "etl") {
2230
+ return {
2231
+ source: source.name,
2232
+ value,
2233
+ has: `(${a}.${ident(physical({ source, property: pk }))} IS NOT NULL)`,
2234
+ at: source.updatedAt ? `CAST(${a}.${ident(physical({ source, property: source.updatedAt }))} AS timestamptz)` : "CAST(NULL AS timestamptz)",
2235
+ by: "CAST(NULL AS text)"
2236
+ };
2237
+ }
2238
+ const at = `${a}.${ident(cellColumnName(name2, "at"))}`;
2239
+ const tombstoned2 = source.kind === "prefetch" ? ` AND NOT ${a}.${ident(RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName)}` : "";
2240
+ return { source: source.name, value, has: `(${at} IS NOT NULL${tombstoned2})`, at, by: `${a}.${ident(cellColumnName(name2, "by"))}` };
2241
+ }
2242
+ function outranks({ i, j, key, direction, inclusive }) {
2243
+ const ki = i[key];
2244
+ const kj = j[key];
2245
+ const op = inclusive ? `${direction}=` : direction;
2246
+ 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}))`;
2247
+ }
2248
+ function rankedWinner({ candidates, key, direction }) {
2249
+ const branches = candidates.map((candidate, index) => {
2250
+ const beats = candidates.map((other, otherIndex) => otherIndex === index ? null : `(NOT ${other.has} OR ${outranks({ i: candidate, j: other, key, direction, inclusive: index < otherIndex })})`).filter((clause) => clause !== null);
2251
+ return `WHEN ${[candidate.has, ...beats].join(" AND ")} THEN ${index + 1}`;
2252
+ });
2253
+ return `CASE ${branches.join(" ")} END`;
2254
+ }
2255
+ function winnerIndex({ comparator, candidates }) {
2256
+ const firstWith = (test) => candidates.map((candidate, index) => `WHEN ${test(candidate)} THEN ${index + 1}`).join(" ");
2257
+ if (candidates.length === 1 || comparator === "priority") return `CASE ${firstWith((c) => c.has)} END`;
2258
+ switch (comparator) {
2259
+ case "latest":
2260
+ return rankedWinner({ candidates, key: "at", direction: ">" });
2261
+ case "highest":
2262
+ return rankedWinner({ candidates, key: "value", direction: ">" });
2263
+ case "lowest":
2264
+ return rankedWinner({ candidates, key: "value", direction: "<" });
2265
+ case "nonNullFirst":
2266
+ return `CASE ${firstWith((c) => `${c.has} AND ${c.value} IS NOT NULL`)} ${firstWith((c) => c.has)} END`;
2267
+ }
2268
+ }
2269
+ function pick(winner, candidates, field) {
2270
+ return `CASE ${winner} ${candidates.map((candidate, index) => `WHEN ${index + 1} THEN ${field(candidate)}`).join(" ")} END`;
2271
+ }
2272
+ function loserSql({ loser, winner }) {
2273
+ 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`;
2274
+ }
2275
+ function losersAgainst({ candidates, winner, isWinner }) {
2276
+ const losers = candidates.flatMap((loser, index) => {
2277
+ const excluded = isWinner(loser, index);
2278
+ if (excluded === true) return [];
2279
+ const clause = loserSql({ loser, winner });
2280
+ return [excluded === false ? clause : `CASE WHEN ${excluded} THEN '[]'::jsonb ELSE ${clause} END`];
2281
+ });
2282
+ return losers.length === 0 ? "CAST(NULL AS jsonb)" : `NULLIF(${losers.join(" || ")}, '[]'::jsonb)`;
2283
+ }
2284
+ function builtinCell({ comparator, candidates }) {
2285
+ const winner = winnerIndex({ comparator, candidates });
2286
+ const conflicts = comparator === "latest" || candidates.length < 2 ? "CAST(NULL AS jsonb)" : `CASE ${winner} ${candidates.map((known, index) => `WHEN ${index + 1} THEN ${losersAgainst({ candidates, winner: known, isWinner: (_c, other) => other === index })}`).join(" ")} END`;
2287
+ return {
2288
+ value: pick(winner, candidates, (c) => c.value),
2289
+ parts: {
2290
+ source: pick(winner, candidates, (c) => literal(c.source)),
2291
+ at: pick(winner, candidates, (c) => c.at),
2292
+ by: pick(winner, candidates, (c) => c.by),
2293
+ conflicts
2294
+ }
2295
+ };
2296
+ }
2297
+ function customCell({ candidates, orderBy, index }) {
2298
+ const w = ident(`w_${index}`);
2299
+ const rows = candidates.map((c, ord) => `(${literal(c.source)}, ${c.has}, ${c.value}, ${c.at}, ${c.by}, ${ord})`);
2300
+ 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`;
2301
+ const winner = { value: `${w}.value`, at: `${w}.updated_at` };
2302
+ return {
2303
+ value: winner.value,
2304
+ lateral,
2305
+ parts: {
2306
+ source: `${w}.source`,
2307
+ at: winner.at,
2308
+ by: `${w}.actor`,
2309
+ conflicts: candidates.length < 2 ? "CAST(NULL AS jsonb)" : losersAgainst({ candidates, winner, isWinner: (c) => `${w}.source = ${literal(c.source)}` })
2310
+ }
2311
+ };
2312
+ }
2313
+ function buildResolvedQuery({ context, plan }) {
2314
+ const { qb, relation } = context;
2315
+ const pkColumn = plan.columns[plan.pk].columnNameFor(plan.pk);
2316
+ const alias = (source) => ident(`s_${source}`);
2317
+ const tombstoned2 = RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName;
2318
+ const sourceByName = new Map(plan.plans.map((source) => [source.name, source]));
2319
+ const schema = pgCore.getTableConfig(relation(plan.plans[0].table)).schema;
2320
+ const keyBranches = plan.plans.map((source) => {
2321
+ const where = source.kind === "prefetch" ? drizzleOrm.sql.raw(` WHERE NOT ${ident(tombstoned2)}`) : drizzleOrm.sql.raw("");
2322
+ const sourcePk = ident(physical({ source, property: plan.pk }));
2323
+ const selectedPk = sourcePk === ident(pkColumn) ? sourcePk : `${sourcePk} AS ${ident(pkColumn)}`;
2324
+ return drizzleOrm.sql`SELECT ${drizzleOrm.sql.raw(selectedPk)} FROM ${relation(source.table)}${where}`;
2325
+ });
2326
+ const joins = plan.plans.map(
2327
+ (source) => drizzleOrm.sql` LEFT JOIN ${relation(source.table)} AS ${drizzleOrm.sql.raw(alias(source.name))} ON ${drizzleOrm.sql.raw(`${alias(source.name)}.${ident(physical({ source, property: plan.pk }))} = "k".${ident(pkColumn)}`)}`
2328
+ );
2329
+ const cells = /* @__PURE__ */ new Map();
2330
+ plan.rules.forEach((rule, index) => {
2331
+ const canonical = plan.columns[rule.column].toCanonicalColumn(rule.column);
2332
+ const pgType = typeof canonical.type === "string" ? PG_TYPE[canonical.type] : `${schema === void 0 ? "" : `${ident(schema)}.`}${ident(canonical.type.enum)}`;
2333
+ const candidates = rule.candidates.map((name2) => candidateSql({ source: sourceByName.get(name2), column: rule.column, pk: plan.pk, pgType }));
2334
+ cells.set(rule.column, rule.comparator === "custom" ? customCell({ candidates, orderBy: rule.orderBy, index }) : builtinCell({ comparator: rule.comparator, candidates }));
2335
+ });
2336
+ const laterals = [...cells.values()].flatMap((cell) => cell.lateral ? [drizzleOrm.sql.raw(cell.lateral)] : []);
2337
+ const from = drizzleOrm.sql.join([drizzleOrm.sql`(${drizzleOrm.sql.join(keyBranches, drizzleOrm.sql.raw(" UNION "))}) AS "k"`, ...joins, ...laterals], drizzleOrm.sql.raw(""));
2338
+ const selected = {
2339
+ [plan.pk]: drizzleOrm.sql.raw(`"k".${ident(pkColumn)}`).as(pkColumn)
2340
+ };
2341
+ for (const [property, builder] of Object.entries(plan.columns)) {
2342
+ if (property === plan.pk) continue;
2343
+ const cell = cells.get(property);
2344
+ const columnName = builder.columnNameFor(property);
2345
+ selected[property] = drizzleOrm.sql.raw(cell.value).as(columnName);
2346
+ for (const [part, expression] of Object.entries(cell.parts)) {
2347
+ selected[cellColumnName(property, part)] = drizzleOrm.sql.raw(expression).as(cellColumnName(columnName, part));
2348
+ }
2349
+ }
2350
+ const manual = plan.plans.find((source) => source.kind === "manual");
2351
+ const system = RESOLVED_VIEW_SYSTEM_COLUMNS;
2352
+ selected[system.writebackStatus.name] = drizzleOrm.sql.raw(`${alias(manual.name)}.${ident(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackStatus.columnName)}`).as(system.writebackStatus.columnName);
2353
+ selected[system.writebackError.name] = drizzleOrm.sql.raw(`${alias(manual.name)}.${ident(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackError.columnName)}`).as(system.writebackError.columnName);
2354
+ const hidden = plan.deletedBy.map((name2) => `NOT COALESCE(${alias(name2)}.${ident(tombstoned2)}, false)`);
2355
+ const query = qb.select(selected).from(from);
2356
+ return hidden.length > 0 ? query.where(drizzleOrm.sql.raw(hidden.join(" AND "))) : query;
2357
+ }
2358
+ function managedFn(id) {
2359
+ return { kind: "managed", id };
2360
+ }
2361
+ function manualSource() {
2362
+ return { kind: "manual" };
2363
+ }
2364
+ function etlSource(input) {
2365
+ return { kind: "etl", ...input };
2366
+ }
2367
+ function prefetchSource(input) {
2368
+ return { kind: "prefetch", ...input };
2369
+ }
2370
+ var latest = (candidates) => ({ kind: "latest", candidates });
2371
+ var highest = (candidates) => ({ kind: "highest", candidates });
2372
+ var lowest = (candidates) => ({ kind: "lowest", candidates });
2373
+ var priority = (candidates) => ({ kind: "priority", candidates });
2374
+ var nonNullFirst = (candidates) => ({ kind: "nonNullFirst", candidates });
2375
+ var custom = (candidates, orderBy) => ({ kind: "custom", candidates, orderBy });
2376
+ var SYSTEM_VIEW_COLUMNS = {
2377
+ [RESOLVED_VIEW_SYSTEM_COLUMNS.writebackStatus.name]: () => t.text(RESOLVED_VIEW_SYSTEM_COLUMNS.writebackStatus.columnName),
2378
+ [RESOLVED_VIEW_SYSTEM_COLUMNS.writebackError.name]: () => t.text(RESOLVED_VIEW_SYSTEM_COLUMNS.writebackError.columnName)
2379
+ };
2380
+ function defineResolvedTable(name2, columns, config) {
2381
+ const file = activeAuthoringContext()?.currentFile;
2382
+ const fail = (message) => {
2383
+ throw new OrmDefinitionError(`resolved table "${name2}": ${message}`, file);
2384
+ };
2385
+ if (!IDENTIFIER_PATTERN.test(name2)) fail(`name must match ${IDENTIFIER_PATTERN}`);
2386
+ for (const property of Object.keys(columns)) {
2387
+ if (RESERVED_RESOLVED_COLUMN_NAMES.has(property) || RESERVED_RESOLVED_COLUMN_NAMES.has(columns[property].columnNameFor(property))) {
2388
+ fail(`column "${property}" collides with a reserved system column`);
2389
+ }
2390
+ }
2391
+ for (const property of Object.keys(columns)) {
2392
+ if (columns[property].columnNameFor(property).length > MAX_TRACKED_COLUMN_NAME) {
2393
+ fail(`column "${property}" is too long for a resolved table (max ${MAX_TRACKED_COLUMN_NAME} characters, room for cell parts)`);
2394
+ }
2395
+ }
2396
+ const pkEntries = Object.entries(columns).filter(([, builder]) => builder.isPrimaryKey);
2397
+ if (pkEntries.length !== 1) fail("declare exactly one .primaryKey() column");
2398
+ const [pk, pkBuilder] = pkEntries[0];
2399
+ const dataColumns = Object.keys(columns).filter((property) => property !== pk);
2400
+ if (dataColumns.length === 0) fail("declare at least one non-key column");
2401
+ const sourceEntries = Object.entries(config.sources);
2402
+ if (sourceEntries.filter(([, source]) => source.kind === "manual").length !== 1) fail("declare exactly one manualSource()");
2403
+ const plans = sourceEntries.map(
2404
+ ([sourceName, input]) => planSource({ viewName: name2, sourceName, input, columns, pk, pkBuilder, dataColumns, fail })
2405
+ );
2406
+ const rules = planRules({ config, plans, dataColumns, fail });
2407
+ const deletedBy = [...config.rows?.deletedBy ?? []];
2408
+ for (const source of deletedBy) {
2409
+ if (plans.find((plan) => plan.name === source)?.kind !== "manual") fail(`rows.deletedBy "${source}" must be the manual source`);
2410
+ }
2411
+ const partType = { source: t.text, at: t.timestamptz, by: t.text, conflicts: t.jsonb };
2412
+ const viewColumns = {};
2413
+ for (const [property, builder] of Object.entries(columns)) {
2414
+ viewColumns[property] = cloneColumn({ builder, property, notNull: property === pk, primaryKey: property === pk });
2415
+ if (property === pk) continue;
2416
+ const columnName = builder.columnNameFor(property);
2417
+ for (const part of RESOLVED_VIEW_CELL_PARTS) {
2418
+ viewColumns[cellColumnName(property, part)] = partType[part](cellColumnName(columnName, part)).cellPart(property, part);
2419
+ }
2420
+ }
2421
+ for (const [property, make] of Object.entries(SYSTEM_VIEW_COLUMNS)) viewColumns[property] = make();
2422
+ const { read, ...writePolicies } = config.policies;
2423
+ const resolved = {
2424
+ internalTables: plans.filter((plan) => plan.kind !== "etl").map((plan) => plan.table),
2425
+ sources: plans,
2426
+ rules,
2427
+ deletedBy,
2428
+ policies: writePolicies
2429
+ };
2430
+ const queryPlan = { columns, pk, plans, rules, deletedBy };
2431
+ const typedColumns = viewColumns;
2432
+ const surface = {
2433
+ ...config.description !== void 0 ? { description: config.description } : {},
2434
+ ...config.links !== void 0 ? { links: config.links } : {},
2435
+ policies: { read }
2436
+ };
2437
+ if (!config.materialize) {
2438
+ return defineView(name2, typedColumns, { ...surface, query: (context) => buildResolvedQuery({ context, plan: queryPlan }), resolved });
2439
+ }
2440
+ const materialize = config.materialize === true ? {} : config.materialize;
2441
+ const computeName = `${name2}__compute`;
2442
+ const tableName = `${name2}__materialized`;
2443
+ const compute = defineView(computeName, typedColumns, {
2444
+ policies: { read },
2445
+ query: (context) => buildResolvedQuery({ context, plan: queryPlan }),
2446
+ internal: true
2447
+ });
2448
+ const table = defineTable(tableName, viewColumns, {
2449
+ description: `Materialized rows of resolved table "${name2}". System-maintained from "${computeName}".`,
2450
+ internal: true,
2451
+ materializes: name2,
2452
+ ...materialize.indexes !== void 0 ? { indexes: materialize.indexes } : {}
2453
+ });
2454
+ return defineView(name2, typedColumns, {
2455
+ ...surface,
2456
+ query: (context) => {
2457
+ const handle = context.relation(table);
2458
+ const selected = Object.fromEntries(Object.entries(viewColumns).map(([property, builder]) => [
2459
+ property,
2460
+ drizzleOrm.sql`${handle[property]}`.as(builder.columnNameFor(property))
2461
+ ]));
2462
+ return context.qb.select(selected).from(context.relation(table));
2463
+ },
2464
+ resolved: {
2465
+ ...resolved,
2466
+ internalTables: [...resolved.internalTables, table],
2467
+ internalViews: [compute],
2468
+ materialized: { table: tableName, compute: computeName }
2469
+ }
2470
+ });
2471
+ }
2472
+ function planRules({ config, plans, dataColumns, fail }) {
2473
+ const kindOrder = { manual: 0, prefetch: 1, etl: 2 };
2474
+ const fallback = config.default ?? priority([...plans].sort((a, b) => kindOrder[a.kind] - kindOrder[b.kind]).map((plan) => plan.name));
2475
+ for (const column of Object.keys(config.resolve ?? {})) {
2476
+ if (!dataColumns.includes(column)) fail(`resolve names unknown column "${column}"`);
2477
+ }
2478
+ return dataColumns.map((column) => {
2479
+ const explicit = config.resolve?.[column];
2480
+ const comparator = explicit ?? fallback;
2481
+ const candidates = explicit ? [...explicit.candidates] : comparator.candidates.filter((name2) => plans.find((plan) => plan.name === name2)?.columns.includes(column));
2482
+ for (const candidate of candidates) {
2483
+ const plan = plans.find((entry) => entry.name === candidate);
2484
+ if (!plan) fail(`column "${column}" names unknown source "${candidate}"`);
2485
+ else if (!plan.columns.includes(column)) fail(`column "${column}" candidate "${candidate}" does not supply it`);
2486
+ }
2487
+ if (candidates.length === 0) fail(`column "${column}" has no candidate source`);
2488
+ if (comparator.kind === "custom") assertSafeOrderBy({ orderBy: comparator.orderBy ?? "", column, fail });
2489
+ return {
2490
+ column,
2491
+ comparator: comparator.kind,
2492
+ candidates,
2493
+ ...comparator.kind === "custom" && comparator.orderBy !== void 0 ? { orderBy: comparator.orderBy } : {}
2494
+ };
2495
+ });
2496
+ }
2497
+ var ORDER_BY_KEYWORDS = /* @__PURE__ */ new Set([
2498
+ "value",
2499
+ "updated_at",
2500
+ "source",
2501
+ "actor",
2502
+ "ord",
2503
+ "asc",
2504
+ "desc",
2505
+ "nulls",
2506
+ "first",
2507
+ "last",
2508
+ "case",
2509
+ "when",
2510
+ "then",
2511
+ "else",
2512
+ "end",
2513
+ "and",
2514
+ "or",
2515
+ "not",
2516
+ "is",
2517
+ "null",
2518
+ "true",
2519
+ "false",
2520
+ "in",
2521
+ "distinct",
2522
+ "from",
2523
+ "coalesce",
2524
+ "nullif",
2525
+ "greatest",
2526
+ "least",
2527
+ "text",
2528
+ "numeric",
2529
+ "integer",
2530
+ "bigint",
2531
+ "boolean",
2532
+ "timestamptz",
2533
+ "date",
2534
+ "jsonb",
2535
+ "interval"
2536
+ ]);
2537
+ var ALLOWED_FUNCTIONS = new Set(VIEW_QUERY_FUNCTION_ALLOWLIST);
2538
+ var ORDER_BY_TOKEN = /\s+|'(?:[^']|'')*'|\d+(?:\.\d+)?|[a-z_][a-z0-9_]*|::|<=|>=|<>|!=|[-+*/%<>=(),|]/giy;
2539
+ function assertSafeOrderBy({ orderBy, column, fail }) {
2540
+ ORDER_BY_TOKEN.lastIndex = 0;
2541
+ let consumed = 0;
2542
+ for (let match = ORDER_BY_TOKEN.exec(orderBy); match; match = ORDER_BY_TOKEN.exec(orderBy)) {
2543
+ consumed = ORDER_BY_TOKEN.lastIndex;
2544
+ const token = match[0].toLowerCase();
2545
+ if (/^[a-z_]/.test(token) && !ORDER_BY_KEYWORDS.has(token) && !ALLOWED_FUNCTIONS.has(token)) {
2546
+ fail(`column "${column}" custom orderBy uses "${match[0]}" \u2014 only value, updated_at, source, actor, ord, SQL keywords and allowlisted functions`);
2547
+ }
2548
+ }
2549
+ if (consumed !== orderBy.length || orderBy.trim() === "") {
2550
+ fail(`column "${column}" custom orderBy contains an unsupported character near "${orderBy.slice(consumed, consumed + 10)}"`);
2551
+ }
2552
+ }
1278
2553
  function stableJson(value) {
1279
2554
  if (value === null || typeof value !== "object") return JSON.stringify(value);
1280
2555
  if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
@@ -1282,6 +2557,72 @@ function stableJson(value) {
1282
2557
  return `{${entries.join(",")}}`;
1283
2558
  }
1284
2559
 
2560
+ // src/canonical/composition.ts
2561
+ function localContract(definition) {
2562
+ return normalizeCanonical(localDefinitionSchema.parse(definition));
2563
+ }
2564
+ function flattenNamespaceImports(inputs) {
2565
+ const definitions = /* @__PURE__ */ new Map();
2566
+ const visit = (value, depth) => {
2567
+ if (depth > 16 || definitions.size > 32) throw new OrmDefinitionError("namespace import graph is too large");
2568
+ const definition = localContract(localDefinitionSchema.parse(value));
2569
+ const previous = definitions.get(definition.namespace);
2570
+ if (previous) {
2571
+ if (stableJson(previous) !== stableJson(definition)) throw new OrmDefinitionError(`conflicting contracts for namespace '${definition.namespace}'`);
2572
+ }
2573
+ definitions.set(definition.namespace, definition);
2574
+ if (typeof value === "object" && value !== null && "imports" in value && Array.isArray(value.imports)) {
2575
+ for (const dependency of value.imports) visit(dependency, depth + 1);
2576
+ }
2577
+ };
2578
+ for (const input of inputs) visit(input, 0);
2579
+ return [...definitions.values()].sort((a, b) => a.namespace.localeCompare(b.namespace));
2580
+ }
2581
+ function composeNamespace(definition) {
2582
+ const all = [localContract(definition), ...definition.imports ?? []];
2583
+ const owners = /* @__PURE__ */ new Map();
2584
+ const namespaces = /* @__PURE__ */ new Set();
2585
+ const enums = /* @__PURE__ */ new Map();
2586
+ for (const owner of all) {
2587
+ if (namespaces.has(owner.namespace)) throw new OrmDefinitionError(`duplicate imported namespace '${owner.namespace}'`);
2588
+ namespaces.add(owner.namespace);
2589
+ for (const value of owner.enums) {
2590
+ const previous = enums.get(value.name);
2591
+ if (previous && stableJson(previous) !== stableJson(value)) {
2592
+ throw new OrmDefinitionError(`conflicting imported enum '${value.name}'`);
2593
+ }
2594
+ enums.set(value.name, value);
2595
+ }
2596
+ for (const relation of canonicalRelations(owner)) {
2597
+ if (owners.has(relation.table)) throw new OrmDefinitionError(`ambiguous imported relation '${relation.table}'`);
2598
+ owners.set(relation.table, owner);
2599
+ }
2600
+ }
2601
+ for (const owner of definition.imports?.length ? all : []) {
2602
+ for (const relation of canonicalRelations(owner)) {
2603
+ for (const link of relation.links) {
2604
+ if (link.kind === "toPlatformUser") continue;
2605
+ const target = owners.get(link.target ?? "");
2606
+ if (!target || target.namespace !== (link.namespace ?? owner.namespace)) {
2607
+ throw new OrmDefinitionError(`link '${owner.namespace}/${relation.table}.${link.name}' has no matching imported target`);
2608
+ }
2609
+ if (target.namespace !== owner.namespace && !target.exports?.[link.target ?? ""]) {
2610
+ throw new OrmDefinitionError(`target '${target.namespace}/${link.target}' is not exported`);
2611
+ }
2612
+ }
2613
+ }
2614
+ }
2615
+ return {
2616
+ owners,
2617
+ definition: {
2618
+ ...localContract(definition),
2619
+ tables: all.flatMap((item) => item.tables),
2620
+ views: all.flatMap((item) => item.views ?? []),
2621
+ enums: [...enums.values()]
2622
+ }
2623
+ };
2624
+ }
2625
+
1285
2626
  // src/authoring/namespace.ts
1286
2627
  var NAMESPACE_CONFIG_BRAND = "seq-namespace-config";
1287
2628
  function isSeqNamespaceConfig(value) {
@@ -1297,14 +2638,27 @@ function defineNamespace(input) {
1297
2638
  namespace: input.namespace,
1298
2639
  description: input.description,
1299
2640
  capabilities: input.capabilities ?? {},
2641
+ imports: input.imports,
2642
+ exports: input.exports,
1300
2643
  file
1301
2644
  };
1302
2645
  registerNamespaceConfig(config);
1303
2646
  return config;
1304
2647
  }
1305
2648
  function serializeNamespace(input) {
1306
- const { config, tables } = input;
1307
- const views = input.views ?? [];
2649
+ const { config } = input;
2650
+ const views = [...input.views ?? []];
2651
+ const tables = [...input.tables];
2652
+ for (const view of views) {
2653
+ for (const internal of view.config.resolved?.internalTables ?? []) {
2654
+ if (!tables.includes(internal)) tables.push(internal);
2655
+ }
2656
+ }
2657
+ for (const view of [...views]) {
2658
+ for (const internal of view.config.resolved?.internalViews ?? []) {
2659
+ if (!views.includes(internal)) views.push(internal);
2660
+ }
2661
+ }
1308
2662
  const enums = input.enums ?? [];
1309
2663
  const tablesByName = /* @__PURE__ */ new Map();
1310
2664
  for (const table of tables) {
@@ -1330,15 +2684,15 @@ function serializeNamespace(input) {
1330
2684
  }
1331
2685
  const relationsByName = /* @__PURE__ */ new Map();
1332
2686
  for (const relation of [...tables, ...views]) {
1333
- const name = relationName(relation);
1334
- const existing = relationsByName.get(name);
2687
+ const name2 = relationName(relation);
2688
+ const existing = relationsByName.get(name2);
1335
2689
  if (existing) {
1336
2690
  throw new OrmDefinitionError(
1337
- `duplicate relation "${name}" (defined as ${relationKind(existing)} in ${describeFile(existing)} and ${relationKind(relation)} in ${describeFile(relation)})`,
2691
+ `duplicate relation "${name2}" (defined as ${relationKind(existing)} in ${describeFile(existing)} and ${relationKind(relation)} in ${describeFile(relation)})`,
1338
2692
  relation.file
1339
2693
  );
1340
2694
  }
1341
- relationsByName.set(name, relation);
2695
+ relationsByName.set(name2, relation);
1342
2696
  }
1343
2697
  const enumsByName = /* @__PURE__ */ new Map();
1344
2698
  for (const ref of enums) {
@@ -1379,6 +2733,8 @@ function serializeNamespace(input) {
1379
2733
  };
1380
2734
  if (config.description !== void 0) candidate.description = config.description;
1381
2735
  if (canonicalViews.length > 0) candidate.views = canonicalViews;
2736
+ if (config.imports?.length) candidate.imports = flattenNamespaceImports(config.imports);
2737
+ if (config.exports) candidate.exports = config.exports;
1382
2738
  let parsed;
1383
2739
  try {
1384
2740
  parsed = canonicalDefinitionSchema.parse(candidate);
@@ -1388,7 +2744,9 @@ function serializeNamespace(input) {
1388
2744
  }
1389
2745
  throw error;
1390
2746
  }
1391
- return normalizeCanonical(parsed);
2747
+ const normalized = normalizeCanonical(parsed);
2748
+ if (normalized.imports?.length) composeNamespace(normalized);
2749
+ return normalized;
1392
2750
  }
1393
2751
  function computeCanonicalHash(definition) {
1394
2752
  return crypto.createHash("sha256").update(HASH_DOMAIN).update(stableJson(definition)).digest("hex");
@@ -1402,10 +2760,14 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1402
2760
  policies: {},
1403
2761
  actions: []
1404
2762
  };
2763
+ if (table.config.displayName !== void 0) candidate.displayName = table.config.displayName;
1405
2764
  if (table.config.description !== void 0) candidate.description = table.config.description;
1406
2765
  if (table.config.renamedFrom !== void 0) candidate.renamedFrom = table.config.renamedFrom;
1407
2766
  if (table.source !== void 0) candidate.source = table.source;
1408
2767
  if (table.source !== void 0) {
2768
+ if (table.config.checks?.length) {
2769
+ throw new OrmDefinitionError(`synced table "${table.tableName}" is system-owned; checks are not allowed`, table.file);
2770
+ }
1409
2771
  if (table.config.indexes !== void 0 && table.config.indexes.length > 0) {
1410
2772
  throw new OrmDefinitionError(
1411
2773
  `synced table "${table.tableName}" is system-owned; indexes are not allowed`,
@@ -1443,6 +2805,11 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1443
2805
  if (builder.isPrimaryKey) columnLevelPk.push(column.columnName);
1444
2806
  candidate.columns?.push(column);
1445
2807
  }
2808
+ if (table.config.internal === true) candidate.internal = true;
2809
+ if (table.config.materializes !== void 0) candidate.materializes = table.config.materializes;
2810
+ if (table.config.cellMetadata === true && table.source !== void 0) {
2811
+ throw new OrmDefinitionError(`synced table "${table.tableName}" cannot track cell metadata \u2014 its DDL is owned by the sync`, table.file);
2812
+ }
1446
2813
  if (table.config.primaryKey !== void 0) {
1447
2814
  if (columnLevelPk.length > 0) {
1448
2815
  throw new OrmDefinitionError(
@@ -1456,9 +2823,30 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1456
2823
  } else if (columnLevelPk.length > 0) {
1457
2824
  candidate.primaryKey = columnLevelPk;
1458
2825
  }
2826
+ if (table.config.cellMetadata === true) {
2827
+ candidate.cellMetadata = true;
2828
+ const parts = table.config.cellRevisions === true ? ["at", "by", "rev"] : ["at", "by"];
2829
+ const tracked = trackedCellColumns({ columns: candidate.columns ?? [], primaryKey: candidate.primaryKey });
2830
+ for (const column of tracked) {
2831
+ for (const part of parts) {
2832
+ candidate.columns?.push({
2833
+ name: cellColumnName(column.name, part),
2834
+ columnName: cellColumnName(column.columnName, part),
2835
+ type: CELL_PART_TYPES[part],
2836
+ nullable: true,
2837
+ unique: false,
2838
+ deprecated: false,
2839
+ cell: { of: column.name, part }
2840
+ });
2841
+ }
2842
+ }
2843
+ }
1459
2844
  if (table.config.foreignKeys?.length) {
1460
2845
  if (table.source) throw new OrmDefinitionError(`synced table "${table.tableName}" cannot enforce foreign keys`, table.file);
1461
2846
  candidate.foreignKeys = table.config.foreignKeys.map((fk) => {
2847
+ if (typeof fk.target === "object" && fk.target.kind === "seq-external-object") {
2848
+ throw new OrmDefinitionError("physical foreign keys require a local target; use a cross-namespace ORM link", table.file);
2849
+ }
1462
2850
  const target = resolveRelationRef({ ref: fk.target, fail: (message) => {
1463
2851
  throw new OrmDefinitionError(message, table.file);
1464
2852
  } });
@@ -1468,10 +2856,15 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1468
2856
  columns: fk.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: `foreign key "${fk.name}" column` })),
1469
2857
  target: target.tableName,
1470
2858
  references: fk.references.map((ref) => resolveColumnRef({ relation: target, ref, role: `foreign key "${fk.name}" reference` })),
1471
- ...fk.deferrable ? { deferrable: fk.deferrable } : {}
2859
+ ...fk.deferrable ? { deferrable: fk.deferrable } : {},
2860
+ ...fk.onDelete ? { onDelete: fk.onDelete } : {},
2861
+ ...fk.onUpdate ? { onUpdate: fk.onUpdate } : {}
1472
2862
  };
1473
2863
  });
1474
2864
  }
2865
+ if (table.config.checks?.length) {
2866
+ candidate.checks = table.config.checks.map((check) => ({ name: check.name, expression: check.expression }));
2867
+ }
1475
2868
  for (const index of table.config.indexes ?? []) {
1476
2869
  const entry = {
1477
2870
  columns: index.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
@@ -1567,7 +2960,33 @@ function buildCanonicalView({
1567
2960
  columns,
1568
2961
  ...primaryKey !== void 0 ? { primaryKey } : {},
1569
2962
  links,
1570
- policies: { read: toCanonicalPolicy(view.config.policies.read) }
2963
+ policies: { read: toCanonicalPolicy(view.config.policies.read) },
2964
+ ...view.config.resolved !== void 0 ? { resolved: buildCanonicalResolved(view.config.resolved) } : {},
2965
+ ...view.config.internal === true ? { internal: true } : {}
2966
+ };
2967
+ }
2968
+ function buildCanonicalResolved(resolved) {
2969
+ const policies = {};
2970
+ if (resolved.policies.write !== void 0) policies.write = toCanonicalPolicy(resolved.policies.write);
2971
+ if (resolved.policies.insert !== void 0) policies.insert = toCanonicalPolicy(resolved.policies.insert);
2972
+ if (resolved.policies.update !== void 0) policies.update = toCanonicalPolicy(resolved.policies.update);
2973
+ return {
2974
+ sources: resolved.sources.map((source) => ({
2975
+ name: source.name,
2976
+ kind: source.kind,
2977
+ table: source.table.tableName,
2978
+ columns: [...source.columns],
2979
+ ...source.updatedAt !== void 0 ? { updatedAt: source.updatedAt } : {},
2980
+ ...source.key !== void 0 ? { key: source.key } : {},
2981
+ ...source.fetch !== void 0 ? { fetch: source.fetch } : {},
2982
+ ...source.function !== void 0 ? { function: { ...source.function } } : {},
2983
+ ...source.prefetchDefaults !== void 0 ? { prefetchDefaults: { ...source.prefetchDefaults } } : {},
2984
+ ...source.writeback !== void 0 ? { writeback: { ...source.writeback, columns: [...source.writeback.columns] } } : {}
2985
+ })),
2986
+ resolve: resolved.rules.map((rule) => ({ ...rule, candidates: [...rule.candidates] })),
2987
+ deletedBy: [...resolved.deletedBy],
2988
+ ...resolved.materialized ? { materialized: { ...resolved.materialized } } : {},
2989
+ policies
1571
2990
  };
1572
2991
  }
1573
2992
  function buildCanonicalLink({
@@ -1584,6 +3003,9 @@ function buildCanonicalLink({
1584
3003
  if (link.enforce && link.linkKind !== "toOne") {
1585
3004
  return fail("sets enforce: true, which is only supported on toOne links");
1586
3005
  }
3006
+ if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && !link.enforce) {
3007
+ return fail("sets a referential action without enforce: true");
3008
+ }
1587
3009
  if (link.linkKind === "toPlatformUser") {
1588
3010
  if (link.target !== void 0) return fail("is toPlatformUser and must not declare a target table");
1589
3011
  return {
@@ -1595,6 +3017,19 @@ function buildCanonicalLink({
1595
3017
  ...link.description !== void 0 ? { description: link.description } : {}
1596
3018
  };
1597
3019
  }
3020
+ if (typeof link.target === "object" && link.target?.kind === "seq-external-object") {
3021
+ if (link.enforce) return fail("cannot enforce a cross-namespace physical foreign key; use an ORM link without enforce");
3022
+ return {
3023
+ name: linkName,
3024
+ kind: link.linkKind,
3025
+ namespace: link.target.namespace,
3026
+ target: link.target.table,
3027
+ on: link.linkKind === "toOne" ? resolveColumnRef({ relation, ref: link.on, role: "link on" }) : link.on,
3028
+ ...link.references !== void 0 ? { references: link.linkKind === "toMany" ? resolveColumnRef({ relation, ref: link.references, role: "link references" }) : link.references } : {},
3029
+ enforce: false,
3030
+ ...link.description !== void 0 ? { description: link.description } : {}
3031
+ };
3032
+ }
1598
3033
  const target = resolveRelationRef({ ref: link.target, fail });
1599
3034
  const targetName = relationName(target);
1600
3035
  if (!relationsByName.has(targetName)) {
@@ -1612,6 +3047,8 @@ function buildCanonicalLink({
1612
3047
  on: resolveColumnRef({ relation: owningSide, ref: link.on, role: `link "${linkName}" on` }),
1613
3048
  ...link.references !== void 0 ? { references: resolveColumnRef({ relation: referencedSide, ref: link.references, role: `link "${linkName}" references` }) } : {},
1614
3049
  enforce: link.enforce,
3050
+ ...link.onDelete !== void 0 ? { onDelete: link.onDelete } : {},
3051
+ ...link.onUpdate !== void 0 ? { onUpdate: link.onUpdate } : {},
1615
3052
  ...link.description !== void 0 ? { description: link.description } : {}
1616
3053
  };
1617
3054
  }
@@ -1649,11 +3086,11 @@ function assertAcyclicViewDependencies(views) {
1649
3086
  views.map((view) => [view.view, new Set(view.dependencies.filter((dependency) => viewNames.has(dependency)))])
1650
3087
  );
1651
3088
  while (remaining.size > 0) {
1652
- const ready = [...remaining.entries()].filter(([, dependencies]) => [...dependencies].every((dependency) => !remaining.has(dependency))).map(([name]) => name);
3089
+ const ready = [...remaining.entries()].filter(([, dependencies]) => [...dependencies].every((dependency) => !remaining.has(dependency))).map(([name2]) => name2);
1653
3090
  if (ready.length === 0) {
1654
3091
  throw new OrmDefinitionError(`view dependencies form a cycle among: ${[...remaining.keys()].sort().join(", ")}`);
1655
3092
  }
1656
- for (const name of ready) remaining.delete(name);
3093
+ for (const name2 of ready) remaining.delete(name2);
1657
3094
  }
1658
3095
  }
1659
3096
  function toCanonicalPolicy(policy) {
@@ -1699,8 +3136,8 @@ function syncedContractSurface({
1699
3136
  namespace,
1700
3137
  table,
1701
3138
  columns: Object.fromEntries(
1702
- Object.entries(columns).map(([name, column]) => [
1703
- name,
3139
+ Object.entries(columns).map(([name2, column]) => [
3140
+ name2,
1704
3141
  { type: column.type, nullable: column.nullable, key: column.key }
1705
3142
  ])
1706
3143
  ),
@@ -1721,7 +3158,7 @@ function extractOrmSyncedTableContract({
1721
3158
  }
1722
3159
  if (table.source === void 0) {
1723
3160
  throw new OrmDefinitionError(
1724
- `table "${table.table}" is not a synced table \u2014 extractOrmSyncedTableContract requires source: gold.*`
3161
+ `table "${table.table}" is not a synced table \u2014 extractOrmSyncedTableContract requires a qualified source`
1725
3162
  );
1726
3163
  }
1727
3164
  if (table.columns.length === 0) {
@@ -1868,6 +3305,53 @@ function purgeModuleCache(root) {
1868
3305
  if (key.startsWith(prefix)) delete require2.cache[key];
1869
3306
  }
1870
3307
  }
3308
+ var sqlScalarSchema = zod.z.enum(["text", "boolean", "integer", "bigint", "numeric", "double", "uuid", "date", "timestamptz"]);
3309
+ var name = zod.z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).max(63).refine((value) => !["__proto__", "prototype", "constructor"].includes(value), "reserved name");
3310
+ var base = zod.z.object({
3311
+ name,
3312
+ namespace: name,
3313
+ language: zod.z.literal("sql"),
3314
+ kind: zod.z.literal("query"),
3315
+ requires: zod.z.object({ capability: zod.z.string().min(1).max(128) }).strict(),
3316
+ ontology: zod.z.object({ read: zod.z.array(zod.z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*\/[A-Za-z_][A-Za-z0-9_]*$/)).min(1).max(256) }).strict(),
3317
+ maxRows: zod.z.number().int().min(1).max(1e4).default(1e3)
3318
+ });
3319
+ var fixed = base.extend({
3320
+ parameters: zod.z.record(name, zod.z.object({ type: sqlScalarSchema, required: zod.z.boolean().default(true) }).strict()).default({}),
3321
+ sql: zod.z.string().min(1).max(65536)
3322
+ }).strict();
3323
+ var runtime = base.extend({ mode: zod.z.literal("runtime") }).strict();
3324
+ var sqlOperationDefinitionSchema = zod.z.union([fixed, runtime]);
3325
+ function defineOperation(input) {
3326
+ return sqlOperationDefinitionSchema.parse(input);
3327
+ }
3328
+
3329
+ // src/authoring/catalog.ts
3330
+ var SEGMENT = /^[^./\\\s][^/\\]*[^./\\\s]$|^[^./\\\s]$/;
3331
+ function validateNamespaceCatalog(catalog, file) {
3332
+ const paths = /* @__PURE__ */ new Set([""]);
3333
+ function visit(folders, parent) {
3334
+ for (const [name2, children] of Object.entries(folders)) {
3335
+ if (!SEGMENT.test(name2) || ["__proto__", "constructor", "prototype"].includes(name2)) {
3336
+ throw new OrmDefinitionError(`invalid catalog folder name "${name2}"`, file);
3337
+ }
3338
+ const path = parent ? `${parent}/${name2}` : name2;
3339
+ paths.add(path);
3340
+ if (children !== null) {
3341
+ if (typeof children !== "object" || Array.isArray(children)) {
3342
+ throw new OrmDefinitionError(`catalog folder "${path}" must contain folders or null`, file);
3343
+ }
3344
+ visit(children, path);
3345
+ }
3346
+ }
3347
+ }
3348
+ visit(catalog.folders, "");
3349
+ for (const [object, path] of Object.entries(catalog.objects)) {
3350
+ if (!object || !paths.has(path)) {
3351
+ throw new OrmDefinitionError(`catalog object "${object}" references missing folder "${path}"`, file);
3352
+ }
3353
+ }
3354
+ }
1871
3355
 
1872
3356
  exports.COLUMN_BRAND = COLUMN_BRAND;
1873
3357
  exports.COLUMN_DEFAULT_FNS = COLUMN_DEFAULT_FNS;
@@ -1885,23 +3369,36 @@ exports.activeAuthoringContext = activeAuthoringContext;
1885
3369
  exports.beginAuthoringContext = beginAuthoringContext;
1886
3370
  exports.computeCanonicalHash = computeCanonicalHash;
1887
3371
  exports.computeSyncedContractHash = computeSyncedContractHash;
3372
+ exports.custom = custom;
1888
3373
  exports.defineEnum = defineEnum;
1889
3374
  exports.defineNamespace = defineNamespace;
3375
+ exports.defineOperation = defineOperation;
3376
+ exports.defineResolvedTable = defineResolvedTable;
1890
3377
  exports.defineSyncedTable = defineSyncedTable;
1891
3378
  exports.defineTable = defineTable;
1892
3379
  exports.defineView = defineView;
1893
3380
  exports.deriveColumnName = deriveColumnName;
1894
3381
  exports.endAuthoringContext = endAuthoringContext;
3382
+ exports.etlSource = etlSource;
1895
3383
  exports.extractOrmSyncedTableContract = extractOrmSyncedTableContract;
3384
+ exports.highest = highest;
1896
3385
  exports.isColumnBuilder = isColumnBuilder;
1897
3386
  exports.isEnumRef = isEnumRef;
1898
3387
  exports.isSeqLink = isSeqLink;
1899
3388
  exports.isSeqNamespaceConfig = isSeqNamespaceConfig;
1900
3389
  exports.isSeqTable = isSeqTable;
1901
3390
  exports.isSeqView = isSeqView;
3391
+ exports.latest = latest;
1902
3392
  exports.loadNamespaceFromDir = loadNamespaceFromDir;
3393
+ exports.lowest = lowest;
3394
+ exports.managedFn = managedFn;
3395
+ exports.manualSource = manualSource;
3396
+ exports.nonNullFirst = nonNullFirst;
3397
+ exports.objectRef = objectRef;
1903
3398
  exports.overlayWinsUnion = overlayWinsUnion;
1904
3399
  exports.overrideRowMutation = overrideRowMutation;
3400
+ exports.prefetchSource = prefetchSource;
3401
+ exports.priority = priority;
1905
3402
  exports.revertOverlayMutation = revertOverlayMutation;
1906
3403
  exports.serializeNamespace = serializeNamespace;
1907
3404
  exports.syncedContractSurface = syncedContractSurface;
@@ -1909,5 +3406,6 @@ exports.t = t;
1909
3406
  exports.toMany = toMany;
1910
3407
  exports.toOne = toOne;
1911
3408
  exports.toPlatformUser = toPlatformUser;
3409
+ exports.validateNamespaceCatalog = validateNamespaceCatalog;
1912
3410
  //# sourceMappingURL=authoring.cjs.map
1913
3411
  //# sourceMappingURL=authoring.cjs.map