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