@sequenceholdings/orm 0.1.2 → 0.1.3

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 (65) hide show
  1. package/README.md +11 -1
  2. package/dist/authoring.cjs +1066 -59
  3. package/dist/authoring.cjs.map +1 -1
  4. package/dist/authoring.d.cts +289 -24
  5. package/dist/authoring.d.ts +289 -24
  6. package/dist/authoring.js +1056 -60
  7. package/dist/authoring.js.map +1 -1
  8. package/dist/canonical.cjs +415 -9
  9. package/dist/canonical.cjs.map +1 -1
  10. package/dist/canonical.d.cts +235 -1
  11. package/dist/canonical.d.ts +235 -1
  12. package/dist/canonical.js +411 -10
  13. package/dist/canonical.js.map +1 -1
  14. package/dist/cli.cjs +1764 -225
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.js +1765 -226
  17. package/dist/cli.js.map +1 -1
  18. package/dist/client.cjs.map +1 -1
  19. package/dist/client.d.cts +1 -1
  20. package/dist/client.d.ts +1 -1
  21. package/dist/client.js.map +1 -1
  22. package/dist/compiler.cjs +506 -54
  23. package/dist/compiler.cjs.map +1 -1
  24. package/dist/compiler.d.cts +39 -231
  25. package/dist/compiler.d.ts +39 -231
  26. package/dist/compiler.js +504 -55
  27. package/dist/compiler.js.map +1 -1
  28. package/dist/drizzle.cjs +14 -0
  29. package/dist/drizzle.cjs.map +1 -0
  30. package/dist/drizzle.d.cts +1 -0
  31. package/dist/drizzle.d.ts +1 -0
  32. package/dist/drizzle.js +3 -0
  33. package/dist/drizzle.js.map +1 -0
  34. package/dist/dynamic-input.cjs +391 -0
  35. package/dist/dynamic-input.cjs.map +1 -0
  36. package/dist/dynamic-input.d.cts +111 -0
  37. package/dist/dynamic-input.d.ts +111 -0
  38. package/dist/dynamic-input.js +380 -0
  39. package/dist/dynamic-input.js.map +1 -0
  40. package/dist/index.cjs +2223 -219
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/index.d.cts +125 -9
  43. package/dist/index.d.ts +125 -9
  44. package/dist/index.js +2199 -220
  45. package/dist/index.js.map +1 -1
  46. package/dist/ir.cjs +249 -14
  47. package/dist/ir.cjs.map +1 -1
  48. package/dist/ir.d.cts +20 -80
  49. package/dist/ir.d.ts +20 -80
  50. package/dist/ir.js +249 -14
  51. package/dist/ir.js.map +1 -1
  52. package/dist/predicate-v2.cjs +1093 -0
  53. package/dist/predicate-v2.cjs.map +1 -0
  54. package/dist/predicate-v2.d.cts +139 -0
  55. package/dist/predicate-v2.d.ts +139 -0
  56. package/dist/predicate-v2.js +1083 -0
  57. package/dist/predicate-v2.js.map +1 -0
  58. package/dist/sdl.cjs +344 -55
  59. package/dist/sdl.cjs.map +1 -1
  60. package/dist/sdl.d.cts +1 -3
  61. package/dist/sdl.d.ts +1 -3
  62. package/dist/sdl.js +344 -55
  63. package/dist/sdl.js.map +1 -1
  64. package/package.json +16 -1
  65. package/templates/orm-namespace/CLAUDE.md +6 -5
package/dist/index.cjs CHANGED
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ var drizzleOrm = require('drizzle-orm');
4
+ var pgCore = require('drizzle-orm/pg-core');
3
5
  var crypto = require('crypto');
4
6
  var zod = require('zod');
5
7
  var jiti = require('jiti');
@@ -7,8 +9,6 @@ var promises = require('fs/promises');
7
9
  var module$1 = require('module');
8
10
  var path = require('path');
9
11
  var api = require('drizzle-kit/api');
10
- var pgCore = require('drizzle-orm/pg-core');
11
- var drizzleOrm = require('drizzle-orm');
12
12
  var jsYaml = require('js-yaml');
13
13
  var customRolesManifest = require('@sequenceholdings/custom-roles-manifest');
14
14
 
@@ -71,8 +71,8 @@ var ColumnBuilder = class {
71
71
  this.state.defaultFn = void 0;
72
72
  return this;
73
73
  }
74
- description(text3) {
75
- this.state.description = text3;
74
+ description(text4) {
75
+ this.state.description = text4;
76
76
  return this;
77
77
  }
78
78
  /** Migration rename hint: the previous Postgres column name. */
@@ -188,7 +188,7 @@ function contextStack() {
188
188
  return created;
189
189
  }
190
190
  function beginAuthoringContext() {
191
- const context = { currentFile: void 0, tables: [], enums: [], configs: [] };
191
+ const context = { currentFile: void 0, tables: [], views: [], enums: [], configs: [] };
192
192
  contextStack().push(context);
193
193
  return context;
194
194
  }
@@ -212,6 +212,9 @@ function register(bucket, value) {
212
212
  function registerTable(value) {
213
213
  register("tables", value);
214
214
  }
215
+ function registerView(value) {
216
+ register("views", value);
217
+ }
215
218
  function registerEnum(value) {
216
219
  register("enums", value);
217
220
  }
@@ -299,20 +302,69 @@ function defineTable(name, columns, config = {}) {
299
302
  registerTable(table);
300
303
  return table;
301
304
  }
302
- function defineSyncedTable(name, config) {
305
+ function defineSyncedTable(name, columnsOrConfig, maybeConfig) {
303
306
  const file = activeAuthoringContext()?.currentFile;
304
307
  assertIdentifier({ value: name, what: "table name", file });
308
+ const isTyped = maybeConfig !== void 0;
309
+ const columns = isTyped ? columnsOrConfig : {};
310
+ const config = isTyped ? maybeConfig : columnsOrConfig;
305
311
  if (!SYNCED_SOURCE_PATTERN.test(config.source)) {
306
312
  throw new OrmDefinitionError(
307
313
  `synced table "${name}": source "${config.source}" must match ${SYNCED_SOURCE_PATTERN} (gold layer only)`,
308
314
  file
309
315
  );
310
316
  }
317
+ const entries = Object.entries(columns);
318
+ const seenColumnNames = /* @__PURE__ */ new Map();
319
+ const columnLevelPk = [];
320
+ for (const [propertyName, builder] of entries) {
321
+ if (!isColumnBuilder(builder)) {
322
+ throw new OrmDefinitionError(
323
+ `synced table "${name}": column "${propertyName}" is not a column builder \u2014 use the t.* factories (t.uuid(), t.text(), \u2026)`,
324
+ file
325
+ );
326
+ }
327
+ const columnName = builder.columnNameFor(propertyName);
328
+ assertIdentifier({
329
+ value: columnName,
330
+ what: `synced table "${name}": column "${propertyName}" column name`,
331
+ file
332
+ });
333
+ const existing = seenColumnNames.get(columnName);
334
+ if (existing !== void 0) {
335
+ throw new OrmDefinitionError(
336
+ `synced table "${name}": columns "${existing}" and "${propertyName}" both map to Postgres column "${columnName}"`,
337
+ file
338
+ );
339
+ }
340
+ seenColumnNames.set(columnName, propertyName);
341
+ if (builder.isPrimaryKey) columnLevelPk.push(propertyName);
342
+ }
343
+ if (entries.length > 0) {
344
+ const hasConfigPk = config.primaryKey !== void 0 && config.primaryKey.length > 0;
345
+ if (columnLevelPk.length === 0 && !hasConfigPk) {
346
+ throw new OrmDefinitionError(
347
+ `synced table "${name}" declares columns but no primary key \u2014 use .primaryKey() or config.primaryKey`,
348
+ file
349
+ );
350
+ }
351
+ if (columnLevelPk.length > 0 && hasConfigPk) {
352
+ throw new OrmDefinitionError(
353
+ `synced table "${name}": declare the primary key either with .primaryKey() on columns or with config.primaryKey \u2014 not both`,
354
+ file
355
+ );
356
+ }
357
+ }
358
+ const tableConfig = {};
359
+ if (config.description !== void 0) tableConfig.description = config.description;
360
+ if (config.primaryKey !== void 0) tableConfig.primaryKey = config.primaryKey;
361
+ if (config.links !== void 0) tableConfig.links = config.links;
362
+ if (config.policies !== void 0) tableConfig.policies = { read: config.policies.read };
311
363
  const table = {
312
364
  kind: TABLE_BRAND,
313
365
  tableName: name,
314
- columns: {},
315
- config: config.description !== void 0 ? { description: config.description } : {},
366
+ columns,
367
+ config: tableConfig,
316
368
  source: config.source,
317
369
  file
318
370
  };
@@ -342,6 +394,475 @@ function defineEnum(name, values) {
342
394
  registerEnum(ref);
343
395
  return ref;
344
396
  }
397
+ var VIEW_BRAND = "seq-view";
398
+ function isSeqView(value) {
399
+ return typeof value === "object" && value !== null && value.kind === VIEW_BRAND;
400
+ }
401
+ function defineView(name, columns, config) {
402
+ const file = activeAuthoringContext()?.currentFile;
403
+ if (!IDENTIFIER_PATTERN.test(name) || name.length > MAX_IDENTIFIER_LENGTH) {
404
+ throw new OrmDefinitionError(
405
+ `view name "${name}" must match ${IDENTIFIER_PATTERN} and be at most ${MAX_IDENTIFIER_LENGTH} characters`,
406
+ file
407
+ );
408
+ }
409
+ const entries = Object.entries(columns);
410
+ if (entries.length === 0) throw new OrmDefinitionError(`view "${name}" declares no columns`, file);
411
+ const seenColumnNames = /* @__PURE__ */ new Map();
412
+ for (const [propertyName, builder] of entries) {
413
+ if (!isColumnBuilder(builder)) {
414
+ throw new OrmDefinitionError(
415
+ `view "${name}": column "${propertyName}" is not a column builder \u2014 use the t.* factories`,
416
+ file
417
+ );
418
+ }
419
+ const columnName = builder.columnNameFor(propertyName);
420
+ if (!IDENTIFIER_PATTERN.test(columnName) || columnName.length > MAX_IDENTIFIER_LENGTH) {
421
+ throw new OrmDefinitionError(`view "${name}": column "${propertyName}" has invalid column name "${columnName}"`, file);
422
+ }
423
+ const existing = seenColumnNames.get(columnName);
424
+ if (existing !== void 0) {
425
+ throw new OrmDefinitionError(
426
+ `view "${name}": columns "${existing}" and "${propertyName}" both map to Postgres column "${columnName}"`,
427
+ file
428
+ );
429
+ }
430
+ seenColumnNames.set(columnName, propertyName);
431
+ }
432
+ const view = { kind: VIEW_BRAND, viewName: name, columns, config, file };
433
+ registerView(view);
434
+ return view;
435
+ }
436
+ var QUERY_COLUMN_FACTORIES = {
437
+ uuid: (name) => pgCore.uuid(name),
438
+ text: (name) => pgCore.text(name),
439
+ integer: (name) => pgCore.integer(name),
440
+ bigint: (name) => pgCore.bigint(name, { mode: "bigint" }),
441
+ numeric: (name) => pgCore.numeric(name),
442
+ double: (name) => pgCore.doublePrecision(name),
443
+ boolean: (name) => pgCore.boolean(name),
444
+ timestamptz: (name) => pgCore.timestamp(name, { withTimezone: true }),
445
+ date: (name) => pgCore.date(name),
446
+ jsonb: (name) => pgCore.jsonb(name)
447
+ };
448
+ function compileViewQueries(input) {
449
+ const schemaName = `orm_${input.namespace}`;
450
+ const schema = pgCore.pgSchema(schemaName);
451
+ const enumFactories = /* @__PURE__ */ new Map();
452
+ for (const entry of input.enums) {
453
+ const values = entry.values;
454
+ enumFactories.set(entry.name, schema.enum(entry.name, values));
455
+ }
456
+ const handles = /* @__PURE__ */ new Map();
457
+ const physicalToLogical = /* @__PURE__ */ new Map();
458
+ const tableByName = new Map(input.tables.map((table) => [table.table, table]));
459
+ for (const authored of input.authoredTables) {
460
+ const canonical = tableByName.get(authored.tableName);
461
+ if (!canonical) continue;
462
+ const physicalSchema = schemaName;
463
+ const physicalName = canonical.table;
464
+ const relationSchema = pgCore.pgSchema(physicalSchema);
465
+ const columns = buildQueryColumns(canonical.columns, enumFactories);
466
+ const handle = relationSchema.table(physicalName, columns);
467
+ handles.set(authored, handle);
468
+ physicalToLogical.set(`${physicalSchema}.${physicalName}`, canonical.table);
469
+ }
470
+ const viewByName = new Map(input.views.map((view) => [view.view, view]));
471
+ for (const authored of input.authoredViews) {
472
+ const canonical = viewByName.get(authored.viewName);
473
+ if (!canonical) continue;
474
+ const columns = buildQueryColumns(canonical.columns, enumFactories);
475
+ const handle = schema.view(canonical.view, columns).existing();
476
+ handles.set(authored, handle);
477
+ physicalToLogical.set(`${schemaName}.${canonical.view}`, canonical.view);
478
+ }
479
+ const dialect = new pgCore.PgDialect();
480
+ const compiled = /* @__PURE__ */ new Map();
481
+ for (const authored of input.authoredViews) {
482
+ const expectedFields = Object.keys(authored.columns);
483
+ const declaredDependencies = /* @__PURE__ */ new Set();
484
+ const relation = (ref) => {
485
+ let resolved;
486
+ try {
487
+ resolved = typeof ref === "function" ? ref() : ref;
488
+ } catch (error) {
489
+ throw new OrmDefinitionError(
490
+ `view "${authored.viewName}" relation thunk threw: ${error instanceof Error ? error.message : String(error)}`,
491
+ authored.file
492
+ );
493
+ }
494
+ if (!isSeqTable(resolved) && !isSeqView(resolved)) {
495
+ throw new OrmDefinitionError(
496
+ `view "${authored.viewName}" relation() expects a table or view defined by this ORM`,
497
+ authored.file
498
+ );
499
+ }
500
+ const handle = handles.get(resolved);
501
+ if (!handle) {
502
+ const name = isSeqTable(resolved) ? resolved.tableName : resolved.viewName;
503
+ throw new OrmDefinitionError(
504
+ `view "${authored.viewName}" references relation "${name}" which is not part of this namespace`,
505
+ authored.file
506
+ );
507
+ }
508
+ declaredDependencies.add(isSeqTable(resolved) ? resolved.tableName : resolved.viewName);
509
+ return handle;
510
+ };
511
+ let queryBuilder;
512
+ try {
513
+ queryBuilder = authored.config.query({ qb: new pgCore.QueryBuilder(), relation });
514
+ } catch (error) {
515
+ if (error instanceof OrmDefinitionError) throw error;
516
+ throw new OrmDefinitionError(
517
+ `view "${authored.viewName}" query callback failed: ${error instanceof Error ? error.message : String(error)}`,
518
+ authored.file
519
+ );
520
+ }
521
+ let config;
522
+ try {
523
+ config = pgCore.getViewConfig(schema.view(authored.viewName).as(queryBuilder));
524
+ } catch (error) {
525
+ throw new OrmDefinitionError(
526
+ `view "${authored.viewName}" query must return a Drizzle select builder: ${error instanceof Error ? error.message : String(error)}`,
527
+ authored.file
528
+ );
529
+ }
530
+ const selectedFields = Object.keys(config.selectedFields);
531
+ if (selectedFields.length !== expectedFields.length || selectedFields.some((field, index3) => field !== expectedFields[index3])) {
532
+ throw new OrmDefinitionError(
533
+ `view "${authored.viewName}" query selects [${selectedFields.join(", ")}], but declared columns are [${expectedFields.join(", ")}]`,
534
+ authored.file
535
+ );
536
+ }
537
+ if (!config.query) throw new OrmDefinitionError(`view "${authored.viewName}" produced no query`, authored.file);
538
+ const tableDependencies = pgCore.extractUsedTable(config.query).map((physical) => {
539
+ const logical = physicalToLogical.get(physical);
540
+ if (!logical) {
541
+ throw new OrmDefinitionError(
542
+ `view "${authored.viewName}" query references unmanaged relation "${physical}" \u2014 use relation(...) with a namespace table or view`,
543
+ authored.file
544
+ );
545
+ }
546
+ return logical;
547
+ });
548
+ const renderedQuery = dialect.sqlToQuery(config.query.inlineParams());
549
+ if (renderedQuery.params.length > 0) {
550
+ throw new OrmDefinitionError(
551
+ `view "${authored.viewName}" query produced bind parameters which cannot be persisted in view DDL`,
552
+ authored.file
553
+ );
554
+ }
555
+ compiled.set(authored.viewName, {
556
+ query: renderedQuery.sql,
557
+ dependencies: [.../* @__PURE__ */ new Set([...declaredDependencies, ...tableDependencies])].sort()
558
+ });
559
+ }
560
+ return compiled;
561
+ }
562
+ function buildQueryColumns(columns, enumFactories) {
563
+ return Object.fromEntries(columns.map((column) => [column.name, buildQueryColumn(column, enumFactories)]));
564
+ }
565
+ function buildQueryColumn(column, enumFactories) {
566
+ const factory = typeof column.type === "string" ? QUERY_COLUMN_FACTORIES[column.type] : enumFactories.get(column.type.enum);
567
+ if (!factory) {
568
+ const type = typeof column.type === "string" ? column.type : column.type.enum;
569
+ throw new OrmDefinitionError(`view query column '${column.columnName}' references unknown type '${type}'`);
570
+ }
571
+ let builder = factory(column.columnName);
572
+ if (!column.nullable) builder = builder.notNull();
573
+ return builder;
574
+ }
575
+ function relationName(relation) {
576
+ return isSeqTable(relation) ? relation.tableName : relation.viewName;
577
+ }
578
+ function relationKind(relation) {
579
+ return isSeqTable(relation) ? "table" : "view";
580
+ }
581
+ function declaredPrimaryKeyProperties(table) {
582
+ const fromColumns = Object.entries(table.columns).filter(([, builder]) => builder.isPrimaryKey).map(([propertyName]) => propertyName);
583
+ if (fromColumns.length > 0) return fromColumns;
584
+ const configPk = table.config.primaryKey ?? [];
585
+ const resolved = [];
586
+ for (const ref of configPk) {
587
+ if (ref in table.columns) {
588
+ resolved.push(ref);
589
+ continue;
590
+ }
591
+ const byColumnName = Object.entries(table.columns).find(
592
+ ([propertyName, builder]) => builder.columnNameFor(propertyName) === ref
593
+ );
594
+ if (byColumnName) resolved.push(byColumnName[0]);
595
+ }
596
+ return resolved;
597
+ }
598
+ function assertOverlayWinsPrimaryKey({
599
+ table,
600
+ pk,
601
+ role
602
+ }) {
603
+ if (!(pk in table.columns)) {
604
+ throw new OrmDefinitionError(
605
+ `overlayWinsUnion: pk "${pk}" must exist as a property on both overlay and synced tables`
606
+ );
607
+ }
608
+ const declared = declaredPrimaryKeyProperties(table);
609
+ if (declared.length !== 1 || declared[0] !== pk) {
610
+ throw new OrmDefinitionError(
611
+ `overlayWinsUnion: ${role} primary key must be exactly "${pk}" (declared: ${declared.length > 0 ? declared.join(", ") : "none"})`
612
+ );
613
+ }
614
+ }
615
+ function formatCanonicalColumnType(type) {
616
+ return typeof type === "string" ? type : `enum(${type.enum})`;
617
+ }
618
+ function assertOverlayWinsMatchingColumnTypes({
619
+ overlay,
620
+ synced,
621
+ columns
622
+ }) {
623
+ for (const column of columns) {
624
+ const overlayType = overlay.columns[column].toCanonicalColumn(column).type;
625
+ const syncedType = synced.columns[column].toCanonicalColumn(column).type;
626
+ if (JSON.stringify(overlayType) !== JSON.stringify(syncedType)) {
627
+ throw new OrmDefinitionError(
628
+ `overlayWinsUnion: column "${column}" must have the same type on overlay and synced (overlay: ${formatCanonicalColumnType(overlayType)}, synced: ${formatCanonicalColumnType(syncedType)})`
629
+ );
630
+ }
631
+ }
632
+ }
633
+ function overlayWinsUnion(input) {
634
+ const { overlay, synced, pk, tombstone } = input;
635
+ return ({ qb, relation }) => {
636
+ assertOverlayWinsPrimaryKey({ table: overlay, pk, role: "overlay" });
637
+ assertOverlayWinsPrimaryKey({ table: synced, pk, role: "synced" });
638
+ const overlayHandle = relation(overlay);
639
+ const syncedHandle = relation(synced);
640
+ const overlayCols = overlayHandle;
641
+ const syncedCols = syncedHandle;
642
+ const overlayPk = overlayCols[pk];
643
+ const syncedPk = syncedCols[pk];
644
+ if (tombstone !== void 0) {
645
+ const tombstoneBuilder = overlay.columns[tombstone];
646
+ if (!tombstoneBuilder) {
647
+ throw new OrmDefinitionError(
648
+ `overlayWinsUnion: tombstone "${tombstone}" must exist as a property on the overlay table`
649
+ );
650
+ }
651
+ const tombstoneCanonical = tombstoneBuilder.toCanonicalColumn(tombstone);
652
+ if (tombstoneCanonical.type !== "boolean") {
653
+ throw new OrmDefinitionError(
654
+ `overlayWinsUnion: tombstone "${tombstone}" must be a boolean column`
655
+ );
656
+ }
657
+ if (tombstoneCanonical.nullable) {
658
+ throw new OrmDefinitionError(
659
+ `overlayWinsUnion: tombstone "${tombstone}" must be notNull \u2014 nullable tombstones silently drop rows`
660
+ );
661
+ }
662
+ }
663
+ const overlayColumns = Object.keys(overlay.columns);
664
+ const syncedColumns = Object.keys(synced.columns);
665
+ const sharedColumns = overlayColumns.filter((col) => syncedColumns.includes(col) && col !== pk);
666
+ assertOverlayWinsMatchingColumnTypes({
667
+ overlay,
668
+ synced,
669
+ columns: [pk, ...sharedColumns]
670
+ });
671
+ const overlaySelect = {
672
+ [pk]: overlayPk,
673
+ ...Object.fromEntries(sharedColumns.map((col) => [col, overlayCols[col]])),
674
+ provenance: drizzleOrm.sql`'overlay'`.as("provenance")
675
+ };
676
+ const syncedSelect = {
677
+ [pk]: syncedPk,
678
+ ...Object.fromEntries(sharedColumns.map((col) => [col, syncedCols[col]])),
679
+ provenance: drizzleOrm.sql`'synced'`.as("provenance")
680
+ };
681
+ const overlayFrom = qb.select(overlaySelect).from(overlayHandle);
682
+ const overlayQuery = tombstone !== void 0 ? overlayFrom.where(drizzleOrm.eq(overlayCols[tombstone], false)) : overlayFrom;
683
+ const syncedQuery = qb.select(syncedSelect).from(syncedHandle).where(
684
+ drizzleOrm.notExists(
685
+ qb.select({ one: drizzleOrm.sql`1` }).from(overlayHandle).where(drizzleOrm.eq(overlayPk, syncedPk))
686
+ )
687
+ );
688
+ return overlayQuery.unionAll(syncedQuery);
689
+ };
690
+ }
691
+
692
+ // src/authoring/cow-mutations.ts
693
+ function overrideRowMutation(input) {
694
+ const { overlay, synced, pk } = input;
695
+ const name = input.name ?? `override_${overlay.tableName}`;
696
+ assertMutationName(name, "overrideRowMutation");
697
+ assertOverlayPairPrimaryKey({ overlay, synced, pk, helper: "overrideRowMutation" });
698
+ const sharedProperties = sharedNonPkProperties({ overlay, synced, pk });
699
+ const setProperties = resolveSetProperties({
700
+ overlay,
701
+ synced,
702
+ pk,
703
+ set: input.set,
704
+ sharedProperties,
705
+ helper: "overrideRowMutation"
706
+ });
707
+ assertMatchingColumns({
708
+ overlay,
709
+ synced,
710
+ properties: [pk, ...sharedProperties],
711
+ helper: "overrideRowMutation"
712
+ });
713
+ return {
714
+ kind: "override",
715
+ name,
716
+ synced: synced.tableName,
717
+ pk,
718
+ copy: [pk, ...sharedProperties],
719
+ set: setProperties
720
+ };
721
+ }
722
+ function revertOverlayMutation(input) {
723
+ const { overlay, synced, pk } = input;
724
+ const name = input.name ?? `revert_${overlay.tableName}`;
725
+ assertMutationName(name, "revertOverlayMutation");
726
+ assertOverlayPairPrimaryKey({ overlay, synced, pk, helper: "revertOverlayMutation" });
727
+ assertMatchingColumns({ overlay, synced, properties: [pk], helper: "revertOverlayMutation" });
728
+ return {
729
+ kind: "revert",
730
+ name,
731
+ synced: synced.tableName,
732
+ pk,
733
+ copy: [],
734
+ set: []
735
+ };
736
+ }
737
+ function assertMutationName(name, helper) {
738
+ if (!IDENTIFIER_PATTERN.test(name) || name.length > MAX_IDENTIFIER_LENGTH) {
739
+ throw new OrmDefinitionError(
740
+ `${helper}: mutation name "${name}" must match ${IDENTIFIER_PATTERN} and be at most ${MAX_IDENTIFIER_LENGTH} characters`
741
+ );
742
+ }
743
+ }
744
+ function assertOverlayPairPrimaryKey({
745
+ overlay,
746
+ synced,
747
+ pk,
748
+ helper
749
+ }) {
750
+ assertSinglePrimaryKey({ table: overlay, pk, role: "overlay", helper });
751
+ assertSinglePrimaryKey({ table: synced, pk, role: "synced", helper });
752
+ }
753
+ function assertSinglePrimaryKey({
754
+ table,
755
+ pk,
756
+ role,
757
+ helper
758
+ }) {
759
+ if (!(pk in table.columns)) {
760
+ throw new OrmDefinitionError(`${helper}: pk "${pk}" must exist as a property on the ${role} table`);
761
+ }
762
+ const declared = declaredPrimaryKeyProperties2(table);
763
+ if (declared.length !== 1 || declared[0] !== pk) {
764
+ throw new OrmDefinitionError(
765
+ `${helper}: ${role} primary key must be exactly "${pk}" (declared: ${declared.length > 0 ? declared.join(", ") : "none"})`
766
+ );
767
+ }
768
+ }
769
+ function declaredPrimaryKeyProperties2(table) {
770
+ const fromColumns = Object.entries(table.columns).filter(([, builder]) => builder.isPrimaryKey).map(([propertyName]) => propertyName);
771
+ if (fromColumns.length > 0) return fromColumns;
772
+ const configPk = table.config?.primaryKey ?? [];
773
+ const resolved = [];
774
+ for (const ref of configPk) {
775
+ if (ref in table.columns) {
776
+ resolved.push(ref);
777
+ continue;
778
+ }
779
+ const byColumnName = Object.entries(table.columns).find(
780
+ ([propertyName, builder]) => builder.columnNameFor(propertyName) === ref
781
+ );
782
+ if (byColumnName) resolved.push(byColumnName[0]);
783
+ }
784
+ return resolved;
785
+ }
786
+ function sharedNonPkProperties({
787
+ overlay,
788
+ synced,
789
+ pk
790
+ }) {
791
+ return Object.keys(overlay.columns).filter((property) => property !== pk && property in synced.columns);
792
+ }
793
+ function resolveSetProperties({
794
+ overlay,
795
+ synced,
796
+ pk,
797
+ set,
798
+ sharedProperties,
799
+ helper
800
+ }) {
801
+ const properties = set !== void 0 ? [...set] : [...sharedProperties];
802
+ if (properties.length === 0) {
803
+ throw new OrmDefinitionError(
804
+ `${helper}: set must declare at least one non-PK column to update after the copy`
805
+ );
806
+ }
807
+ const seen = /* @__PURE__ */ new Set();
808
+ for (const property of properties) {
809
+ if (property === pk) {
810
+ throw new OrmDefinitionError(`${helper}: set must not include pk "${pk}" \u2014 identity columns are pinned`);
811
+ }
812
+ if (!(property in overlay.columns)) {
813
+ throw new OrmDefinitionError(`${helper}: set column "${property}" must exist on the overlay table`);
814
+ }
815
+ if (!(property in synced.columns)) {
816
+ throw new OrmDefinitionError(
817
+ `${helper}: set column "${property}" must exist on the synced table (contract columns only)`
818
+ );
819
+ }
820
+ if (seen.has(property)) {
821
+ throw new OrmDefinitionError(`${helper}: set column "${property}" is duplicated`);
822
+ }
823
+ seen.add(property);
824
+ }
825
+ return properties;
826
+ }
827
+ function assertMatchingColumns({
828
+ overlay,
829
+ synced,
830
+ properties,
831
+ helper
832
+ }) {
833
+ for (const property of properties) {
834
+ const overlayCanonical = overlay.columns[property].toCanonicalColumn(property);
835
+ const syncedCanonical = synced.columns[property].toCanonicalColumn(property);
836
+ const overlayType = scalarColumnType(overlay, property, helper);
837
+ const syncedType = scalarColumnType(synced, property, helper);
838
+ if (overlayType !== syncedType) {
839
+ throw new OrmDefinitionError(
840
+ `${helper}: column "${property}" must have the same scalar type on overlay and synced (overlay: ${overlayType}, synced: ${syncedType})`
841
+ );
842
+ }
843
+ const overlayColumn = overlay.columns[property].columnNameFor(property);
844
+ const syncedColumn = synced.columns[property].columnNameFor(property);
845
+ if (overlayColumn !== syncedColumn) {
846
+ throw new OrmDefinitionError(
847
+ `${helper}: column "${property}" maps to different Postgres names on overlay (${overlayColumn}) and synced (${syncedColumn})`
848
+ );
849
+ }
850
+ if (syncedCanonical.nullable && !overlayCanonical.nullable) {
851
+ throw new OrmDefinitionError(
852
+ `${helper}: column "${property}" is nullable on synced but NOT NULL on overlay \u2014 a NULL synced value would fail the copy-from-synced insert; make overlay nullable or synced notNull`
853
+ );
854
+ }
855
+ }
856
+ }
857
+ function scalarColumnType(table, property, helper) {
858
+ const type = table.columns[property].toCanonicalColumn(property).type;
859
+ if (typeof type !== "string") {
860
+ throw new OrmDefinitionError(
861
+ `${helper}: column "${property}" on "${table.tableName}" must be a scalar type (enums are not supported in cow mutation arguments)`
862
+ );
863
+ }
864
+ return type;
865
+ }
345
866
  var CANONICAL_FORMAT = 2;
346
867
  var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
347
868
  var identifier = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
@@ -414,8 +935,190 @@ var policyExpressionSchema = zod.z.union([
414
935
  ]);
415
936
  var canonicalPoliciesSchema = zod.z.object({
416
937
  read: policyExpressionSchema.optional(),
417
- write: policyExpressionSchema.optional()
938
+ write: policyExpressionSchema.optional(),
939
+ insert: policyExpressionSchema.optional(),
940
+ update: policyExpressionSchema.optional()
418
941
  });
942
+ function isSingleViewQuerySql(value) {
943
+ const query = value.trim();
944
+ if (!/^\(*(?:select|with)\b/i.test(query) || query.includes("\0")) return false;
945
+ let state = "normal";
946
+ for (let index3 = 0; index3 < query.length; index3 += 1) {
947
+ const char = query[index3];
948
+ const next = query[index3 + 1];
949
+ if (state === "single") {
950
+ if (char === "'" && next === "'") index3 += 1;
951
+ else if (char === "'") state = "normal";
952
+ continue;
953
+ }
954
+ if (state === "double") {
955
+ if (char === '"' && next === '"') index3 += 1;
956
+ else if (char === '"') state = "normal";
957
+ continue;
958
+ }
959
+ if (char === "'") state = "single";
960
+ else if (char === '"') state = "double";
961
+ else if (char === ";" || char === "-" && next === "-" || char === "/" && next === "*") return false;
962
+ }
963
+ return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
964
+ }
965
+ var VIEW_QUERY_FUNCTION_ALLOWLIST = [
966
+ "abs",
967
+ "age",
968
+ "array_agg",
969
+ "array_append",
970
+ "array_cat",
971
+ "array_dims",
972
+ "array_length",
973
+ "array_lower",
974
+ "array_ndims",
975
+ "array_position",
976
+ "array_positions",
977
+ "array_prepend",
978
+ "array_remove",
979
+ "array_replace",
980
+ "array_to_string",
981
+ "array_trim",
982
+ "array_upper",
983
+ "ascii",
984
+ "avg",
985
+ "bit_and",
986
+ "bit_or",
987
+ "bit_xor",
988
+ "bool_and",
989
+ "bool_or",
990
+ "btrim",
991
+ "cardinality",
992
+ "cbrt",
993
+ "ceil",
994
+ "ceiling",
995
+ "char_length",
996
+ "character_length",
997
+ "chr",
998
+ "concat",
999
+ "concat_ws",
1000
+ "count",
1001
+ "cume_dist",
1002
+ "date_bin",
1003
+ "date_part",
1004
+ "date_trunc",
1005
+ "decode",
1006
+ "degrees",
1007
+ "dense_rank",
1008
+ "encode",
1009
+ "enum_first",
1010
+ "enum_last",
1011
+ "enum_range",
1012
+ "every",
1013
+ "exp",
1014
+ "first_value",
1015
+ "floor",
1016
+ "format",
1017
+ "grouping",
1018
+ "initcap",
1019
+ "json_agg",
1020
+ "json_array_length",
1021
+ "json_build_array",
1022
+ "json_build_object",
1023
+ "json_extract_path",
1024
+ "json_extract_path_text",
1025
+ "json_object_agg",
1026
+ "json_object_keys",
1027
+ "json_strip_nulls",
1028
+ "json_typeof",
1029
+ "jsonb_agg",
1030
+ "jsonb_array_length",
1031
+ "jsonb_build_array",
1032
+ "jsonb_build_object",
1033
+ "jsonb_extract_path",
1034
+ "jsonb_extract_path_text",
1035
+ "jsonb_insert",
1036
+ "jsonb_object_agg",
1037
+ "jsonb_object_keys",
1038
+ "jsonb_path_exists",
1039
+ "jsonb_path_match",
1040
+ "jsonb_path_query",
1041
+ "jsonb_path_query_array",
1042
+ "jsonb_path_query_first",
1043
+ "jsonb_pretty",
1044
+ "jsonb_set",
1045
+ "jsonb_set_lax",
1046
+ "jsonb_strip_nulls",
1047
+ "jsonb_typeof",
1048
+ "justify_days",
1049
+ "justify_hours",
1050
+ "justify_interval",
1051
+ "lag",
1052
+ "last_value",
1053
+ "lead",
1054
+ "left",
1055
+ "length",
1056
+ "ln",
1057
+ "log",
1058
+ "lower",
1059
+ "lpad",
1060
+ "ltrim",
1061
+ "make_date",
1062
+ "make_interval",
1063
+ "make_time",
1064
+ "make_timestamp",
1065
+ "make_timestamptz",
1066
+ "max",
1067
+ "md5",
1068
+ "min",
1069
+ "mod",
1070
+ "nth_value",
1071
+ "ntile",
1072
+ "octet_length",
1073
+ "percent_rank",
1074
+ "pi",
1075
+ "power",
1076
+ "quote_ident",
1077
+ "quote_literal",
1078
+ "quote_nullable",
1079
+ "radians",
1080
+ "rank",
1081
+ "regexp_count",
1082
+ "regexp_instr",
1083
+ "regexp_like",
1084
+ "regexp_match",
1085
+ "regexp_matches",
1086
+ "regexp_replace",
1087
+ "regexp_split_to_array",
1088
+ "regexp_split_to_table",
1089
+ "repeat",
1090
+ "replace",
1091
+ "reverse",
1092
+ "right",
1093
+ "round",
1094
+ "row_number",
1095
+ "rpad",
1096
+ "rtrim",
1097
+ "sign",
1098
+ "split_part",
1099
+ "sqrt",
1100
+ "starts_with",
1101
+ "string_agg",
1102
+ "string_to_array",
1103
+ "strpos",
1104
+ "substr",
1105
+ "substring",
1106
+ "sum",
1107
+ "timezone",
1108
+ "to_ascii",
1109
+ "to_char",
1110
+ "to_date",
1111
+ "to_hex",
1112
+ "to_json",
1113
+ "to_jsonb",
1114
+ "to_number",
1115
+ "to_timestamp",
1116
+ "translate",
1117
+ "trunc",
1118
+ "unnest",
1119
+ "upper",
1120
+ "width_bucket"
1121
+ ];
419
1122
  var canonicalActionSchema = zod.z.object({
420
1123
  name: identifier,
421
1124
  requires: zod.z.string().max(1e3).optional(),
@@ -427,6 +1130,37 @@ var canonicalActionSchema = zod.z.object({
427
1130
  returns: zod.z.enum(["rows", "rowsAffected", "scalar"]).default("rowsAffected"),
428
1131
  audit: zod.z.object({ action: zod.z.string(), resource: zod.z.string() }).optional()
429
1132
  });
1133
+ var canonicalCowMutationSchema = zod.z.object({
1134
+ kind: zod.z.enum(["override", "revert"]),
1135
+ name: identifier,
1136
+ /** Synced table (same namespace) the overlay copies from / falls back to. */
1137
+ synced: identifier,
1138
+ /** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
1139
+ pk: zod.z.string().min(1).max(63),
1140
+ /** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
1141
+ copy: zod.z.array(zod.z.string().min(1).max(63)).default([]),
1142
+ /** Override: overlay properties written by the UPDATE (never the pk). Empty for revert. */
1143
+ set: zod.z.array(zod.z.string().min(1).max(63)).default([])
1144
+ }).superRefine((mutation, ctx) => {
1145
+ if (mutation.kind === "override") {
1146
+ if (mutation.set.length === 0) {
1147
+ ctx.addIssue({ code: "custom", path: ["set"], message: "override mutations must set at least one column" });
1148
+ }
1149
+ if (!mutation.copy.includes(mutation.pk)) {
1150
+ ctx.addIssue({ code: "custom", path: ["copy"], message: "override copy list must include the pk" });
1151
+ }
1152
+ } else if (mutation.set.length > 0 || mutation.copy.length > 0) {
1153
+ ctx.addIssue({ code: "custom", path: ["set"], message: "revert mutations take no copy/set columns" });
1154
+ }
1155
+ if (mutation.set.includes(mutation.pk)) {
1156
+ ctx.addIssue({ code: "custom", path: ["set"], message: "set must not include the pk \u2014 identity columns are pinned" });
1157
+ }
1158
+ for (const [field, values] of [["copy", mutation.copy], ["set", mutation.set]]) {
1159
+ if (new Set(values).size !== values.length) {
1160
+ ctx.addIssue({ code: "custom", path: [field], message: `${field} must not contain duplicate columns` });
1161
+ }
1162
+ }
1163
+ });
430
1164
  var canonicalTableSchema = zod.z.object({
431
1165
  table: identifier,
432
1166
  description: zod.z.string().max(2e3).optional(),
@@ -439,7 +1173,45 @@ var canonicalTableSchema = zod.z.object({
439
1173
  links: zod.z.array(canonicalLinkSchema).default([]),
440
1174
  indexes: zod.z.array(canonicalIndexSchema).default([]),
441
1175
  policies: canonicalPoliciesSchema.default({}),
442
- actions: zod.z.array(canonicalActionSchema).default([])
1176
+ /**
1177
+ * v1-conversion carry-over only. v2 has no action surface — the runtime
1178
+ * neither compiles nor executes these; writes are mutations. Kept parseable
1179
+ * so stored definitions and their content hashes stay stable.
1180
+ */
1181
+ actions: zod.z.array(canonicalActionSchema).default([]),
1182
+ /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
1183
+ cowMutations: zod.z.array(canonicalCowMutationSchema).optional()
1184
+ });
1185
+ var canonicalViewSchema = zod.z.object({
1186
+ view: identifier,
1187
+ description: zod.z.string().max(2e3).optional(),
1188
+ columns: zod.z.array(canonicalColumnSchema).min(1),
1189
+ /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
1190
+ primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
1191
+ links: zod.z.array(canonicalLinkSchema).default([]),
1192
+ policies: zod.z.object({ read: policyExpressionSchema }).strict(),
1193
+ query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
1194
+ message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
1195
+ }),
1196
+ /** Namespace-local logical relation names, sorted for deterministic hashing. */
1197
+ dependencies: zod.z.array(identifier).max(500)
1198
+ }).superRefine((view, ctx) => {
1199
+ for (const [index3, column] of view.columns.entries()) {
1200
+ if (column.default !== void 0 || column.defaultFn !== void 0) {
1201
+ ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare a default` });
1202
+ }
1203
+ if (column.unique) {
1204
+ ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare unique` });
1205
+ }
1206
+ if (column.renamedFrom !== void 0 || column.cast !== void 0) {
1207
+ ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare migration hints` });
1208
+ }
1209
+ }
1210
+ for (const [index3, link] of view.links.entries()) {
1211
+ if (link.enforce) {
1212
+ ctx.addIssue({ code: "custom", path: ["links", index3, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
1213
+ }
1214
+ }
443
1215
  });
444
1216
  var canonicalDefinitionSchema = zod.z.object({
445
1217
  definition_format: zod.z.literal(CANONICAL_FORMAT),
@@ -447,24 +1219,174 @@ var canonicalDefinitionSchema = zod.z.object({
447
1219
  description: zod.z.string().max(2e3).optional(),
448
1220
  enums: zod.z.array(canonicalEnumSchema).default([]),
449
1221
  tables: zod.z.array(canonicalTableSchema).min(0).max(500),
1222
+ /** Optional to preserve byte-identical hashes for pre-view canonical v2 documents. */
1223
+ views: zod.z.array(canonicalViewSchema).max(500).optional(),
450
1224
  /** Verbatim custom-roles grammar (@sequenceholdings/custom-roles-manifest). */
451
1225
  capabilities: zod.z.object({
452
1226
  roles: zod.z.unknown().optional(),
453
1227
  uses: zod.z.array(zod.z.string()).default([])
454
1228
  }).default({ uses: [] })
1229
+ }).superRefine((definition, ctx) => {
1230
+ const seen = /* @__PURE__ */ new Set();
1231
+ for (const [index3, table] of definition.tables.entries()) {
1232
+ if (seen.has(table.table)) {
1233
+ ctx.addIssue({ code: "custom", path: ["tables", index3, "table"], message: `duplicate relation '${table.table}'` });
1234
+ }
1235
+ seen.add(table.table);
1236
+ }
1237
+ const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
1238
+ for (const [tableIndex, table] of definition.tables.entries()) {
1239
+ const cowMutations = table.cowMutations ?? [];
1240
+ if (cowMutations.length === 0) continue;
1241
+ const path = (index3, ...rest) => ["tables", tableIndex, "cowMutations", index3, ...rest];
1242
+ if (table.source !== void 0) {
1243
+ ctx.addIssue({ code: "custom", path: path(0), message: `synced table '${table.table}' cannot declare cow mutations \u2014 it is read-only` });
1244
+ continue;
1245
+ }
1246
+ for (const [index3, mutation] of cowMutations.entries()) {
1247
+ const synced = tableByName.get(mutation.synced);
1248
+ if (!synced || synced.source === void 0) {
1249
+ ctx.addIssue({
1250
+ code: "custom",
1251
+ path: path(index3, "synced"),
1252
+ message: `cow mutation '${mutation.name}' references '${mutation.synced}', which is not a synced table in this namespace`
1253
+ });
1254
+ continue;
1255
+ }
1256
+ const overlayColumnsByName = new Map(table.columns.map((column) => [column.name, column]));
1257
+ const syncedColumnsByName = new Map(synced.columns.map((column) => [column.name, column]));
1258
+ let columnsExist = true;
1259
+ for (const property of [mutation.pk, ...mutation.copy, ...mutation.set]) {
1260
+ if (!overlayColumnsByName.has(property)) {
1261
+ columnsExist = false;
1262
+ ctx.addIssue({ code: "custom", path: path(index3), message: `cow mutation '${mutation.name}': column '${property}' does not exist on overlay '${table.table}'` });
1263
+ }
1264
+ }
1265
+ for (const property of [mutation.pk, ...mutation.copy]) {
1266
+ if (!syncedColumnsByName.has(property)) {
1267
+ columnsExist = false;
1268
+ ctx.addIssue({ code: "custom", path: path(index3), message: `cow mutation '${mutation.name}': column '${property}' does not exist on synced '${mutation.synced}'` });
1269
+ }
1270
+ }
1271
+ if (!columnsExist) continue;
1272
+ const overlayPk = table.primaryKey ?? [];
1273
+ const syncedPk = synced.primaryKey ?? [];
1274
+ if (overlayPk.length !== 1 || overlayPk[0] !== mutation.pk || syncedPk.length !== 1 || syncedPk[0] !== mutation.pk) {
1275
+ ctx.addIssue({
1276
+ code: "custom",
1277
+ path: path(index3, "pk"),
1278
+ message: `cow mutation '${mutation.name}': pk '${mutation.pk}' must be the single-column primary key on both '${table.table}' and '${mutation.synced}'`
1279
+ });
1280
+ }
1281
+ for (const property of [mutation.pk, ...mutation.copy]) {
1282
+ const overlayColumn = overlayColumnsByName.get(property);
1283
+ const syncedColumn = syncedColumnsByName.get(property);
1284
+ if (!overlayColumn || !syncedColumn) continue;
1285
+ if (JSON.stringify(overlayColumn.type) !== JSON.stringify(syncedColumn.type)) {
1286
+ ctx.addIssue({
1287
+ code: "custom",
1288
+ path: path(index3),
1289
+ message: `cow mutation '${mutation.name}': column '${property}' must have the same type on overlay and synced`
1290
+ });
1291
+ }
1292
+ if (overlayColumn.columnName !== syncedColumn.columnName) {
1293
+ ctx.addIssue({
1294
+ code: "custom",
1295
+ path: path(index3),
1296
+ message: `cow mutation '${mutation.name}': column '${property}' maps to different Postgres names on overlay ('${overlayColumn.columnName}') and synced ('${syncedColumn.columnName}')`
1297
+ });
1298
+ }
1299
+ if (syncedColumn.nullable !== false && overlayColumn.nullable === false) {
1300
+ ctx.addIssue({
1301
+ code: "custom",
1302
+ path: path(index3),
1303
+ message: `cow mutation '${mutation.name}': column '${property}' is nullable on synced but NOT NULL on overlay \u2014 a NULL synced value would fail the copy`
1304
+ });
1305
+ }
1306
+ }
1307
+ }
1308
+ }
1309
+ for (const [index3, view] of (definition.views ?? []).entries()) {
1310
+ if (seen.has(view.view)) {
1311
+ ctx.addIssue({ code: "custom", path: ["views", index3, "view"], message: `duplicate relation '${view.view}'` });
1312
+ }
1313
+ seen.add(view.view);
1314
+ }
1315
+ const views = definition.views ?? [];
1316
+ const viewByName = new Map(views.map((view) => [view.view, view]));
1317
+ for (const [viewIndex, view] of views.entries()) {
1318
+ for (const [dependencyIndex, dependency] of view.dependencies.entries()) {
1319
+ if (!seen.has(dependency)) {
1320
+ ctx.addIssue({
1321
+ code: "custom",
1322
+ path: ["views", viewIndex, "dependencies", dependencyIndex],
1323
+ message: `view '${view.view}' depends on unknown relation '${dependency}'`
1324
+ });
1325
+ }
1326
+ }
1327
+ }
1328
+ const visiting = /* @__PURE__ */ new Set();
1329
+ const visited = /* @__PURE__ */ new Set();
1330
+ let cycleReported = false;
1331
+ const visit = (name, path) => {
1332
+ if (visited.has(name) || cycleReported) return;
1333
+ if (visiting.has(name)) {
1334
+ cycleReported = true;
1335
+ ctx.addIssue({
1336
+ code: "custom",
1337
+ path: ["views"],
1338
+ message: `view dependencies form a cycle: ${[...path, name].join(" -> ")}`
1339
+ });
1340
+ return;
1341
+ }
1342
+ visiting.add(name);
1343
+ const view = viewByName.get(name);
1344
+ for (const dependency of view?.dependencies ?? []) {
1345
+ if (viewByName.has(dependency)) visit(dependency, [...path, name]);
1346
+ }
1347
+ visiting.delete(name);
1348
+ visited.add(name);
1349
+ };
1350
+ for (const view of views) visit(view.view, []);
455
1351
  });
1352
+ function canonicalRelations(definition) {
1353
+ const views = (definition.views ?? []).map((view) => ({
1354
+ table: view.view,
1355
+ ...view.description !== void 0 ? { description: view.description } : {},
1356
+ columns: view.columns,
1357
+ ...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
1358
+ links: view.links,
1359
+ indexes: [],
1360
+ policies: { read: view.policies.read },
1361
+ actions: []
1362
+ }));
1363
+ return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
1364
+ }
456
1365
  function normalizeCanonical(definition) {
457
1366
  const sortByName = (items) => [...items].sort((a, b) => a.name.localeCompare(b.name));
458
- return {
1367
+ const normalized = {
459
1368
  ...definition,
460
1369
  enums: sortByName(definition.enums),
461
- tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => ({
462
- ...t2,
463
- links: sortByName(t2.links),
464
- indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
465
- actions: sortByName(t2.actions)
466
- }))
1370
+ tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
1371
+ const { cowMutations, ...rest } = t2;
1372
+ return {
1373
+ ...rest,
1374
+ links: sortByName(t2.links),
1375
+ indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
1376
+ actions: sortByName(t2.actions),
1377
+ // Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
1378
+ ...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
1379
+ };
1380
+ })
467
1381
  };
1382
+ if (definition.views !== void 0) {
1383
+ normalized.views = [...definition.views].sort((a, b) => a.view.localeCompare(b.view)).map((view) => ({
1384
+ ...view,
1385
+ links: sortByName(view.links),
1386
+ dependencies: [...view.dependencies].sort()
1387
+ }));
1388
+ }
1389
+ return normalized;
468
1390
  }
469
1391
  function stableJson(value) {
470
1392
  if (value === null || typeof value !== "object") return JSON.stringify(value);
@@ -486,9 +1408,13 @@ function canonicalTable(table) {
486
1408
  source: table.source ?? null,
487
1409
  links: emptyToNull(table.links),
488
1410
  indexes: emptyToNull(table.indexes),
1411
+ // insert/update are omitted rather than nulled when absent, so definitions
1412
+ // predating split policies keep the hash they were registered under.
489
1413
  policies: {
490
1414
  read: table.policies?.read?.source ?? null,
491
- write: table.policies?.write?.source ?? null
1415
+ write: table.policies?.write?.source ?? null,
1416
+ ...table.policies?.insert !== void 0 ? { insert: table.policies.insert.source } : {},
1417
+ ...table.policies?.update !== void 0 ? { update: table.policies.update.source } : {}
492
1418
  },
493
1419
  actions: emptyToNull(
494
1420
  Object.fromEntries(
@@ -569,6 +1495,7 @@ function defineNamespace(input) {
569
1495
  }
570
1496
  function serializeNamespace(input) {
571
1497
  const { config, tables } = input;
1498
+ const views = input.views ?? [];
572
1499
  const enums = input.enums ?? [];
573
1500
  const tablesByName = /* @__PURE__ */ new Map();
574
1501
  for (const table of tables) {
@@ -581,6 +1508,29 @@ function serializeNamespace(input) {
581
1508
  }
582
1509
  tablesByName.set(table.tableName, table);
583
1510
  }
1511
+ const viewsByName = /* @__PURE__ */ new Map();
1512
+ for (const view of views) {
1513
+ const existing = viewsByName.get(view.viewName);
1514
+ if (existing) {
1515
+ throw new OrmDefinitionError(
1516
+ `duplicate view "${view.viewName}" (defined in ${describeFile(existing)} and ${describeFile(view)})`,
1517
+ view.file
1518
+ );
1519
+ }
1520
+ viewsByName.set(view.viewName, view);
1521
+ }
1522
+ const relationsByName = /* @__PURE__ */ new Map();
1523
+ for (const relation of [...tables, ...views]) {
1524
+ const name = relationName(relation);
1525
+ const existing = relationsByName.get(name);
1526
+ if (existing) {
1527
+ throw new OrmDefinitionError(
1528
+ `duplicate relation "${name}" (defined as ${relationKind(existing)} in ${describeFile(existing)} and ${relationKind(relation)} in ${describeFile(relation)})`,
1529
+ relation.file
1530
+ );
1531
+ }
1532
+ relationsByName.set(name, relation);
1533
+ }
584
1534
  const enumsByName = /* @__PURE__ */ new Map();
585
1535
  for (const ref of enums) {
586
1536
  if (enumsByName.has(ref.name)) {
@@ -588,11 +1538,26 @@ function serializeNamespace(input) {
588
1538
  }
589
1539
  enumsByName.set(ref.name, ref);
590
1540
  }
591
- const canonicalTables = tables.map((table) => buildCanonicalTable({ table, tablesByName, enumsByName }));
1541
+ const canonicalTables = tables.map((table) => buildCanonicalTable({ table, relationsByName, enumsByName }));
592
1542
  const canonicalEnums = [...enumsByName.values()].map((ref) => ({
593
1543
  name: ref.name,
594
1544
  values: [...ref.values]
595
1545
  }));
1546
+ const viewDrafts = views.map((view) => buildCanonicalView({ view, relationsByName, enumsByName }));
1547
+ const compiledViews = compileViewQueries({
1548
+ namespace: config.namespace,
1549
+ tables: canonicalTables,
1550
+ views: viewDrafts,
1551
+ enums: canonicalEnums,
1552
+ authoredTables: tables,
1553
+ authoredViews: views
1554
+ });
1555
+ const canonicalViews = viewDrafts.map((draft) => {
1556
+ const compiled = compiledViews.get(draft.view);
1557
+ if (!compiled) throw new OrmDefinitionError(`view "${draft.view}" did not compile`, viewsByName.get(draft.view)?.file);
1558
+ return canonicalViewSchema.parse({ ...draft, ...compiled });
1559
+ });
1560
+ assertAcyclicViewDependencies(canonicalViews);
596
1561
  const candidate = {
597
1562
  definition_format: CANONICAL_FORMAT,
598
1563
  namespace: config.namespace,
@@ -604,6 +1569,7 @@ function serializeNamespace(input) {
604
1569
  }
605
1570
  };
606
1571
  if (config.description !== void 0) candidate.description = config.description;
1572
+ if (canonicalViews.length > 0) candidate.views = canonicalViews;
607
1573
  let parsed;
608
1574
  try {
609
1575
  parsed = canonicalDefinitionSchema.parse(candidate);
@@ -618,7 +1584,7 @@ function serializeNamespace(input) {
618
1584
  function computeCanonicalHash(definition) {
619
1585
  return crypto.createHash("sha256").update(HASH_DOMAIN).update(stableJson(definition)).digest("hex");
620
1586
  }
621
- function buildCanonicalTable({ table, tablesByName, enumsByName }) {
1587
+ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
622
1588
  const candidate = {
623
1589
  table: table.tableName,
624
1590
  columns: [],
@@ -630,6 +1596,31 @@ function buildCanonicalTable({ table, tablesByName, enumsByName }) {
630
1596
  if (table.config.description !== void 0) candidate.description = table.config.description;
631
1597
  if (table.config.renamedFrom !== void 0) candidate.renamedFrom = table.config.renamedFrom;
632
1598
  if (table.source !== void 0) candidate.source = table.source;
1599
+ if (table.source !== void 0) {
1600
+ if (table.config.indexes !== void 0 && table.config.indexes.length > 0) {
1601
+ throw new OrmDefinitionError(
1602
+ `synced table "${table.tableName}" is system-owned; indexes are not allowed`,
1603
+ table.file
1604
+ );
1605
+ }
1606
+ if (table.config.cowMutations !== void 0 && table.config.cowMutations.length > 0) {
1607
+ throw new OrmDefinitionError(
1608
+ `synced table "${table.tableName}" is read-only; cow mutations belong on the overlay table`,
1609
+ table.file
1610
+ );
1611
+ }
1612
+ if (table.config.policies?.write !== void 0 || table.config.policies?.insert !== void 0 || table.config.policies?.update !== void 0) {
1613
+ throw new OrmDefinitionError(`synced table "${table.tableName}" is read-only`, table.file);
1614
+ }
1615
+ for (const [linkName, link] of Object.entries(table.config.links ?? {})) {
1616
+ if (link.enforce) {
1617
+ throw new OrmDefinitionError(
1618
+ `synced table "${table.tableName}": link "${linkName}" cannot enforce foreign keys \u2014 its DDL is owned by the sync`,
1619
+ table.file
1620
+ );
1621
+ }
1622
+ }
1623
+ }
633
1624
  const columnLevelPk = [];
634
1625
  for (const [propertyName, builder] of Object.entries(table.columns)) {
635
1626
  const column = builder.toCanonicalColumn(propertyName);
@@ -651,40 +1642,38 @@ function buildCanonicalTable({ table, tablesByName, enumsByName }) {
651
1642
  );
652
1643
  }
653
1644
  candidate.primaryKey = table.config.primaryKey.map(
654
- (ref) => resolveColumnRef({ table, ref, role: "primaryKey" })
1645
+ (ref) => resolveColumnRef({ relation: table, ref, role: "primaryKey" })
655
1646
  );
656
1647
  } else if (columnLevelPk.length > 0) {
657
1648
  candidate.primaryKey = columnLevelPk;
658
1649
  }
659
1650
  for (const index3 of table.config.indexes ?? []) {
660
1651
  const entry = {
661
- columns: index3.columns.map((ref) => resolveColumnRef({ table, ref, role: "index" }))
1652
+ columns: index3.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
662
1653
  };
663
1654
  if (index3.unique !== void 0) entry.unique = index3.unique;
664
1655
  if (index3.name !== void 0) entry.name = index3.name;
665
1656
  candidate.indexes?.push(entry);
666
1657
  }
667
1658
  for (const [linkName, link] of Object.entries(table.config.links ?? {})) {
668
- candidate.links?.push(buildCanonicalLink({ table, linkName, link, tablesByName }));
1659
+ candidate.links?.push(buildCanonicalLink({ relation: table, linkName, link, relationsByName }));
669
1660
  }
670
1661
  const policies = {};
671
1662
  if (table.config.policies?.read !== void 0) policies.read = toCanonicalPolicy(table.config.policies.read);
672
1663
  if (table.config.policies?.write !== void 0) policies.write = toCanonicalPolicy(table.config.policies.write);
1664
+ if (table.config.policies?.insert !== void 0) policies.insert = toCanonicalPolicy(table.config.policies.insert);
1665
+ if (table.config.policies?.update !== void 0) policies.update = toCanonicalPolicy(table.config.policies.update);
673
1666
  candidate.policies = policies;
674
- for (const action of table.config.actions ?? []) {
675
- candidate.actions?.push({
676
- name: action.name,
677
- ...action.requires !== void 0 ? { requires: action.requires } : {},
678
- params: Object.fromEntries(
679
- Object.entries(action.params ?? {}).map(([name, param]) => [
680
- name,
681
- { type: param.type, nullable: param.nullable ?? false }
682
- ])
683
- ),
684
- executes: action.executes,
685
- returns: action.returns ?? "rowsAffected",
686
- ...action.audit !== void 0 ? { audit: action.audit } : {}
687
- });
1667
+ const cowMutations = table.config.cowMutations ?? [];
1668
+ if (cowMutations.length > 0) {
1669
+ candidate.cowMutations = cowMutations.map((mutation) => ({
1670
+ kind: mutation.kind,
1671
+ name: mutation.name,
1672
+ synced: mutation.synced,
1673
+ pk: mutation.pk,
1674
+ copy: [...mutation.copy],
1675
+ set: [...mutation.set]
1676
+ }));
688
1677
  }
689
1678
  try {
690
1679
  return canonicalTableSchema.parse(candidate);
@@ -695,14 +1684,71 @@ function buildCanonicalTable({ table, tablesByName, enumsByName }) {
695
1684
  throw error;
696
1685
  }
697
1686
  }
1687
+ function buildCanonicalView({
1688
+ view,
1689
+ relationsByName,
1690
+ enumsByName
1691
+ }) {
1692
+ if (view.config.policies?.read === void 0) {
1693
+ throw new OrmDefinitionError(`view "${view.viewName}" must declare policies.read`, view.file);
1694
+ }
1695
+ const columns = [];
1696
+ const columnLevelPk = [];
1697
+ for (const [propertyName, builder] of Object.entries(view.columns)) {
1698
+ const column = builder.toCanonicalColumn(propertyName);
1699
+ if (column.default !== void 0 || column.defaultFn !== void 0) {
1700
+ throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare a default`, view.file);
1701
+ }
1702
+ if (column.unique) {
1703
+ throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare unique`, view.file);
1704
+ }
1705
+ if (column.renamedFrom !== void 0 || column.cast !== void 0) {
1706
+ throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare migration hints`, view.file);
1707
+ }
1708
+ const enumName = builder.enumName;
1709
+ if (enumName !== void 0 && !enumsByName.has(enumName)) {
1710
+ throw new OrmDefinitionError(
1711
+ `view "${view.viewName}": column "${propertyName}" references enum "${enumName}" which is not part of this namespace`,
1712
+ view.file
1713
+ );
1714
+ }
1715
+ if (builder.isPrimaryKey) columnLevelPk.push(column.columnName);
1716
+ columns.push(column);
1717
+ }
1718
+ let primaryKey2;
1719
+ if (view.config.primaryKey !== void 0) {
1720
+ if (columnLevelPk.length > 0) {
1721
+ throw new OrmDefinitionError(
1722
+ `view "${view.viewName}": declare the logical primary key either on columns or with config.primaryKey \u2014 not both`,
1723
+ view.file
1724
+ );
1725
+ }
1726
+ primaryKey2 = view.config.primaryKey.map((ref) => resolveColumnRef({ relation: view, ref, role: "primaryKey" }));
1727
+ } else if (columnLevelPk.length > 0) {
1728
+ primaryKey2 = columnLevelPk;
1729
+ }
1730
+ const links = Object.entries(view.config.links ?? {}).map(
1731
+ ([linkName, link]) => buildCanonicalLink({ relation: view, linkName, link, relationsByName })
1732
+ );
1733
+ return {
1734
+ view: view.viewName,
1735
+ ...view.config.description !== void 0 ? { description: view.config.description } : {},
1736
+ columns,
1737
+ ...primaryKey2 !== void 0 ? { primaryKey: primaryKey2 } : {},
1738
+ links,
1739
+ policies: { read: toCanonicalPolicy(view.config.policies.read) }
1740
+ };
1741
+ }
698
1742
  function buildCanonicalLink({
699
- table,
1743
+ relation,
700
1744
  linkName,
701
1745
  link,
702
- tablesByName
1746
+ relationsByName
703
1747
  }) {
1748
+ const ownerKind = relationKind(relation);
1749
+ const ownerName = relationName(relation);
704
1750
  const fail = (message) => {
705
- throw new OrmDefinitionError(`table "${table.tableName}": link "${linkName}" ${message}`, table.file);
1751
+ throw new OrmDefinitionError(`${ownerKind} "${ownerName}": link "${linkName}" ${message}`, relation.file);
706
1752
  };
707
1753
  if (link.enforce && link.linkKind !== "toOne") {
708
1754
  return fail("sets enforce: true, which is only supported on toOne links");
@@ -712,29 +1758,34 @@ function buildCanonicalLink({
712
1758
  return {
713
1759
  name: linkName,
714
1760
  kind: "toPlatformUser",
715
- on: resolveColumnRef({ table, ref: link.on, role: `link "${linkName}" on` }),
1761
+ on: resolveColumnRef({ relation, ref: link.on, role: `link "${linkName}" on` }),
716
1762
  ...link.references !== void 0 ? { references: deriveColumnName(link.references) } : {},
717
1763
  enforce: false,
718
1764
  ...link.description !== void 0 ? { description: link.description } : {}
719
1765
  };
720
1766
  }
721
- const target = resolveTableRef({ ref: link.target, fail });
722
- if (!tablesByName.has(target.tableName)) {
723
- return fail(`targets table "${target.tableName}" which is not part of this namespace`);
1767
+ const target = resolveRelationRef({ ref: link.target, fail });
1768
+ const targetName = relationName(target);
1769
+ if (!relationsByName.has(targetName)) {
1770
+ return fail(`targets ${relationKind(target)} "${targetName}" which is not part of this namespace`);
724
1771
  }
725
- const owningSide = link.linkKind === "toOne" ? table : target;
1772
+ if (link.enforce && (!isSeqTable(relation) || !isSeqTable(target))) {
1773
+ return fail("cannot enforce a PostgreSQL foreign key involving a view");
1774
+ }
1775
+ const owningSide = link.linkKind === "toOne" ? relation : target;
1776
+ const referencedSide = link.linkKind === "toOne" ? target : relation;
726
1777
  return {
727
1778
  name: linkName,
728
1779
  kind: link.linkKind,
729
- target: target.tableName,
730
- on: resolveColumnRef({ table: owningSide, ref: link.on, role: `link "${linkName}" on` }),
731
- ...link.references !== void 0 ? { references: resolveColumnRef({ table: target, ref: link.references, role: `link "${linkName}" references` }) } : {},
1780
+ target: targetName,
1781
+ on: resolveColumnRef({ relation: owningSide, ref: link.on, role: `link "${linkName}" on` }),
1782
+ ...link.references !== void 0 ? { references: resolveColumnRef({ relation: referencedSide, ref: link.references, role: `link "${linkName}" references` }) } : {},
732
1783
  enforce: link.enforce,
733
1784
  ...link.description !== void 0 ? { description: link.description } : {}
734
1785
  };
735
1786
  }
736
- function resolveTableRef({ ref, fail }) {
737
- if (ref === void 0) return fail("declares no target table");
1787
+ function resolveRelationRef({ ref, fail }) {
1788
+ if (ref === void 0) return fail("declares no target relation");
738
1789
  if (typeof ref !== "function") return ref;
739
1790
  let resolved;
740
1791
  try {
@@ -744,20 +1795,35 @@ function resolveTableRef({ ref, fail }) {
744
1795
  `target thunk threw (${error instanceof Error ? error.message : String(error)}) \u2014 lazy refs resolve at serialize time; make sure the target module ran`
745
1796
  );
746
1797
  }
747
- if (!isSeqTable(resolved)) return fail("target thunk did not return a table defined with defineTable");
1798
+ if (!isSeqTable(resolved) && !isSeqView(resolved)) {
1799
+ return fail("target thunk did not return a relation defined with defineTable/defineView");
1800
+ }
748
1801
  return resolved;
749
1802
  }
750
- function resolveColumnRef({ table, ref, role }) {
751
- const direct = table.columns[ref];
1803
+ function resolveColumnRef({ relation, ref, role }) {
1804
+ const direct = relation.columns[ref];
752
1805
  if (direct) return direct.columnNameFor(ref);
753
- for (const [propertyName, builder] of Object.entries(table.columns)) {
1806
+ for (const [propertyName, builder] of Object.entries(relation.columns)) {
754
1807
  if (builder.columnNameFor(propertyName) === ref) return ref;
755
1808
  }
756
- const known = Object.keys(table.columns).join(", ");
1809
+ const known = Object.keys(relation.columns).join(", ");
757
1810
  throw new OrmDefinitionError(
758
- `table "${table.tableName}": ${role} references unknown column "${ref}" (known columns: ${known || "none"})`,
759
- table.file
1811
+ `${relationKind(relation)} "${relationName(relation)}": ${role} references unknown column "${ref}" (known columns: ${known || "none"})`,
1812
+ relation.file
1813
+ );
1814
+ }
1815
+ function assertAcyclicViewDependencies(views) {
1816
+ const viewNames = new Set(views.map((view) => view.view));
1817
+ const remaining = new Map(
1818
+ views.map((view) => [view.view, new Set(view.dependencies.filter((dependency) => viewNames.has(dependency)))])
760
1819
  );
1820
+ while (remaining.size > 0) {
1821
+ const ready = [...remaining.entries()].filter(([, dependencies]) => [...dependencies].every((dependency) => !remaining.has(dependency))).map(([name]) => name);
1822
+ if (ready.length === 0) {
1823
+ throw new OrmDefinitionError(`view dependencies form a cycle among: ${[...remaining.keys()].sort().join(", ")}`);
1824
+ }
1825
+ for (const name of ready) remaining.delete(name);
1826
+ }
761
1827
  }
762
1828
  function toCanonicalPolicy(policy) {
763
1829
  if (typeof policy === "string") return policy;
@@ -770,12 +1836,110 @@ function toCanonicalPolicy(policy) {
770
1836
  }))
771
1837
  };
772
1838
  }
773
- function describeFile(table) {
774
- return table.file ?? "<unknown file>";
1839
+ function describeFile(relation) {
1840
+ return relation.file ?? "<unknown file>";
775
1841
  }
776
1842
  function formatZodIssues(error) {
777
1843
  return error.issues.map((issue) => issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message).join("; ");
778
1844
  }
1845
+ var SYNCED_CONTRACT_HASH_DOMAIN = "sequence-orm-synced-contract\0";
1846
+ var ORM_TO_PIPELINE_TYPE = {
1847
+ uuid: "uuid",
1848
+ text: "string",
1849
+ integer: "int",
1850
+ bigint: "long",
1851
+ numeric: "numeric",
1852
+ double: "double",
1853
+ boolean: "boolean",
1854
+ timestamptz: "timestamp",
1855
+ date: "date",
1856
+ jsonb: "json"
1857
+ };
1858
+ function isColumnType(value) {
1859
+ return typeof value === "string" && value in ORM_TO_PIPELINE_TYPE;
1860
+ }
1861
+ function syncedContractSurface({
1862
+ namespace,
1863
+ table,
1864
+ columns,
1865
+ primary_key
1866
+ }) {
1867
+ return {
1868
+ namespace,
1869
+ table,
1870
+ columns: Object.fromEntries(
1871
+ Object.entries(columns).map(([name, column]) => [
1872
+ name,
1873
+ { type: column.type, nullable: column.nullable, key: column.key }
1874
+ ])
1875
+ ),
1876
+ primary_key: [...primary_key]
1877
+ };
1878
+ }
1879
+ function computeSyncedContractHash(surface) {
1880
+ return crypto.createHash("sha256").update(SYNCED_CONTRACT_HASH_DOMAIN).update(stableJson(surface)).digest("hex");
1881
+ }
1882
+ function extractOrmSyncedTableContract({
1883
+ namespace,
1884
+ table
1885
+ }) {
1886
+ if (!NAMESPACE_PATTERN.test(namespace)) {
1887
+ throw new OrmDefinitionError(
1888
+ `namespace "${namespace}" must match ${NAMESPACE_PATTERN}`
1889
+ );
1890
+ }
1891
+ if (table.source === void 0) {
1892
+ throw new OrmDefinitionError(
1893
+ `table "${table.table}" is not a synced table \u2014 extractOrmSyncedTableContract requires source: gold.*`
1894
+ );
1895
+ }
1896
+ if (table.columns.length === 0) {
1897
+ throw new OrmDefinitionError(
1898
+ `synced table "${table.table}" has no typed columns \u2014 declare a typed defineSyncedTable contract before exporting`
1899
+ );
1900
+ }
1901
+ const primaryKey2 = table.primaryKey;
1902
+ if (primaryKey2 === void 0 || primaryKey2.length === 0) {
1903
+ throw new OrmDefinitionError(
1904
+ `synced table "${table.table}" has no primary key \u2014 typed synced contracts require a primary key`
1905
+ );
1906
+ }
1907
+ const pkSet = new Set(primaryKey2);
1908
+ const columns = {};
1909
+ for (const column of table.columns) {
1910
+ if (!isColumnType(column.type)) {
1911
+ const enumName = typeof column.type === "object" && column.type !== null && "enum" in column.type ? column.type.enum : String(column.type);
1912
+ throw new OrmDefinitionError(
1913
+ `synced table "${table.table}": column "${column.columnName}" has enum/type '${enumName}', which has no pipeline-type mapping \u2014 use an ORM-mappable scalar`
1914
+ );
1915
+ }
1916
+ columns[column.columnName] = {
1917
+ type: ORM_TO_PIPELINE_TYPE[column.type],
1918
+ nullable: column.nullable,
1919
+ key: pkSet.has(column.columnName)
1920
+ };
1921
+ }
1922
+ for (const key of primaryKey2) {
1923
+ if (!(key in columns)) {
1924
+ throw new OrmDefinitionError(
1925
+ `synced table "${table.table}": primary key column "${key}" is not declared in columns`
1926
+ );
1927
+ }
1928
+ }
1929
+ const surface = syncedContractSurface({
1930
+ namespace,
1931
+ table: table.table,
1932
+ columns,
1933
+ primary_key: primaryKey2
1934
+ });
1935
+ return {
1936
+ namespace,
1937
+ table: table.table,
1938
+ columns,
1939
+ primary_key: [...primaryKey2],
1940
+ content_hash: computeSyncedContractHash(surface)
1941
+ };
1942
+ }
779
1943
  var NAMESPACE_CONFIG_FILE = "sequence.config.ts";
780
1944
  var SCHEMA_DIR = "schema";
781
1945
  async function loadNamespaceFromDir(dir, options = {}) {
@@ -834,16 +1998,20 @@ async function loadNamespaceFromDir(dir, options = {}) {
834
1998
  for (const entry of context.tables) {
835
1999
  if (isSeqTable(entry.value)) tables.push(entry.value);
836
2000
  }
837
- if (tables.length === 0) {
2001
+ const views = [];
2002
+ for (const entry of context.views) {
2003
+ if (isSeqView(entry.value)) views.push(entry.value);
2004
+ }
2005
+ if (tables.length === 0 && views.length === 0) {
838
2006
  throw new OrmDefinitionError(
839
- `no tables registered \u2014 ${SCHEMA_DIR}/*.ts modules must call defineTable(...) at import time`
2007
+ `no relations registered \u2014 ${SCHEMA_DIR}/*.ts modules must call defineTable(...) or defineView(...) at import time`
840
2008
  );
841
2009
  }
842
2010
  const enums = [];
843
2011
  for (const entry of context.enums) {
844
2012
  if (isEnumRef(entry.value)) enums.push(entry.value);
845
2013
  }
846
- const canonical = serializeNamespace({ config: configEntry.value, tables, enums });
2014
+ const canonical = serializeNamespace({ config: configEntry.value, tables, views, enums });
847
2015
  return { canonical, hash: computeCanonicalHash(canonical), files };
848
2016
  }
849
2017
  async function listSchemaFiles(root) {
@@ -881,13 +2049,17 @@ function generateNamespaceTypes(definition) {
881
2049
  `export type ${typesInterfaceName(definition.namespace)} = {`
882
2050
  ];
883
2051
  for (const table of definition.tables) {
884
- if (table.source) {
2052
+ const columns = Object.entries(table.columns ?? {});
2053
+ if (table.source && columns.length === 0) {
885
2054
  lines.push(` /** Synced from ${table.source} \u2014 columns reflect the gold source. */`);
886
2055
  lines.push(` ${table.table}: Record<string, unknown>`);
887
2056
  continue;
888
2057
  }
2058
+ if (table.source) {
2059
+ lines.push(` /** Synced from ${table.source} \u2014 read-only typed contract. */`);
2060
+ }
889
2061
  lines.push(` ${table.table}: {`);
890
- for (const [name, column] of Object.entries(table.columns ?? {})) {
2062
+ for (const [name, column] of columns) {
891
2063
  if (column.deprecated) continue;
892
2064
  lines.push(` ${name}: ${columnWireType(column)}`);
893
2065
  }
@@ -1344,8 +2516,8 @@ var actionSchema = zod.z.strictObject({
1344
2516
  }
1345
2517
  }
1346
2518
  });
1347
- function extractNamedParams(sql3) {
1348
- const withoutStrings = sql3.replace(QUOTED_STRING_PATTERN, "''");
2519
+ function extractNamedParams(sql4) {
2520
+ const withoutStrings = sql4.replace(QUOTED_STRING_PATTERN, "''");
1349
2521
  const names = [...withoutStrings.matchAll(NAMED_PARAM_PATTERN)].map((match) => match[1]);
1350
2522
  return [...new Set(names)];
1351
2523
  }
@@ -1415,11 +2587,13 @@ var sqlTypeName = (type) => SQL_TYPE_NAMES[type] ?? type;
1415
2587
  // src/compiler/sql.ts
1416
2588
  var CLAIMS_SETTING = "request.jwt.claims";
1417
2589
  var CAPABILITIES_CLAIM_KEY = "capabilities";
2590
+ var ROLES_CLAIM_KEY = "roles";
1418
2591
  var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
1419
2592
  var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
1420
2593
  var CLAIMS_SOURCE = `nullif(current_setting('${CLAIMS_SETTING}', true), '')::jsonb`;
1421
2594
  var claimExpression = (key) => `(${CLAIMS_SOURCE} ->> ${quoteLiteral(key)})`;
1422
2595
  var capabilityExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${CAPABILITIES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
2596
+ var roleExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${ROLES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
1423
2597
 
1424
2598
  // src/compiler/predicates.ts
1425
2599
  var SQL_COMPARE_OPS = {
@@ -1500,8 +2674,8 @@ var actionParamArg = (name) => `p_${name}`;
1500
2674
  function compileActionStatements(action, renderParam = actionParamArg) {
1501
2675
  return actionStatementSources(action.executes).map((statement) => compileStatement(statement, renderParam));
1502
2676
  }
1503
- function compileStatement(sql3, renderParam) {
1504
- const compiled = sql3.replace(TOKEN_PATTERN, (token, claimKey, capabilityKey, paramName) => {
2677
+ function compileStatement(sql4, renderParam) {
2678
+ const compiled = sql4.replace(TOKEN_PATTERN, (token, claimKey, capabilityKey, paramName) => {
1505
2679
  if (claimKey !== void 0) {
1506
2680
  if (!CLAIM_KEY_PATTERN.test(claimKey)) throw new OrmDefinitionError(`invalid claim key in executes: ${token}`);
1507
2681
  return claimExpression(claimKey);
@@ -1533,6 +2707,118 @@ function referencedExecuteCapabilities(action) {
1533
2707
  return keys;
1534
2708
  }
1535
2709
 
2710
+ // src/compiler/pred-ir-sql.ts
2711
+ function compilePredIrToSql(pred, options = {}) {
2712
+ switch (pred.kind) {
2713
+ case "const":
2714
+ return pred.value ? "TRUE" : "FALSE";
2715
+ case "and":
2716
+ return `(${pred.operands.map((operand) => compilePredIrToSql(operand, options)).join(" AND ")})`;
2717
+ case "or":
2718
+ return `(${pred.operands.map((operand) => compilePredIrToSql(operand, options)).join(" OR ")})`;
2719
+ case "not":
2720
+ return `(NOT ${compilePredIrToSql(pred.operand, options)})`;
2721
+ case "cmp":
2722
+ return compileCmp(pred.op, pred.column, pred.value, options);
2723
+ case "capability":
2724
+ return `(${capabilityExpression(pred.key)})`;
2725
+ case "role":
2726
+ return `(${roleExpression(pred.key)})`;
2727
+ case "exists":
2728
+ case "aggPredicate":
2729
+ case "dynamicFilter":
2730
+ throw new OrmDefinitionError(
2731
+ `synced view read policy cannot use '${pred.kind}' \u2014 use row filters and claim/capability/role checks only`
2732
+ );
2733
+ default: {
2734
+ const exhausted = pred;
2735
+ throw new OrmDefinitionError(`unsupported synced view predicate kind '${String(exhausted)}'`);
2736
+ }
2737
+ }
2738
+ }
2739
+ function physicalColumn(logical, options) {
2740
+ return options.columnNames?.[logical] ?? logical;
2741
+ }
2742
+ function compileCmp(op, column, value, options) {
2743
+ const physical = physicalColumn(column, options);
2744
+ const declared = options.columnTypes?.[physical] ?? options.columnTypes?.[column];
2745
+ const isEnum = typeof declared === "object" && declared !== null;
2746
+ const col = isEnum ? `${quoteIdentifier(physical)}::text` : quoteIdentifier(physical);
2747
+ const columnType = isEnum ? "text" : declared;
2748
+ switch (op) {
2749
+ case "eq":
2750
+ return compileEquality(col, value, columnType, false);
2751
+ case "neq":
2752
+ return compileEquality(col, value, columnType, true);
2753
+ case "gt":
2754
+ return `${col} > ${compileValue(value, columnType)}`;
2755
+ case "gte":
2756
+ return `${col} >= ${compileValue(value, columnType)}`;
2757
+ case "lt":
2758
+ return `${col} < ${compileValue(value, columnType)}`;
2759
+ case "lte":
2760
+ return `${col} <= ${compileValue(value, columnType)}`;
2761
+ case "in":
2762
+ return `${col} = ANY(${compileArrayValue(value, columnType)})`;
2763
+ case "nin":
2764
+ return `${col} <> ALL(${compileArrayValue(value, columnType)})`;
2765
+ case "like":
2766
+ return `${col} LIKE ${compileValue(value, columnType)}`;
2767
+ case "ilike":
2768
+ return `${col} ILIKE ${compileValue(value, columnType)}`;
2769
+ case "isNull":
2770
+ return value.kind === "literal" && value.value === true ? `${col} IS NULL` : `${col} IS NOT NULL`;
2771
+ case "contains":
2772
+ case "hasKey":
2773
+ throw new OrmDefinitionError(`synced view read policy cannot use json operator '${op}'`);
2774
+ default: {
2775
+ const exhausted = op;
2776
+ throw new OrmDefinitionError(`unsupported synced view comparison '${String(exhausted)}'`);
2777
+ }
2778
+ }
2779
+ }
2780
+ function compileEquality(col, value, columnType, negated, options) {
2781
+ if (value.kind === "literal" && value.value === null) {
2782
+ return negated ? `${col} IS NOT NULL` : `${col} IS NULL`;
2783
+ }
2784
+ const rhs = compileValue(value, columnType);
2785
+ return negated ? `${col} <> ${rhs}` : `${col} = ${rhs}`;
2786
+ }
2787
+ function compileValue(value, columnType, _options) {
2788
+ switch (value.kind) {
2789
+ case "literal": {
2790
+ const literal = value.value;
2791
+ if (literal === null) return "NULL";
2792
+ if (typeof literal === "string") return quoteLiteral(literal);
2793
+ if (typeof literal === "boolean") return literal ? "TRUE" : "FALSE";
2794
+ return String(literal);
2795
+ }
2796
+ case "claim": {
2797
+ const expression = claimExpression(value.key);
2798
+ return columnType && columnType !== "text" ? `(${expression})::${sqlTypeName(columnType)}` : expression;
2799
+ }
2800
+ case "variable":
2801
+ throw new OrmDefinitionError("synced view read policy cannot reference operation variables");
2802
+ default: {
2803
+ const exhausted = value;
2804
+ throw new OrmDefinitionError(`unsupported synced view value kind '${String(exhausted)}'`);
2805
+ }
2806
+ }
2807
+ }
2808
+ function compileArrayValue(value, columnType, _options) {
2809
+ if (value.kind !== "literal" || !Array.isArray(value.value)) {
2810
+ throw new OrmDefinitionError("synced view IN/NIN comparisons require literal arrays");
2811
+ }
2812
+ const entries = value.value.map((entry) => {
2813
+ if (entry === null) return "NULL";
2814
+ if (typeof entry === "string") return quoteLiteral(entry);
2815
+ if (typeof entry === "boolean") return entry ? "TRUE" : "FALSE";
2816
+ return String(entry);
2817
+ });
2818
+ const array = `ARRAY[${entries.join(", ")}]`;
2819
+ return columnType ? `${array}::${sqlTypeName(columnType)}[]` : array;
2820
+ }
2821
+
1536
2822
  // src/engine/actions.ts
1537
2823
  var ACTION_REQUIRES_DENIED_SQLSTATE = "ORMRQ";
1538
2824
  var ACTION_VALIDATES_FAILED_SQLSTATE = "ORMVL";
@@ -1554,8 +2840,8 @@ function actionPrepareChecks(definition) {
1554
2840
  const order = actionParamOrder(action);
1555
2841
  const signature = order.length > 0 ? ` (${order.map((param) => sqlTypeName(action.params[param].type)).join(", ")})` : "";
1556
2842
  const compiled = compileActionStatements(action, (param) => `$${order.indexOf(param) + 1}`);
1557
- compiled.forEach((sql3, index3) => {
1558
- const checked = action.returns && index3 === compiled.length - 1 ? returnProjection(action.returns, sql3) : sql3;
2843
+ compiled.forEach((sql4, index3) => {
2844
+ const checked = action.returns && index3 === compiled.length - 1 ? returnProjection(action.returns, sql4) : sql4;
1559
2845
  statements.push(`PREPARE ${quoteIdentifier(`orm_check_${name}_${index3}`)}${signature} AS ${checked}`);
1560
2846
  });
1561
2847
  }
@@ -1674,7 +2960,9 @@ var indexSchema = zod.z.union([
1674
2960
  ]).transform((index3) => Array.isArray(index3) ? { columns: index3 } : index3);
1675
2961
  var policiesSchema = zod.z.strictObject({
1676
2962
  read: parsedPredicateSchema.optional(),
1677
- write: parsedPredicateSchema.optional()
2963
+ write: parsedPredicateSchema.optional(),
2964
+ insert: parsedPredicateSchema.optional(),
2965
+ update: parsedPredicateSchema.optional()
1678
2966
  });
1679
2967
  var tableFileSchema = zod.z.strictObject({
1680
2968
  namespace: namespaceSchema,
@@ -1710,7 +2998,6 @@ var policyName = (table, suffix) => `${table}_${suffix}`;
1710
2998
  var foreignKeyName = (table, link) => `fk_${table}_${link}`;
1711
2999
  function validateSyncedTable(table, ctx) {
1712
3000
  const forbidden = [
1713
- ["columns", "synced tables reflect their source; columns are not declared"],
1714
3001
  ["actions", "synced tables are read-only; actions are not allowed"],
1715
3002
  ["indexes", "synced tables are system-owned; indexes are not allowed"]
1716
3003
  ];
@@ -1720,6 +3007,39 @@ function validateSyncedTable(table, ctx) {
1720
3007
  if (table.policies?.write) {
1721
3008
  ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "write"] });
1722
3009
  }
3010
+ if (table.policies?.insert) {
3011
+ ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "insert"] });
3012
+ }
3013
+ if (table.policies?.update) {
3014
+ ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "update"] });
3015
+ }
3016
+ const columns = table.columns ?? {};
3017
+ const names = Object.keys(columns);
3018
+ if (names.length === 0) return;
3019
+ const primaryKeys = names.filter((name) => columns[name]?.primary_key);
3020
+ if (primaryKeys.length !== 1) {
3021
+ ctx.addIssue({ code: "custom", message: "exactly one column must be primary_key", path: ["columns"] });
3022
+ }
3023
+ validateRenames(columns, names, ctx);
3024
+ for (const name of names) {
3025
+ const column = columns[name];
3026
+ if (!column) continue;
3027
+ const cast = column.cast ? castParts(column.cast) : null;
3028
+ if (column.cast && cast?.column !== name) {
3029
+ ctx.addIssue({
3030
+ code: "custom",
3031
+ message: "cast must reference the column it is declared on",
3032
+ path: ["columns", name, "cast"]
3033
+ });
3034
+ }
3035
+ if (cast && cast.type !== column.type) {
3036
+ ctx.addIssue({
3037
+ code: "custom",
3038
+ message: `cast target '${cast.type}' must match the declared type '${column.type}'`,
3039
+ path: ["columns", name, "cast"]
3040
+ });
3041
+ }
3042
+ }
1723
3043
  }
1724
3044
  function validateRenames(columns, names, ctx) {
1725
3045
  const renameSources = /* @__PURE__ */ new Map();
@@ -1797,7 +3117,7 @@ function validateColumnTable(table, ctx) {
1797
3117
  function validateLinks(table, ctx) {
1798
3118
  const columnNames = Object.keys(table.columns ?? {});
1799
3119
  for (const [name, link] of Object.entries(table.links ?? {})) {
1800
- if (!table.source && !columnNames.includes(link.on)) {
3120
+ if (columnNames.length > 0 && !columnNames.includes(link.on)) {
1801
3121
  ctx.addIssue({ code: "custom", message: `link references unknown column '${link.on}'`, path: ["links", name, "on"] });
1802
3122
  }
1803
3123
  if (link.enforce && link.to.includes(".")) {
@@ -1870,10 +3190,10 @@ var TYPE_GROUPS = {
1870
3190
  };
1871
3191
  var columnTypeGroup = (type) => TYPE_GROUPS[type];
1872
3192
  function validatePolicyColumns(table, ctx) {
1873
- if (table.source) return;
1874
3193
  const columns = table.columns ?? {};
1875
3194
  const columnNames = Object.keys(columns);
1876
- for (const kind of ["read", "write"]) {
3195
+ if (columnNames.length === 0) return;
3196
+ for (const kind of ["read", "write", "insert", "update"]) {
1877
3197
  const predicate = table.policies?.[kind];
1878
3198
  if (!predicate) continue;
1879
3199
  for (const column of referencedColumns(predicate.ast)) {
@@ -1950,10 +3270,15 @@ function policyStatements(schema, table) {
1950
3270
  if (read) {
1951
3271
  statements.push(`CREATE POLICY ${policyName2("read")} ON ${qualified} FOR SELECT USING (${compilePredicate(read.ast, columnTypes)})`);
1952
3272
  }
1953
- const write = table.policies?.write;
1954
- if (write) {
1955
- const predicate = compilePredicate(write.ast, columnTypes);
1956
- statements.push(`CREATE POLICY ${policyName2("insert")} ON ${qualified} FOR INSERT WITH CHECK (${predicate})`);
3273
+ const insertPolicy = table.policies?.insert ?? table.policies?.write;
3274
+ const updatePolicy = table.policies?.update ?? table.policies?.write;
3275
+ if (insertPolicy) {
3276
+ statements.push(
3277
+ `CREATE POLICY ${policyName2("insert")} ON ${qualified} FOR INSERT WITH CHECK (${compilePredicate(insertPolicy.ast, columnTypes)})`
3278
+ );
3279
+ }
3280
+ if (updatePolicy) {
3281
+ const predicate = compilePredicate(updatePolicy.ast, columnTypes);
1957
3282
  statements.push(
1958
3283
  `CREATE POLICY ${policyName2("update")} ON ${qualified} FOR UPDATE USING (${predicate}) WITH CHECK (${predicate})`
1959
3284
  );
@@ -1962,6 +3287,118 @@ function policyStatements(schema, table) {
1962
3287
  return statements;
1963
3288
  }
1964
3289
  var columnTables = (definition) => definition.tables.filter((table) => !table.source);
3290
+ var syncedTables = (definition) => definition.tables.filter((table) => !!table.source);
3291
+ var SYNCED_VIEW_EXECUTION_OWNER_ROLE = "orm_exec";
3292
+ function syncedViewStatements({
3293
+ definition,
3294
+ backings,
3295
+ readPolicyIrByTable,
3296
+ columnNamesByTable,
3297
+ columnTypesByTable,
3298
+ executionOwnerRole = SYNCED_VIEW_EXECUTION_OWNER_ROLE
3299
+ }) {
3300
+ const schema = quoteIdentifier(ormSchemaName(definition.namespace));
3301
+ const role = quoteIdentifier(consumerRoleName(definition.namespace));
3302
+ const owner = quoteIdentifier(executionOwnerRole);
3303
+ const backingMap = /* @__PURE__ */ new Map();
3304
+ for (const backing of backings) {
3305
+ const existing = backingMap.get(backing.table);
3306
+ if (existing) {
3307
+ throw new OrmDefinitionError(
3308
+ `duplicate synced backing for table "${backing.table}" ('${existing.physicalRelation}' and '${backing.physicalRelation}') \u2014 refusing to materialize an ambiguous authorization boundary`
3309
+ );
3310
+ }
3311
+ backingMap.set(backing.table, backing);
3312
+ }
3313
+ const statements = [];
3314
+ for (const table of syncedTables(definition)) {
3315
+ const backing = backingMap.get(table.table);
3316
+ if (!backing) continue;
3317
+ const viewName = `${schema}.${quoteIdentifier(table.table)}`;
3318
+ const physical = splitPhysicalRelation(backing.physicalRelation);
3319
+ const physicalTarget = `${quoteIdentifier(physical.schema)}.${quoteIdentifier(physical.table)}`;
3320
+ const columnTypes = mergeSyncedViewColumnTypes({
3321
+ table,
3322
+ canonicalColumnTypes: columnTypesByTable?.get(table.table),
3323
+ backingColumnTypes: backing.columnTypes,
3324
+ columnNames: columnNamesByTable?.get(table.table)
3325
+ });
3326
+ const readPolicyIr = readPolicyIrByTable?.get(table.table);
3327
+ const readPolicy = table.policies?.read;
3328
+ const whereClause = readPolicyIr ? ` WHERE ${compilePredIrToSql(readPolicyIr, {
3329
+ columnTypes,
3330
+ columnNames: columnNamesByTable?.get(table.table)
3331
+ })}` : readPolicy ? ` WHERE ${compilePredicate(readPolicy.ast, narrowToV1ColumnTypes(columnTypes))}` : " WHERE false";
3332
+ statements.push(`GRANT USAGE ON SCHEMA ${quoteIdentifier(physical.schema)} TO ${owner}`);
3333
+ statements.push(`GRANT SELECT ON ${physicalTarget} TO ${owner}`);
3334
+ statements.push(
3335
+ `CREATE OR REPLACE VIEW ${viewName} AS SELECT * FROM ${physicalTarget}${whereClause}`
3336
+ );
3337
+ statements.push(`GRANT SELECT ON ${viewName} TO ${role}`);
3338
+ }
3339
+ return statements;
3340
+ }
3341
+ function mergeSyncedViewColumnTypes({
3342
+ table,
3343
+ canonicalColumnTypes,
3344
+ backingColumnTypes,
3345
+ columnNames
3346
+ }) {
3347
+ const fromTable = {};
3348
+ for (const [name, def] of Object.entries(table.columns ?? {})) {
3349
+ if (def?.type) fromTable[name] = def.type;
3350
+ }
3351
+ if (columnNames) {
3352
+ for (const [logical, physical] of Object.entries(columnNames)) {
3353
+ const declared = fromTable[physical] ?? fromTable[logical];
3354
+ if (!declared) continue;
3355
+ fromTable[physical] = declared;
3356
+ fromTable[logical] = declared;
3357
+ }
3358
+ }
3359
+ const canonicalEnums = Object.fromEntries(
3360
+ Object.entries(canonicalColumnTypes ?? {}).filter(([, type]) => typeof type === "object")
3361
+ );
3362
+ return { ...fromTable, ...canonicalColumnTypes ?? {}, ...backingColumnTypes ?? {}, ...canonicalEnums };
3363
+ }
3364
+ function narrowToV1ColumnTypes(columnTypes) {
3365
+ return Object.fromEntries(
3366
+ Object.entries(columnTypes).map(([column, type]) => [column, typeof type === "object" ? "text" : type])
3367
+ );
3368
+ }
3369
+ function splitPhysicalRelation(physicalRelation) {
3370
+ const parts = physicalRelation.split(".");
3371
+ if (parts.length === 1 && parts[0]) return { schema: "public", table: parts[0] };
3372
+ if (parts.length === 2 && parts[0] && parts[1]) return { schema: parts[0], table: parts[1] };
3373
+ throw new OrmDefinitionError(
3374
+ `synced view physical relation must be "table" or "schema.table" (got "${physicalRelation}")`
3375
+ );
3376
+ }
3377
+ function syncedViewDropStatements({
3378
+ schemaName,
3379
+ tables
3380
+ }) {
3381
+ const schema = quoteIdentifier(schemaName);
3382
+ return tables.map(
3383
+ (table) => `DROP VIEW IF EXISTS ${schema}.${quoteIdentifier(table)} CASCADE`
3384
+ );
3385
+ }
3386
+ function syncedBackingRevokeStatements({
3387
+ backings,
3388
+ executionOwnerRole = SYNCED_VIEW_EXECUTION_OWNER_ROLE
3389
+ }) {
3390
+ const owner = quoteIdentifier(executionOwnerRole);
3391
+ const seen = /* @__PURE__ */ new Set();
3392
+ const statements = [];
3393
+ for (const backing of backings) {
3394
+ const physical = splitPhysicalRelation(backing.physicalRelation);
3395
+ const physicalTarget = `${quoteIdentifier(physical.schema)}.${quoteIdentifier(physical.table)}`;
3396
+ if (seen.has(physicalTarget)) continue;
3397
+ seen.add(physicalTarget);
3398
+ statements.push(`REVOKE SELECT ON ${physicalTarget} FROM ${owner}`);
3399
+ }
3400
+ return statements;
3401
+ }
1965
3402
  var FACTORIES = {
1966
3403
  uuid: (name) => pgCore.uuid(name),
1967
3404
  text: (name) => pgCore.text(name),
@@ -2155,99 +3592,16 @@ function applyMigrationHints(previous, definition) {
2155
3592
  }
2156
3593
  }
2157
3594
  }
2158
- return { statements, adjusted };
2159
- }
2160
- function findTable(snapshot, schema, table) {
2161
- const tables = snapshot.tables ?? {};
2162
- return Object.values(tables).find((entry) => entry.schema === schema && entry.name === table);
2163
- }
2164
- function snapshotTableNames(snapshot, schema) {
2165
- const tables = snapshot.tables ?? {};
2166
- return Object.values(tables).filter((entry) => entry.schema === schema).map((entry) => entry.name);
2167
- }
2168
- var OWNED_STATEMENT = new RegExp(
2169
- [
2170
- String.raw`^\s*(CREATE|DROP)\s+POLICY\b`,
2171
- String.raw`^\s*(GRANT|REVOKE)\b`,
2172
- String.raw`^\s*(CREATE|DROP|ALTER)\s+ROLE\b`,
2173
- String.raw`^\s*(CREATE|DROP)\s+(OR\s+REPLACE\s+)?VIEW\b`,
2174
- // Only ever appears in an RLS toggle (ALTER TABLE … [ENABLE|FORCE] ROW LEVEL
2175
- // SECURITY) — a three-word phrase, never an identifier.
2176
- String.raw`\bROW LEVEL SECURITY\b`
2177
- ].join("|"),
2178
- "i"
2179
- );
2180
- function isOwnedStatement(statement) {
2181
- return OWNED_STATEMENT.test(statement);
2182
- }
2183
- async function pendingKitStatements({
2184
- definition,
2185
- db
2186
- }) {
2187
- const { imports, schemaName } = buildDrizzleEntities(definition);
2188
- const { statementsToExecute } = await withExitGuard(
2189
- () => api.pushSchema(imports, shimExecute(db), [schemaName])
2190
- );
2191
- return statementsToExecute.filter((statement) => !OWNED_STATEMENT.test(statement));
2192
- }
2193
- function shimExecute(db) {
2194
- const execute = db.execute.bind(db);
2195
- return {
2196
- execute: async (query) => {
2197
- const result = await execute(query);
2198
- if (result && typeof result === "object" && "rows" in result) return result;
2199
- return { rows: result };
2200
- }
2201
- };
2202
- }
2203
- var exitGuardChain = Promise.resolve();
2204
- function withExitGuard(run) {
2205
- const next = exitGuardChain.then(() => guardedRun(run));
2206
- exitGuardChain = next.then(
2207
- () => void 0,
2208
- () => void 0
2209
- );
2210
- return next;
2211
- }
2212
- async function guardedRun(run) {
2213
- const originalExit = process.exit;
2214
- process.exit = ((code) => {
2215
- throw new Error(`drizzle-kit attempted process.exit(${code ?? 0}) during pushSchema introspection`);
2216
- });
2217
- try {
2218
- return await run();
2219
- } finally {
2220
- process.exit = originalExit;
2221
- }
3595
+ return { statements, adjusted };
2222
3596
  }
2223
-
2224
- // src/engine/index.ts
2225
- var STATEMENT_KINDS = ["hint", "ddl", "security", "function"];
2226
- async function planMigration({
2227
- definition,
2228
- previousSnapshot
2229
- }) {
2230
- const schema = ormSchemaName(definition.namespace);
2231
- const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(definition.namespace);
2232
- const hints = applyMigrationHints(previous, definition);
2233
- const snapshot = generateSnapshot(definition, previousId(previous));
2234
- const ddl = await diffStatements(hints.adjusted, snapshot);
2235
- const policyDrops = policyDropStatements(schema, snapshotTableNames(previous, schema));
2236
- const statements = [
2237
- ...policyDrops.map((sql3) => ({ kind: "security", sql: sql3 })),
2238
- ...hints.statements.map((sql3) => ({ kind: "hint", sql: sql3 })),
2239
- ...ddl.map((sql3) => ({ kind: "ddl", sql: sql3.replace(/;\s*$/, "") })),
2240
- ...securityStatements(definition).map((sql3) => ({ kind: "security", sql: sql3 })),
2241
- // PREPARE each action's executes first (plpgsql bodies don't check columns
2242
- // at CREATE), then create the functions. Both reference the tables the DDL
2243
- // above creates, so they close the plan.
2244
- ...actionPrepareChecks(definition).map((sql3) => ({ kind: "function", sql: sql3 })),
2245
- ...actionFunctionStatements(definition).map((sql3) => ({ kind: "function", sql: sql3 }))
2246
- ];
2247
- return { namespace: definition.namespace, schema, statements, snapshot };
3597
+ function findTable(snapshot, schema, table) {
3598
+ const tables = snapshot.tables ?? {};
3599
+ return Object.values(tables).find((entry) => entry.schema === schema && entry.name === table);
3600
+ }
3601
+ function snapshotTableNames(snapshot, schema) {
3602
+ const tables = snapshot.tables ?? {};
3603
+ return Object.values(tables).filter((entry) => entry.schema === schema).map((entry) => entry.name);
2248
3604
  }
2249
- var compileNamespace = (definition) => planMigration({ definition });
2250
- var previousId = (snapshot) => snapshot.id;
2251
3605
  var FACTORIES2 = {
2252
3606
  uuid: (name) => pgCore.uuid(name),
2253
3607
  text: (name) => pgCore.text(name),
@@ -2285,11 +3639,268 @@ function buildDrizzleEntitiesV2(definition) {
2285
3639
  imports[table.table] = drizzleTable;
2286
3640
  tables[table.table] = drizzleTable;
2287
3641
  }
3642
+ for (const view of definition.views ?? []) {
3643
+ const columns = Object.fromEntries(
3644
+ view.columns.map((column) => [column.columnName, buildViewColumnV2({ view: view.view, column, enums })])
3645
+ );
3646
+ imports[view.view] = schema.view(view.view, columns).as(
3647
+ drizzleOrm.sql.raw(
3648
+ rewriteSyncedSourceRelationsInViewQuery({
3649
+ query: view.query,
3650
+ canonical: definition,
3651
+ schemaName,
3652
+ dependencies: view.dependencies
3653
+ })
3654
+ )
3655
+ );
3656
+ }
2288
3657
  return { imports, tables, schemaName };
2289
3658
  }
3659
+ function rewriteSyncedSourceRelationsInViewQuery({
3660
+ query,
3661
+ canonical,
3662
+ schemaName,
3663
+ dependencies
3664
+ }) {
3665
+ const tablesBySource = /* @__PURE__ */ new Map();
3666
+ for (const table of canonical.tables) {
3667
+ if (!table.source) continue;
3668
+ tablesBySource.set(table.source, [...tablesBySource.get(table.source) ?? [], table.table]);
3669
+ }
3670
+ const declared = new Set(dependencies ?? []);
3671
+ const boundTableBySource = /* @__PURE__ */ new Map();
3672
+ for (const [source, tables] of tablesBySource) {
3673
+ if (tables.length === 1) {
3674
+ boundTableBySource.set(source, tables[0]);
3675
+ continue;
3676
+ }
3677
+ const [sourceSchema, sourceName] = source.split(".");
3678
+ if (!sourceSchema || !sourceName) continue;
3679
+ const referenced = sqlReferencesQualifiedRelation(query, sourceSchema, sourceName);
3680
+ if (!referenced) continue;
3681
+ const declaredMatches = tables.filter((table) => declared.has(table));
3682
+ if (declaredMatches.length === 1) {
3683
+ boundTableBySource.set(source, declaredMatches[0]);
3684
+ continue;
3685
+ }
3686
+ throw new OrmDefinitionError(
3687
+ `synced source '${source}' is declared by multiple synced tables (${[...tables].sort().join(", ")}) \u2014 this view references it but its declared dependencies do not name exactly one of them, so the rewrite is ambiguous`
3688
+ );
3689
+ }
3690
+ return transformSqlOutsideLiterals(query, (segment) => {
3691
+ let rewritten = segment;
3692
+ for (const table of canonical.tables) {
3693
+ if (!table.source) continue;
3694
+ const boundTable = boundTableBySource.get(table.source);
3695
+ if (boundTable === void 0 || boundTable !== table.table) continue;
3696
+ const [sourceSchema, sourceName] = table.source.split(".");
3697
+ if (!sourceSchema || !sourceName) continue;
3698
+ rewritten = replaceQualifiedRelation(
3699
+ rewritten,
3700
+ sourceSchema,
3701
+ sourceName,
3702
+ schemaName,
3703
+ table.table
3704
+ );
3705
+ }
3706
+ return rewritten;
3707
+ });
3708
+ }
3709
+ function transformSqlOutsideLiterals(sql4, transform) {
3710
+ const out = [];
3711
+ let state = { kind: "normal" };
3712
+ let i = 0;
3713
+ let codeStart = 0;
3714
+ const emitCode = (end) => {
3715
+ if (end > codeStart) out.push(transform(sql4.slice(codeStart, end)));
3716
+ codeStart = end;
3717
+ };
3718
+ while (i < sql4.length) {
3719
+ const c = sql4[i];
3720
+ const next = i + 1 < sql4.length ? sql4[i + 1] : "";
3721
+ switch (state.kind) {
3722
+ case "normal": {
3723
+ if (c === "-" && next === "-") {
3724
+ emitCode(i);
3725
+ state = { kind: "line-comment" };
3726
+ out.push("--");
3727
+ i += 2;
3728
+ codeStart = i;
3729
+ break;
3730
+ }
3731
+ if (c === "/" && next === "*") {
3732
+ emitCode(i);
3733
+ state = { kind: "block-comment", depth: 1 };
3734
+ out.push("/*");
3735
+ i += 2;
3736
+ codeStart = i;
3737
+ break;
3738
+ }
3739
+ if ((c === "E" || c === "e") && next === "'") {
3740
+ const prev = i > 0 ? sql4[i - 1] : "";
3741
+ if (!/[A-Za-z0-9_]/.test(prev)) {
3742
+ emitCode(i);
3743
+ state = { kind: "e-single" };
3744
+ out.push("E'");
3745
+ i += 2;
3746
+ codeStart = i;
3747
+ break;
3748
+ }
3749
+ }
3750
+ if (c === "'") {
3751
+ emitCode(i);
3752
+ state = { kind: "single" };
3753
+ out.push("'");
3754
+ i += 1;
3755
+ codeStart = i;
3756
+ break;
3757
+ }
3758
+ if (c === "$") {
3759
+ const prev = i > 0 ? sql4[i - 1] : "";
3760
+ if (!/[A-Za-z0-9_$]/.test(prev)) {
3761
+ const tagMatch = /^\$([A-Za-z_][A-Za-z0-9_]*)?\$/.exec(sql4.slice(i));
3762
+ if (tagMatch) {
3763
+ emitCode(i);
3764
+ state = { kind: "dollar", tag: tagMatch[1] ?? "" };
3765
+ out.push(tagMatch[0]);
3766
+ i += tagMatch[0].length;
3767
+ codeStart = i;
3768
+ break;
3769
+ }
3770
+ }
3771
+ }
3772
+ i += 1;
3773
+ break;
3774
+ }
3775
+ case "single": {
3776
+ if (c === "'" && next === "'") {
3777
+ out.push("''");
3778
+ i += 2;
3779
+ break;
3780
+ }
3781
+ if (c === "'") {
3782
+ out.push("'");
3783
+ state = { kind: "normal" };
3784
+ i += 1;
3785
+ codeStart = i;
3786
+ break;
3787
+ }
3788
+ out.push(c);
3789
+ i += 1;
3790
+ break;
3791
+ }
3792
+ case "e-single": {
3793
+ if (c === "\\" && i + 1 < sql4.length) {
3794
+ out.push(c, sql4[i + 1]);
3795
+ i += 2;
3796
+ break;
3797
+ }
3798
+ if (c === "'" && next === "'") {
3799
+ out.push("''");
3800
+ i += 2;
3801
+ break;
3802
+ }
3803
+ if (c === "'") {
3804
+ out.push("'");
3805
+ state = { kind: "normal" };
3806
+ i += 1;
3807
+ codeStart = i;
3808
+ break;
3809
+ }
3810
+ out.push(c);
3811
+ i += 1;
3812
+ break;
3813
+ }
3814
+ case "dollar": {
3815
+ const closer = `$${state.tag}$`;
3816
+ if (sql4.startsWith(closer, i)) {
3817
+ out.push(closer);
3818
+ state = { kind: "normal" };
3819
+ i += closer.length;
3820
+ codeStart = i;
3821
+ break;
3822
+ }
3823
+ out.push(c);
3824
+ i += 1;
3825
+ break;
3826
+ }
3827
+ case "line-comment": {
3828
+ if (c === "\n") {
3829
+ out.push("\n");
3830
+ state = { kind: "normal" };
3831
+ i += 1;
3832
+ codeStart = i;
3833
+ break;
3834
+ }
3835
+ out.push(c);
3836
+ i += 1;
3837
+ break;
3838
+ }
3839
+ case "block-comment": {
3840
+ if (c === "/" && next === "*") {
3841
+ out.push("/*");
3842
+ state = { kind: "block-comment", depth: state.depth + 1 };
3843
+ i += 2;
3844
+ break;
3845
+ }
3846
+ if (c === "*" && next === "/") {
3847
+ out.push("*/");
3848
+ const newDepth = state.depth - 1;
3849
+ state = newDepth === 0 ? { kind: "normal" } : { kind: "block-comment", depth: newDepth };
3850
+ i += 2;
3851
+ if (state.kind === "normal") codeStart = i;
3852
+ break;
3853
+ }
3854
+ out.push(c);
3855
+ i += 1;
3856
+ break;
3857
+ }
3858
+ }
3859
+ }
3860
+ emitCode(sql4.length);
3861
+ return out.join("");
3862
+ }
3863
+ function escapeRegExp(value) {
3864
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3865
+ }
3866
+ function sqlReferencesQualifiedRelation(sql4, sourceSchema, sourceName) {
3867
+ let referenced = false;
3868
+ transformSqlOutsideLiterals(sql4, (segment) => {
3869
+ if (qualifiedRelationPattern(sourceSchema, sourceName).test(segment)) referenced = true;
3870
+ return segment;
3871
+ });
3872
+ return referenced;
3873
+ }
3874
+ function qualifiedRelationPattern(sourceSchema, sourceName) {
3875
+ const schemaPattern = `(?:"${escapeRegExp(sourceSchema)}"|${escapeRegExp(sourceSchema)})`;
3876
+ const namePattern = `(?:"${escapeRegExp(sourceName)}"|${escapeRegExp(sourceName)})`;
3877
+ return new RegExp(`(?<![\\w."])${schemaPattern}\\.${namePattern}(?![\\w"])`, "g");
3878
+ }
3879
+ function replaceQualifiedRelation(sql4, sourceSchema, sourceName, boundSchema, boundTable) {
3880
+ return sql4.replace(qualifiedRelationPattern(sourceSchema, sourceName), `"${boundSchema}"."${boundTable}"`);
3881
+ }
2290
3882
  function generateSnapshotV2(definition, previousId3) {
2291
3883
  const { imports, schemaName } = buildDrizzleEntitiesV2(definition);
2292
- return api.generateDrizzleJson(imports, previousId3, [schemaName]);
3884
+ const snapshot = api.generateDrizzleJson(imports, previousId3, [schemaName]);
3885
+ if ((definition.views?.length ?? 0) > 0) {
3886
+ const extended = snapshot;
3887
+ extended._sequenceOrm = {
3888
+ viewDependencies: Object.fromEntries(
3889
+ (definition.views ?? []).map((view) => [view.view, [...view.dependencies].sort()])
3890
+ )
3891
+ };
3892
+ }
3893
+ return snapshot;
3894
+ }
3895
+ function buildViewColumnV2({
3896
+ view,
3897
+ column,
3898
+ enums
3899
+ }) {
3900
+ const factory = resolveColumnFactory(view, column, enums);
3901
+ let builder = factory(column.columnName);
3902
+ if (!column.nullable) builder = builder.notNull();
3903
+ return builder;
2293
3904
  }
2294
3905
  function buildTable2(schema, table, built, enums, byName) {
2295
3906
  const primaryKeyColumns = table.primaryKey ?? [];
@@ -2533,15 +4144,379 @@ async function planMigrationV2({
2533
4144
  }) {
2534
4145
  const schema = ormSchemaName(canonical.namespace);
2535
4146
  const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
4147
+ assertValidViewDependencies(canonical);
2536
4148
  const hints = applyMigrationHintsV2(previous, canonical);
2537
- const snapshot = generateSnapshotV2(canonical, previousId2(previous));
2538
- const ddl = await diffStatements(hints.adjusted, snapshot);
4149
+ const snapshot = generateSnapshotV2(canonical, previousId(previous));
4150
+ const baseDdl = await diffStatements(withoutViews(hints.adjusted), withoutViews(snapshot));
4151
+ const previousViews = snapshotViews(hints.adjusted);
4152
+ const currentViews = snapshotViews(snapshot);
4153
+ const previousDependencies = snapshotViewDependencies(hints.adjusted);
4154
+ const currentDependencies = snapshotViewDependencies(snapshot);
4155
+ const affected = affectedViewNames({
4156
+ previousViews,
4157
+ currentViews,
4158
+ previousDependencies,
4159
+ currentDependencies,
4160
+ baseChanged: hints.statements.length > 0 || baseDdl.length > 0
4161
+ });
4162
+ const dropOrder = topologicalViewNames(
4163
+ new Set([...affected].filter((name) => previousViews.has(name))),
4164
+ previousDependencies
4165
+ ).reverse();
4166
+ const createOrder = topologicalViewNames(
4167
+ new Set([...affected].filter((name) => currentViews.has(name))),
4168
+ currentDependencies
4169
+ );
4170
+ const dropDdl = await generatedViewStatements({
4171
+ snapshot: hints.adjusted,
4172
+ names: new Set(dropOrder),
4173
+ mode: "drop",
4174
+ schema
4175
+ });
4176
+ const createDdl = await generatedViewStatements({
4177
+ snapshot,
4178
+ names: new Set(createOrder),
4179
+ mode: "create",
4180
+ schema
4181
+ });
4182
+ const currentViewDefinitions = new Map((canonical.views ?? []).map((view) => [view.view, view]));
2539
4183
  const statements = [
4184
+ ...dropOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(dropDdl, name, schema, "DROP") })),
2540
4185
  ...hints.statements.map((statement) => ({ kind: "hint", sql: statement })),
2541
- ...ddl.map((statement) => ({ kind: "ddl", sql: statement.replace(/;\s*$/, "") }))
4186
+ ...baseDdl.map((statement) => ({ kind: "ddl", sql: statement.replace(/;\s*$/, "") })),
4187
+ ...createOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(createDdl, name, schema, "CREATE") })),
4188
+ ...createOrder.map((name) => {
4189
+ const view = currentViewDefinitions.get(name);
4190
+ if (!view) throw new OrmDefinitionError(`missing canonical definition for view '${name}'`);
4191
+ return { kind: "ddl", sql: validateViewCatalogStatement({ canonical, view, schema }) };
4192
+ })
2542
4193
  ];
2543
4194
  return { namespace: canonical.namespace, schema, statements, snapshot };
2544
4195
  }
4196
+ function validateViewCatalogStatement({
4197
+ canonical,
4198
+ view,
4199
+ schema
4200
+ }) {
4201
+ const qualified = `${schema}.${view.view}`;
4202
+ const expectedColumns = sqlTextArray(view.columns.map((column) => column.columnName));
4203
+ const expectedTypes = `ARRAY[${view.columns.map((column) => `${viewTypeRegtype(column, schema)}::oid`).join(", ")}]::oid[]`;
4204
+ const relationIdentity = /* @__PURE__ */ new Map();
4205
+ for (const table of canonical.tables) {
4206
+ relationIdentity.set(table.table, `${schema}.${table.table}`);
4207
+ }
4208
+ for (const candidate of canonical.views ?? []) {
4209
+ relationIdentity.set(candidate.view, `${schema}.${candidate.view}`);
4210
+ }
4211
+ const expectedDependencies = sqlTextArray(
4212
+ view.dependencies.map((dependency) => {
4213
+ const physical = relationIdentity.get(dependency);
4214
+ if (!physical) throw new OrmDefinitionError(`view '${view.view}' depends on unknown relation '${dependency}'`);
4215
+ return physical;
4216
+ }).sort()
4217
+ );
4218
+ const allowedFunctions = sqlTextArray(VIEW_QUERY_FUNCTION_ALLOWLIST);
4219
+ return `DO $sequence_orm_validate_view$
4220
+ DECLARE
4221
+ actual_columns text[];
4222
+ actual_types oid[];
4223
+ actual_dependencies text[];
4224
+ BEGIN
4225
+ SELECT
4226
+ array_agg(attribute.attname ORDER BY attribute.attnum),
4227
+ array_agg(attribute.atttypid ORDER BY attribute.attnum)
4228
+ INTO actual_columns, actual_types
4229
+ FROM pg_catalog.pg_attribute AS attribute
4230
+ WHERE attribute.attrelid = pg_catalog.to_regclass(${quoteLiteral(qualified)})
4231
+ AND attribute.attnum > 0
4232
+ AND NOT attribute.attisdropped;
4233
+
4234
+ IF actual_columns IS DISTINCT FROM ${expectedColumns} THEN
4235
+ RAISE EXCEPTION 'managed view ${qualified} columns do not match its declaration: expected %, got %', ${expectedColumns}, actual_columns
4236
+ USING ERRCODE = '42804';
4237
+ END IF;
4238
+ IF actual_types IS DISTINCT FROM ${expectedTypes} THEN
4239
+ RAISE EXCEPTION 'managed view ${qualified} types do not match its declaration: expected %, got %', ${expectedTypes}, actual_types
4240
+ USING ERRCODE = '42804';
4241
+ END IF;
4242
+
4243
+ SELECT coalesce(array_agg(found.name ORDER BY found.name), ARRAY[]::text[])
4244
+ INTO actual_dependencies
4245
+ FROM (
4246
+ SELECT DISTINCT dependency_namespace.nspname || '.' || dependency_relation.relname AS name
4247
+ FROM pg_catalog.pg_rewrite AS rewrite
4248
+ JOIN pg_catalog.pg_depend AS dependency
4249
+ ON dependency.classid = 'pg_catalog.pg_rewrite'::regclass
4250
+ AND dependency.objid = rewrite.oid
4251
+ AND dependency.refclassid = 'pg_catalog.pg_class'::regclass
4252
+ JOIN pg_catalog.pg_class AS dependency_relation ON dependency_relation.oid = dependency.refobjid
4253
+ JOIN pg_catalog.pg_namespace AS dependency_namespace ON dependency_namespace.oid = dependency_relation.relnamespace
4254
+ WHERE rewrite.ev_class = pg_catalog.to_regclass(${quoteLiteral(qualified)})
4255
+ AND dependency.refobjid <> rewrite.ev_class
4256
+ AND dependency_relation.relkind IN ('r', 'p', 'v', 'm', 'f')
4257
+ ) AS found;
4258
+
4259
+ IF actual_dependencies IS DISTINCT FROM ${expectedDependencies} THEN
4260
+ RAISE EXCEPTION 'managed view ${qualified} dependencies do not match its declaration: expected %, got %', ${expectedDependencies}, actual_dependencies
4261
+ USING ERRCODE = '2BP01';
4262
+ END IF;
4263
+
4264
+ IF EXISTS (
4265
+ SELECT 1
4266
+ FROM pg_catalog.pg_rewrite AS rewrite
4267
+ JOIN pg_catalog.pg_depend AS dependency
4268
+ ON dependency.classid = 'pg_catalog.pg_rewrite'::regclass
4269
+ AND dependency.objid = rewrite.oid
4270
+ AND dependency.refclassid = 'pg_catalog.pg_proc'::regclass
4271
+ JOIN pg_catalog.pg_proc AS dependency_function ON dependency_function.oid = dependency.refobjid
4272
+ JOIN pg_catalog.pg_namespace AS function_namespace ON function_namespace.oid = dependency_function.pronamespace
4273
+ WHERE rewrite.ev_class = pg_catalog.to_regclass(${quoteLiteral(qualified)})
4274
+ AND (
4275
+ function_namespace.nspname <> 'pg_catalog'
4276
+ OR dependency_function.proname <> ALL (${allowedFunctions})
4277
+ )
4278
+ ) THEN
4279
+ RAISE EXCEPTION 'managed view ${qualified} may only call explicitly allowlisted side-effect-free pg_catalog functions'
4280
+ USING ERRCODE = '42501';
4281
+ END IF;
4282
+
4283
+ IF EXISTS (
4284
+ SELECT 1
4285
+ FROM pg_catalog.pg_rewrite AS rewrite
4286
+ JOIN pg_catalog.pg_depend AS dependency
4287
+ ON dependency.classid = 'pg_catalog.pg_rewrite'::regclass
4288
+ AND dependency.objid = rewrite.oid
4289
+ AND dependency.refclassid = 'pg_catalog.pg_operator'::regclass
4290
+ JOIN pg_catalog.pg_operator AS dependency_operator ON dependency_operator.oid = dependency.refobjid
4291
+ JOIN pg_catalog.pg_namespace AS operator_namespace ON operator_namespace.oid = dependency_operator.oprnamespace
4292
+ WHERE rewrite.ev_class = pg_catalog.to_regclass(${quoteLiteral(qualified)})
4293
+ AND operator_namespace.nspname <> 'pg_catalog'
4294
+ ) THEN
4295
+ RAISE EXCEPTION 'managed view ${qualified} may only use pg_catalog operators'
4296
+ USING ERRCODE = '42501';
4297
+ END IF;
4298
+ END
4299
+ $sequence_orm_validate_view$`;
4300
+ }
4301
+ function sqlTextArray(values) {
4302
+ return values.length === 0 ? "ARRAY[]::text[]" : `ARRAY[${values.map((value) => quoteLiteral(value)).join(", ")}]::text[]`;
4303
+ }
4304
+ function viewTypeRegtype(column, schema) {
4305
+ if (typeof column.type === "string") return `${quoteLiteral(sqlTypeName(column.type))}::regtype`;
4306
+ return `${quoteLiteral(`${quoteIdentifier(schema)}.${quoteIdentifier(column.type.enum)}`)}::regtype`;
4307
+ }
4308
+ var previousId = (snapshot) => snapshot.id;
4309
+ function snapshotViews(snapshot) {
4310
+ const views = snapshot.views ?? {};
4311
+ return new Map(Object.values(views).map((view) => [view.name, view]));
4312
+ }
4313
+ function snapshotViewDependencies(snapshot) {
4314
+ const metadata = snapshot._sequenceOrm;
4315
+ return metadata?.viewDependencies ?? {};
4316
+ }
4317
+ function withoutViews(snapshot) {
4318
+ const clone = structuredClone(snapshot);
4319
+ clone.views = {};
4320
+ delete clone._sequenceOrm;
4321
+ return clone;
4322
+ }
4323
+ function affectedViewNames({
4324
+ previousViews,
4325
+ currentViews,
4326
+ previousDependencies,
4327
+ currentDependencies,
4328
+ baseChanged
4329
+ }) {
4330
+ const allNames = /* @__PURE__ */ new Set([...previousViews.keys(), ...currentViews.keys()]);
4331
+ if (baseChanged) return allNames;
4332
+ const affected = new Set(
4333
+ [...allNames].filter((name) => {
4334
+ const before = previousViews.get(name);
4335
+ const after = currentViews.get(name);
4336
+ return before === void 0 || after === void 0 || stableJson(before) !== stableJson(after);
4337
+ })
4338
+ );
4339
+ let grew = true;
4340
+ while (grew) {
4341
+ grew = false;
4342
+ for (const name of allNames) {
4343
+ if (affected.has(name)) continue;
4344
+ const dependencies = /* @__PURE__ */ new Set([...previousDependencies[name] ?? [], ...currentDependencies[name] ?? []]);
4345
+ if ([...dependencies].some((dependency) => affected.has(dependency))) {
4346
+ affected.add(name);
4347
+ grew = true;
4348
+ }
4349
+ }
4350
+ }
4351
+ return affected;
4352
+ }
4353
+ function topologicalViewNames(names, dependencies) {
4354
+ const remaining = new Set(names);
4355
+ const ordered = [];
4356
+ while (remaining.size > 0) {
4357
+ const ready = [...remaining].filter((name) => (dependencies[name] ?? []).every((dependency) => !remaining.has(dependency))).sort();
4358
+ if (ready.length === 0) {
4359
+ throw new OrmDefinitionError(`view dependencies form a cycle among: ${[...remaining].sort().join(", ")}`);
4360
+ }
4361
+ for (const name of ready) {
4362
+ ordered.push(name);
4363
+ remaining.delete(name);
4364
+ }
4365
+ }
4366
+ return ordered;
4367
+ }
4368
+ async function generatedViewStatements({
4369
+ snapshot,
4370
+ names,
4371
+ mode,
4372
+ schema
4373
+ }) {
4374
+ if (names.size === 0) return /* @__PURE__ */ new Map();
4375
+ const without = structuredClone(snapshot);
4376
+ const holder = without;
4377
+ const views = holder.views ?? {};
4378
+ holder.views = Object.fromEntries(Object.entries(views).filter(([, view]) => !names.has(view.name)));
4379
+ const generated = mode === "drop" ? await diffStatements(snapshot, without) : await diffStatements(without, snapshot);
4380
+ const statements = /* @__PURE__ */ new Map();
4381
+ for (const name of names) {
4382
+ const qualified = `${quoteIdentifier(schema)}.${quoteIdentifier(name)}`;
4383
+ const prefix = mode === "drop" ? "DROP VIEW" : "CREATE VIEW";
4384
+ const statement = generated.find((candidate) => candidate.trimStart().startsWith(`${prefix} ${qualified}`));
4385
+ if (statement) statements.set(name, statement.replace(/;\s*$/, ""));
4386
+ }
4387
+ return statements;
4388
+ }
4389
+ function requiredViewStatement(statements, name, schema, operation) {
4390
+ const statement = statements.get(name);
4391
+ if (!statement) {
4392
+ throw new OrmDefinitionError(`drizzle-kit did not generate ${operation} VIEW for '${schema}.${name}'`);
4393
+ }
4394
+ return statement;
4395
+ }
4396
+ function assertValidViewDependencies(canonical) {
4397
+ const views = canonical.views ?? [];
4398
+ const known = /* @__PURE__ */ new Set([...canonical.tables.map((table) => table.table), ...views.map((view) => view.view)]);
4399
+ for (const view of views) {
4400
+ for (const dependency of view.dependencies) {
4401
+ if (!known.has(dependency)) {
4402
+ throw new OrmDefinitionError(`view '${view.view}' depends on unknown relation '${dependency}'`);
4403
+ }
4404
+ }
4405
+ }
4406
+ topologicalViewNames(
4407
+ new Set(views.map((view) => view.view)),
4408
+ Object.fromEntries(views.map((view) => [view.view, view.dependencies]))
4409
+ );
4410
+ }
4411
+
4412
+ // src/engine/converge.ts
4413
+ var OWNED_STATEMENT = new RegExp(
4414
+ [
4415
+ String.raw`^\s*(CREATE|DROP)\s+POLICY\b`,
4416
+ String.raw`^\s*(GRANT|REVOKE)\b`,
4417
+ String.raw`^\s*(CREATE|DROP|ALTER)\s+ROLE\b`,
4418
+ String.raw`^\s*(CREATE|DROP)\s+(OR\s+REPLACE\s+)?VIEW\b`,
4419
+ // Only ever appears in an RLS toggle (ALTER TABLE … [ENABLE|FORCE] ROW LEVEL
4420
+ // SECURITY) — a three-word phrase, never an identifier.
4421
+ String.raw`\bROW LEVEL SECURITY\b`
4422
+ ].join("|"),
4423
+ "i"
4424
+ );
4425
+ var V2_OWNED_STATEMENT = new RegExp(
4426
+ [
4427
+ String.raw`^\s*(CREATE|DROP)\s+POLICY\b`,
4428
+ String.raw`^\s*(GRANT|REVOKE)\b`,
4429
+ String.raw`^\s*(CREATE|DROP|ALTER)\s+ROLE\b`,
4430
+ String.raw`\bROW LEVEL SECURITY\b`
4431
+ ].join("|"),
4432
+ "i"
4433
+ );
4434
+ function isOwnedStatement(statement) {
4435
+ return OWNED_STATEMENT.test(statement);
4436
+ }
4437
+ function isOwnedStatementV2(statement) {
4438
+ return V2_OWNED_STATEMENT.test(statement);
4439
+ }
4440
+ async function pendingKitStatements({
4441
+ definition,
4442
+ db
4443
+ }) {
4444
+ return pendingStatements({ entities: buildDrizzleEntities(definition), db, owned: OWNED_STATEMENT });
4445
+ }
4446
+ async function pendingKitStatementsV2({
4447
+ canonical,
4448
+ db
4449
+ }) {
4450
+ return pendingStatements({ entities: buildDrizzleEntitiesV2(canonical), db, owned: V2_OWNED_STATEMENT });
4451
+ }
4452
+ async function pendingStatements({
4453
+ entities,
4454
+ db,
4455
+ owned
4456
+ }) {
4457
+ const { imports, schemaName } = entities;
4458
+ const { statementsToExecute } = await withExitGuard(
4459
+ () => api.pushSchema(imports, shimExecute(db), [schemaName])
4460
+ );
4461
+ return statementsToExecute.filter((statement) => !owned.test(statement));
4462
+ }
4463
+ function shimExecute(db) {
4464
+ const execute = db.execute.bind(db);
4465
+ return {
4466
+ execute: async (query) => {
4467
+ const result = await execute(query);
4468
+ if (result && typeof result === "object" && "rows" in result) return result;
4469
+ return { rows: result };
4470
+ }
4471
+ };
4472
+ }
4473
+ var exitGuardChain = Promise.resolve();
4474
+ function withExitGuard(run) {
4475
+ const next = exitGuardChain.then(() => guardedRun(run));
4476
+ exitGuardChain = next.then(
4477
+ () => void 0,
4478
+ () => void 0
4479
+ );
4480
+ return next;
4481
+ }
4482
+ async function guardedRun(run) {
4483
+ const originalExit = process.exit;
4484
+ process.exit = ((code) => {
4485
+ throw new Error(`drizzle-kit attempted process.exit(${code ?? 0}) during pushSchema introspection`);
4486
+ });
4487
+ try {
4488
+ return await run();
4489
+ } finally {
4490
+ process.exit = originalExit;
4491
+ }
4492
+ }
4493
+
4494
+ // src/engine/index.ts
4495
+ var STATEMENT_KINDS = ["hint", "ddl", "security", "function", "synced_view"];
4496
+ async function planMigration({
4497
+ definition,
4498
+ previousSnapshot
4499
+ }) {
4500
+ const schema = ormSchemaName(definition.namespace);
4501
+ const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(definition.namespace);
4502
+ const hints = applyMigrationHints(previous, definition);
4503
+ const snapshot = generateSnapshot(definition, previousId2(previous));
4504
+ const ddl = await diffStatements(hints.adjusted, snapshot);
4505
+ const policyDrops = policyDropStatements(schema, snapshotTableNames(previous, schema));
4506
+ const statements = [
4507
+ ...policyDrops.map((sql4) => ({ kind: "security", sql: sql4 })),
4508
+ ...hints.statements.map((sql4) => ({ kind: "hint", sql: sql4 })),
4509
+ ...ddl.map((sql4) => ({ kind: "ddl", sql: sql4.replace(/;\s*$/, "") })),
4510
+ ...securityStatements(definition).map((sql4) => ({ kind: "security", sql: sql4 })),
4511
+ // PREPARE each action's executes first (plpgsql bodies don't check columns
4512
+ // at CREATE), then create the functions. Both reference the tables the DDL
4513
+ // above creates, so they close the plan.
4514
+ ...actionPrepareChecks(definition).map((sql4) => ({ kind: "function", sql: sql4 })),
4515
+ ...actionFunctionStatements(definition).map((sql4) => ({ kind: "function", sql: sql4 }))
4516
+ ];
4517
+ return { namespace: definition.namespace, schema, statements, snapshot };
4518
+ }
4519
+ var compileNamespace = (definition) => planMigration({ definition });
2545
4520
  var previousId2 = (snapshot) => snapshot.id;
2546
4521
  var IR_VERSION = 1;
2547
4522
  var CLAIMS_CONTRACT = {
@@ -2680,9 +4655,23 @@ var onConflictIrSchema = zod.z.union([
2680
4655
  ]);
2681
4656
  var mutationRootSchema = zod.z.object({
2682
4657
  kind: zod.z.literal("mutation"),
2683
- op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk"]),
4658
+ /**
4659
+ * CRUD verbs plus the structured copy-on-write ops. `cowOverride` copies the
4660
+ * synced row into the overlay (INSERT…SELECT, ON CONFLICT DO NOTHING) then
4661
+ * applies `set` — both statements generated by the executor from this
4662
+ * metadata, never authored SQL. `cowRevert` deletes the override so the
4663
+ * coalesce view falls back to synced. Both are gated on a matching synced
4664
+ * row and carry the write policy in `where` like update/delete.
4665
+ */
4666
+ op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
2684
4667
  alias: zod.z.string(),
2685
4668
  table: zod.z.string(),
4669
+ /** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
4670
+ cowSynced: zod.z.string().optional(),
4671
+ /** cowOverride: logical properties copied INSERT…SELECT from synced (pk included). */
4672
+ cowCopyColumns: zod.z.array(zod.z.string()).optional(),
4673
+ /** cowOverride: insert-policy WITH CHECK evaluated over the copied rows. */
4674
+ cowCopyCheck: predIrSchema.optional(),
2686
4675
  /** insert/insertOne: rows come from a slot ($object/$objects) or literals. */
2687
4676
  objects: zod.z.union([
2688
4677
  zod.z.object({ slot: zod.z.string() }),
@@ -2703,19 +4692,11 @@ var mutationRootSchema = zod.z.object({
2703
4692
  affectedRows: zod.z.boolean().default(false),
2704
4693
  includeIf: includeIfSchema.optional()
2705
4694
  });
2706
- var actionRootSchema = zod.z.object({
2707
- kind: zod.z.literal("action"),
2708
- alias: zod.z.string(),
2709
- name: zod.z.string(),
2710
- params: zod.z.array(zod.z.object({ name: zod.z.string(), value: valueIrSchema })),
2711
- returns: zod.z.enum(["rows", "rowsAffected", "scalar"])
2712
- });
2713
4695
  var rootIrSchema = zod.z.discriminatedUnion("kind", [
2714
4696
  selectRootSchema,
2715
4697
  byPkRootSchema,
2716
4698
  aggregateRootSchema,
2717
- mutationRootSchema,
2718
- actionRootSchema
4699
+ mutationRootSchema
2719
4700
  ]);
2720
4701
  var INPUT_REF_KINDS = [
2721
4702
  "boolExp",
@@ -2747,7 +4728,7 @@ var compiledOperationSchema = zod.z.object({
2747
4728
  irVersion: zod.z.literal(IR_VERSION),
2748
4729
  name: zod.z.string(),
2749
4730
  opHash: zod.z.string().regex(/^sha256:[0-9a-f]{64}$/),
2750
- kind: zod.z.enum(["query", "mutation", "action"]),
4731
+ kind: zod.z.enum(["query", "mutation"]),
2751
4732
  requires: zod.z.object({
2752
4733
  capability: zod.z.string().optional(),
2753
4734
  role: zod.z.string().optional()
@@ -2761,13 +4742,24 @@ var tableMetaIrSchema = zod.z.object({
2761
4742
  table: zod.z.string(),
2762
4743
  schema: zod.z.string(),
2763
4744
  typename: zod.z.string(),
4745
+ /**
4746
+ * True when statements over this relation evaluate a claims-gated synced
4747
+ * view: synced twins (`source: gold.*`) materialize as views whose baked
4748
+ * policy reads `current_setting('request.jwt.claims')`, and namespace views
4749
+ * inherit the gate transitively through their declared dependencies. The
4750
+ * executor must stamp that GUC (transaction-scoped) before any statement
4751
+ * touching such a relation, or the synced arm yields zero rows regardless
4752
+ * of the compiled predicates.
4753
+ */
4754
+ claimsGated: zod.z.boolean().default(false),
2764
4755
  columns: zod.z.array(zod.z.object({
2765
4756
  name: zod.z.string(),
2766
4757
  columnName: zod.z.string(),
2767
4758
  type: zod.z.union([zod.z.enum(COLUMN_TYPES), zod.z.object({ enum: zod.z.string(), values: zod.z.array(zod.z.string()) })]),
2768
4759
  nullable: zod.z.boolean()
2769
4760
  })),
2770
- primaryKey: zod.z.array(zod.z.string()).min(1),
4761
+ /** Empty for keyless read-only views; keyed tables and views list logical columns. */
4762
+ primaryKey: zod.z.array(zod.z.string()),
2771
4763
  links: zod.z.array(zod.z.object({
2772
4764
  name: zod.z.string(),
2773
4765
  kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
@@ -2934,6 +4926,8 @@ function validateCapabilityRefs(files, declared) {
2934
4926
  const referenced = [
2935
4927
  ...table.policies?.read ? referencedCapabilities(table.policies.read.ast) : [],
2936
4928
  ...table.policies?.write ? referencedCapabilities(table.policies.write.ast) : [],
4929
+ ...table.policies?.insert ? referencedCapabilities(table.policies.insert.ast) : [],
4930
+ ...table.policies?.update ? referencedCapabilities(table.policies.update.ast) : [],
2937
4931
  ...Object.values(table.actions ?? {}).flatMap((action) => [
2938
4932
  ...action.requires ? referencedCapabilities(action.requires.ast) : [],
2939
4933
  ...referencedExecuteCapabilities(action)
@@ -3091,7 +5085,7 @@ async function readMigrationSql(dir, entry) {
3091
5085
  throw new OrmDefinitionError("missing migration file", path$1);
3092
5086
  });
3093
5087
  }
3094
- var checksumOf = (text3) => crypto.createHash("sha256").update(text3).digest("hex");
5088
+ var checksumOf = (text4) => crypto.createHash("sha256").update(text4).digest("hex");
3095
5089
  var migrationName = (ordinal, contentHash) => `${pad(ordinal)}_${contentHash.slice(0, 12)}`;
3096
5090
  function renderMigrationSql({
3097
5091
  name,
@@ -3117,12 +5111,12 @@ async function writeMigration({
3117
5111
  const tip = workspace.journal.entries.at(-1);
3118
5112
  const ordinal = (tip?.ordinal ?? 0) + 1;
3119
5113
  const name = migrationName(ordinal, contentHash);
3120
- const sql3 = renderMigrationSql({ name, namespace: workspace.journal.namespace, contentHash, plan });
5114
+ const sql4 = renderMigrationSql({ name, namespace: workspace.journal.namespace, contentHash, plan });
3121
5115
  const metaDir = path.join(dir, MIGRATIONS_DIRNAME, META_DIRNAME);
3122
5116
  await promises.mkdir(metaDir, { recursive: true });
3123
5117
  const snapshotText = `${JSON.stringify(plan.snapshot, null, 2)}
3124
5118
  `;
3125
- await promises.writeFile(path.join(dir, MIGRATIONS_DIRNAME, `${name}.sql`), sql3);
5119
+ await promises.writeFile(path.join(dir, MIGRATIONS_DIRNAME, `${name}.sql`), sql4);
3126
5120
  await promises.writeFile(path.join(metaDir, `${pad(ordinal)}.snapshot.json`), snapshotText);
3127
5121
  const journal = {
3128
5122
  ...workspace.journal,
@@ -3133,7 +5127,7 @@ async function writeMigration({
3133
5127
  name,
3134
5128
  content_hash: contentHash,
3135
5129
  previous_content_hash: tip?.content_hash ?? null,
3136
- checksum: checksumOf(sql3),
5130
+ checksum: checksumOf(sql4),
3137
5131
  snapshot_checksum: checksumOf(snapshotText),
3138
5132
  destructive
3139
5133
  }
@@ -3248,8 +5242,8 @@ async function checkWorkspace({
3248
5242
  let previousId3;
3249
5243
  let previousHash = null;
3250
5244
  for (const entry of workspace.journal.entries) {
3251
- const sql3 = await readMigrationSql(dir, entry);
3252
- if (checksumOf(sql3) !== entry.checksum) {
5245
+ const sql4 = await readMigrationSql(dir, entry);
5246
+ if (checksumOf(sql4) !== entry.checksum) {
3253
5247
  log(`${MIGRATIONS_DIRNAME}/${entry.name}.sql does not match its journal checksum \u2014 committed migrations must not be edited`);
3254
5248
  return 1;
3255
5249
  }
@@ -3289,7 +5283,7 @@ async function checkWorkspace({
3289
5283
  log(`migrations in sync \u2014 ${workspace.journal.entries.length} committed, tip ${tip.name}`);
3290
5284
  return 0;
3291
5285
  }
3292
- var firstLine = (sql3) => sql3.split("\n")[0] ?? sql3;
5286
+ var firstLine = (sql4) => sql4.split("\n")[0] ?? sql4;
3293
5287
 
3294
5288
  // src/migrations/from-yaml.ts
3295
5289
  function convertV1DefinitionToCanonical(definition) {
@@ -3321,7 +5315,7 @@ function convertTable(table) {
3321
5315
  description: table.description,
3322
5316
  source: table.source,
3323
5317
  columns: columns.map(([name, column]) => convertColumn(name, column)),
3324
- // Synced tables reflect their source — no columns, no primary key.
5318
+ // Synced tables may declare a typed column/PK contract; untyped remain empty.
3325
5319
  primaryKey: primaryKey2.length > 0 ? primaryKey2 : void 0,
3326
5320
  links: Object.entries(table.links ?? {}).map(([name, link]) => convertLink(table.table, name, link)),
3327
5321
  indexes: (table.indexes ?? []).map((index3) => ({
@@ -3334,7 +5328,10 @@ function convertTable(table) {
3334
5328
  read: table.policies?.read?.source,
3335
5329
  write: table.policies?.write?.source
3336
5330
  },
3337
- actions: Object.entries(table.actions ?? {}).map(([name, action]) => convertAction(table.table, name, action))
5331
+ // v2 has no action surface (writes are mutations); the transition sweep
5332
+ // drops the plpgsql action_* functions, so carrying the metadata would be
5333
+ // dead weight. droppedV1Actions() surfaces them for the emitter's TODO.
5334
+ actions: []
3338
5335
  };
3339
5336
  }
3340
5337
  function convertColumn(name, column) {
@@ -3370,25 +5367,6 @@ function convertLink(table, name, link) {
3370
5367
  description: link.description
3371
5368
  };
3372
5369
  }
3373
- function convertAction(table, name, action) {
3374
- return {
3375
- name,
3376
- requires: action.requires?.source,
3377
- params: Object.fromEntries(
3378
- Object.entries(action.params ?? {}).map(([param, definition]) => [
3379
- param,
3380
- { type: definition.type, nullable: definition.optional === true }
3381
- ])
3382
- ),
3383
- // The v2 contract carries one SQL string; statement boundaries survive as
3384
- // `;\n` (v1 validation guarantees no bare `;` inside a single statement).
3385
- executes: actionStatementSources(action.executes).join(";\n"),
3386
- // v1 declared the RETURNING columns inline; v2 types them at compile time,
3387
- // so the shape collapses to rows-vs-rowsAffected.
3388
- returns: action.returns ? "rows" : "rowsAffected",
3389
- audit: action.audit ? { action: action.audit.action, resource: action.audit.resource ?? table } : void 0
3390
- };
3391
- }
3392
5370
 
3393
5371
  // src/migrations/v2.ts
3394
5372
  var SHA256_HEX = /^[0-9a-f]{64}$/;
@@ -3411,7 +5389,7 @@ function buildRebaseJournalEntry({ namespace, previousHash, newHash, ordinal, sn
3411
5389
  throw new OrmDefinitionError(`rebase ordinal must be an integer \u2265 2 (a rebase sits on an existing journal tip); got ${ordinal}`);
3412
5390
  }
3413
5391
  const name = migrationName(ordinal, newHash);
3414
- const sql3 = renderRebaseMigrationSql({ name, namespace, previousHash, newHash });
5392
+ const sql4 = renderRebaseMigrationSql({ name, namespace, previousHash, newHash });
3415
5393
  const snapshotText = `${JSON.stringify(snapshot, null, 2)}
3416
5394
  `;
3417
5395
  const entry = {
@@ -3419,11 +5397,11 @@ function buildRebaseJournalEntry({ namespace, previousHash, newHash, ordinal, sn
3419
5397
  name,
3420
5398
  content_hash: newHash,
3421
5399
  previous_content_hash: previousHash,
3422
- checksum: checksumOf(sql3),
5400
+ checksum: checksumOf(sql4),
3423
5401
  snapshot_checksum: checksumOf(snapshotText),
3424
5402
  destructive: false
3425
5403
  };
3426
- return { name, entry, sql: sql3, snapshotText };
5404
+ return { name, entry, sql: sql4, snapshotText };
3427
5405
  }
3428
5406
  function renderRebaseMigrationSql({
3429
5407
  name,
@@ -3496,8 +5474,9 @@ function canonicalPolicies(value) {
3496
5474
  if (value === null || typeof value !== "object") return void 0;
3497
5475
  const record = value;
3498
5476
  const policies = {};
3499
- if (typeof record["read"] === "string") policies["read"] = record["read"];
3500
- if (typeof record["write"] === "string") policies["write"] = record["write"];
5477
+ for (const kind of ["read", "write", "insert", "update"]) {
5478
+ if (typeof record[kind] === "string") policies[kind] = record[kind];
5479
+ }
3501
5480
  return Object.keys(policies).length > 0 ? policies : void 0;
3502
5481
  }
3503
5482
  function canonicalActions(value) {
@@ -3575,15 +5554,20 @@ exports.NAMESPACE_PATTERN = NAMESPACE_PATTERN;
3575
5554
  exports.ORM_DATA_TOKEN_HEADER = ORM_DATA_TOKEN_HEADER;
3576
5555
  exports.ORM_SCHEMA_PREFIX = ORM_SCHEMA_PREFIX;
3577
5556
  exports.ORM_SERVICE_DATA_TOKEN_HEADER = ORM_SERVICE_DATA_TOKEN_HEADER;
5557
+ exports.ORM_TO_PIPELINE_TYPE = ORM_TO_PIPELINE_TYPE;
3578
5558
  exports.OrmDefinitionError = OrmDefinitionError;
3579
5559
  exports.PredicateParseError = PredicateParseError;
3580
5560
  exports.QUALIFIED_TABLE_PATTERN = QUALIFIED_TABLE_PATTERN;
3581
5561
  exports.RESERVED_COLUMN_NAMES = RESERVED_COLUMN_NAMES;
3582
5562
  exports.RESERVED_TABLE_NAMES = RESERVED_TABLE_NAMES;
5563
+ exports.ROLES_CLAIM_KEY = ROLES_CLAIM_KEY;
3583
5564
  exports.SCHEMA_DIR = SCHEMA_DIR;
3584
5565
  exports.STATEMENT_KINDS = STATEMENT_KINDS;
5566
+ exports.SYNCED_CONTRACT_HASH_DOMAIN = SYNCED_CONTRACT_HASH_DOMAIN;
3585
5567
  exports.SYNCED_SOURCE_PATTERN = SYNCED_SOURCE_PATTERN;
5568
+ exports.SYNCED_VIEW_EXECUTION_OWNER_ROLE = SYNCED_VIEW_EXECUTION_OWNER_ROLE;
3586
5569
  exports.TABLE_BRAND = TABLE_BRAND;
5570
+ exports.VIEW_BRAND = VIEW_BRAND;
3587
5571
  exports.actionFunctionName = actionFunctionName;
3588
5572
  exports.actionFunctionStatements = actionFunctionStatements;
3589
5573
  exports.actionParamArg = actionParamArg;
@@ -3609,7 +5593,9 @@ exports.canonicalEnumSchema = canonicalEnumSchema;
3609
5593
  exports.canonicalIndexSchema = canonicalIndexSchema;
3610
5594
  exports.canonicalLinkSchema = canonicalLinkSchema;
3611
5595
  exports.canonicalPoliciesSchema = canonicalPoliciesSchema;
5596
+ exports.canonicalRelations = canonicalRelations;
3612
5597
  exports.canonicalTableSchema = canonicalTableSchema;
5598
+ exports.canonicalViewSchema = canonicalViewSchema;
3613
5599
  exports.canonicalizeDefinition = canonicalizeDefinition;
3614
5600
  exports.capabilityExpression = capabilityExpression;
3615
5601
  exports.castParts = castParts;
@@ -3621,11 +5607,13 @@ exports.columnTypeSchema = columnTypeSchema2;
3621
5607
  exports.compileActionRequires = compileActionRequires;
3622
5608
  exports.compileActionStatements = compileActionStatements;
3623
5609
  exports.compileNamespace = compileNamespace;
5610
+ exports.compilePredIrToSql = compilePredIrToSql;
3624
5611
  exports.compilePredicate = compilePredicate;
3625
5612
  exports.compiledArtifactSchema = compiledArtifactSchema;
3626
5613
  exports.compiledOperationSchema = compiledOperationSchema;
3627
5614
  exports.computeCanonicalHash = computeCanonicalHash;
3628
5615
  exports.computeDefinitionHash = computeDefinitionHash;
5616
+ exports.computeSyncedContractHash = computeSyncedContractHash;
3629
5617
  exports.consumerRoleName = consumerRoleName;
3630
5618
  exports.convertV1DefinitionToCanonical = convertV1DefinitionToCanonical;
3631
5619
  exports.declaredCapabilityKeys = declaredCapabilityKeys;
@@ -3633,6 +5621,7 @@ exports.defineEnum = defineEnum;
3633
5621
  exports.defineNamespace = defineNamespace;
3634
5622
  exports.defineSyncedTable = defineSyncedTable;
3635
5623
  exports.defineTable = defineTable;
5624
+ exports.defineView = defineView;
3636
5625
  exports.deriveColumnName = deriveColumnName;
3637
5626
  exports.diffCommand = diffCommand;
3638
5627
  exports.diffStatements = diffStatements;
@@ -3640,6 +5629,7 @@ exports.emptyJournal = emptyJournal;
3640
5629
  exports.emptySnapshot = emptySnapshot;
3641
5630
  exports.endAuthoringContext = endAuthoringContext;
3642
5631
  exports.extractNamedParams = extractNamedParams;
5632
+ exports.extractOrmSyncedTableContract = extractOrmSyncedTableContract;
3643
5633
  exports.generateNamespaceTypes = generateNamespaceTypes;
3644
5634
  exports.generateSnapshot = generateSnapshot;
3645
5635
  exports.generateSnapshotV2 = generateSnapshotV2;
@@ -3649,10 +5639,12 @@ exports.isColumnBuilder = isColumnBuilder;
3649
5639
  exports.isEnumRef = isEnumRef;
3650
5640
  exports.isFunctionDefault = isFunctionDefault;
3651
5641
  exports.isOwnedStatement = isOwnedStatement;
5642
+ exports.isOwnedStatementV2 = isOwnedStatementV2;
3652
5643
  exports.isQualifiedReference = isQualifiedReference;
3653
5644
  exports.isSeqLink = isSeqLink;
3654
5645
  exports.isSeqNamespaceConfig = isSeqNamespaceConfig;
3655
5646
  exports.isSeqTable = isSeqTable;
5647
+ exports.isSeqView = isSeqView;
3656
5648
  exports.limitIrSchema = limitIrSchema;
3657
5649
  exports.loadCommittedMigrations = loadCommittedMigrations;
3658
5650
  exports.loadCommittedTipMigration = loadCommittedTipMigration;
@@ -3666,12 +5658,15 @@ exports.normalizeIr = normalizeIr;
3666
5658
  exports.normalizeSnapshot = normalizeSnapshot;
3667
5659
  exports.orderIrSchema = orderIrSchema;
3668
5660
  exports.ormSchemaName = ormSchemaName;
5661
+ exports.overlayWinsUnion = overlayWinsUnion;
5662
+ exports.overrideRowMutation = overrideRowMutation;
3669
5663
  exports.parseCanonicalDefinition = parseCanonicalDefinition;
3670
5664
  exports.parseDefinitionSources = parseDefinitionSources;
3671
5665
  exports.parsePredicate = parsePredicate;
3672
5666
  exports.parseTableFile = parseTableFile;
3673
5667
  exports.parsedPredicateSchema = parsedPredicateSchema;
3674
5668
  exports.pendingKitStatements = pendingKitStatements;
5669
+ exports.pendingKitStatementsV2 = pendingKitStatementsV2;
3675
5670
  exports.planMigration = planMigration;
3676
5671
  exports.planMigrationV2 = planMigrationV2;
3677
5672
  exports.policyExpressionSchema = policyExpressionSchema;
@@ -3687,12 +5682,20 @@ exports.rebaseViolations = rebaseViolations;
3687
5682
  exports.referencedCapabilities = referencedCapabilities;
3688
5683
  exports.referencedColumns = referencedColumns;
3689
5684
  exports.renderMigrationSql = renderMigrationSql;
5685
+ exports.revertOverlayMutation = revertOverlayMutation;
5686
+ exports.rewriteSyncedSourceRelationsInViewQuery = rewriteSyncedSourceRelationsInViewQuery;
5687
+ exports.roleExpression = roleExpression;
3690
5688
  exports.rootIrSchema = rootIrSchema;
3691
5689
  exports.securityStatements = securityStatements;
3692
5690
  exports.selectionIrSchema = selectionIrSchema;
3693
5691
  exports.serializeNamespace = serializeNamespace;
3694
5692
  exports.sqlTypeName = sqlTypeName;
3695
5693
  exports.stableJson = stableJson;
5694
+ exports.syncedBackingRevokeStatements = syncedBackingRevokeStatements;
5695
+ exports.syncedContractSurface = syncedContractSurface;
5696
+ exports.syncedTables = syncedTables;
5697
+ exports.syncedViewDropStatements = syncedViewDropStatements;
5698
+ exports.syncedViewStatements = syncedViewStatements;
3696
5699
  exports.t = t;
3697
5700
  exports.tableFileSchema = tableFileSchema;
3698
5701
  exports.tableMetaIrSchema = tableMetaIrSchema;
@@ -3701,6 +5704,7 @@ exports.toMany = toMany;
3701
5704
  exports.toOne = toOne;
3702
5705
  exports.toPlatformUser = toPlatformUser;
3703
5706
  exports.typesInterfaceName = typesInterfaceName;
5707
+ exports.validateViewCatalogStatement = validateViewCatalogStatement;
3704
5708
  exports.valueIrSchema = valueIrSchema;
3705
5709
  exports.variableDefaultIrSchema = variableDefaultIrSchema;
3706
5710
  exports.variableIrSchema = variableIrSchema;