@sequenceholdings/orm 0.1.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +49 -24
  2. package/dist/authoring.cjs +1072 -60
  3. package/dist/authoring.cjs.map +1 -1
  4. package/dist/authoring.d.cts +291 -26
  5. package/dist/authoring.d.ts +291 -26
  6. package/dist/authoring.js +1062 -61
  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 +1860 -228
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.js +1861 -229
  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 +512 -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 +510 -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 +2229 -220
  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 +2205 -221
  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 +1099 -0
  53. package/dist/predicate-v2.cjs.map +1 -0
  54. package/dist/predicate-v2.d.cts +142 -0
  55. package/dist/predicate-v2.d.ts +142 -0
  56. package/dist/predicate-v2.js +1089 -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 +20 -3
  65. package/src/codegen-gql/preset.cjs +64 -0
  66. package/src/codegen-gql/server-fns.cjs +11 -6
  67. package/templates/orm-namespace/CLAUDE.md +6 -5
  68. package/templates/orm-namespace/codegen.preset.cjs +17 -0
  69. package/templates/orm-namespace/codegen.ts +1 -1
  70. package/templates/orm-namespace/package.json +3 -0
  71. package/templates/orm-namespace/schema/tables.ts +1 -1
  72. package/templates/orm-namespace/sequence.config.ts +1 -1
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`);
1769
+ }
1770
+ if (link.enforce && (!isSeqTable(relation) || !isSeqTable(target))) {
1771
+ return fail("cannot enforce a PostgreSQL foreign key involving a view");
722
1772
  }
723
- const owningSide = link.linkKind === "toOne" ? table : target;
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,21 +1793,36 @@ 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
758
1811
  );
759
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)))])
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
+ }
1825
+ }
760
1826
  function toCanonicalPolicy(policy) {
761
1827
  if (typeof policy === "string") return policy;
762
1828
  return {
@@ -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 = {}) {
@@ -791,7 +1955,12 @@ async function loadNamespaceFromDir(dir, options = {}) {
791
1955
  const jiti = createJiti(import.meta.url, {
792
1956
  interopDefault: true,
793
1957
  fsCache: false,
794
- ...options.ormModulePath !== void 0 ? { alias: { "@sequenceholdings/orm": options.ormModulePath } } : {}
1958
+ ...options.ormModulePath !== void 0 ? {
1959
+ alias: {
1960
+ "@sequenceholdings/orm": options.ormModulePath,
1961
+ "@sequenceholdings/orm/authoring": options.ormModulePath
1962
+ }
1963
+ } : {}
795
1964
  });
796
1965
  const context = beginAuthoringContext();
797
1966
  try {
@@ -832,16 +2001,20 @@ async function loadNamespaceFromDir(dir, options = {}) {
832
2001
  for (const entry of context.tables) {
833
2002
  if (isSeqTable(entry.value)) tables.push(entry.value);
834
2003
  }
835
- if (tables.length === 0) {
2004
+ const views = [];
2005
+ for (const entry of context.views) {
2006
+ if (isSeqView(entry.value)) views.push(entry.value);
2007
+ }
2008
+ if (tables.length === 0 && views.length === 0) {
836
2009
  throw new OrmDefinitionError(
837
- `no tables registered \u2014 ${SCHEMA_DIR}/*.ts modules must call defineTable(...) at import time`
2010
+ `no relations registered \u2014 ${SCHEMA_DIR}/*.ts modules must call defineTable(...) or defineView(...) at import time`
838
2011
  );
839
2012
  }
840
2013
  const enums = [];
841
2014
  for (const entry of context.enums) {
842
2015
  if (isEnumRef(entry.value)) enums.push(entry.value);
843
2016
  }
844
- const canonical = serializeNamespace({ config: configEntry.value, tables, enums });
2017
+ const canonical = serializeNamespace({ config: configEntry.value, tables, views, enums });
845
2018
  return { canonical, hash: computeCanonicalHash(canonical), files };
846
2019
  }
847
2020
  async function listSchemaFiles(root) {
@@ -879,13 +2052,17 @@ function generateNamespaceTypes(definition) {
879
2052
  `export type ${typesInterfaceName(definition.namespace)} = {`
880
2053
  ];
881
2054
  for (const table of definition.tables) {
882
- if (table.source) {
2055
+ const columns = Object.entries(table.columns ?? {});
2056
+ if (table.source && columns.length === 0) {
883
2057
  lines.push(` /** Synced from ${table.source} \u2014 columns reflect the gold source. */`);
884
2058
  lines.push(` ${table.table}: Record<string, unknown>`);
885
2059
  continue;
886
2060
  }
2061
+ if (table.source) {
2062
+ lines.push(` /** Synced from ${table.source} \u2014 read-only typed contract. */`);
2063
+ }
887
2064
  lines.push(` ${table.table}: {`);
888
- for (const [name, column] of Object.entries(table.columns ?? {})) {
2065
+ for (const [name, column] of columns) {
889
2066
  if (column.deprecated) continue;
890
2067
  lines.push(` ${name}: ${columnWireType(column)}`);
891
2068
  }
@@ -1342,8 +2519,8 @@ var actionSchema = z.strictObject({
1342
2519
  }
1343
2520
  }
1344
2521
  });
1345
- function extractNamedParams(sql3) {
1346
- const withoutStrings = sql3.replace(QUOTED_STRING_PATTERN, "''");
2522
+ function extractNamedParams(sql4) {
2523
+ const withoutStrings = sql4.replace(QUOTED_STRING_PATTERN, "''");
1347
2524
  const names = [...withoutStrings.matchAll(NAMED_PARAM_PATTERN)].map((match) => match[1]);
1348
2525
  return [...new Set(names)];
1349
2526
  }
@@ -1413,11 +2590,13 @@ var sqlTypeName = (type) => SQL_TYPE_NAMES[type] ?? type;
1413
2590
  // src/compiler/sql.ts
1414
2591
  var CLAIMS_SETTING = "request.jwt.claims";
1415
2592
  var CAPABILITIES_CLAIM_KEY = "capabilities";
2593
+ var ROLES_CLAIM_KEY = "roles";
1416
2594
  var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
1417
2595
  var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
1418
2596
  var CLAIMS_SOURCE = `nullif(current_setting('${CLAIMS_SETTING}', true), '')::jsonb`;
1419
2597
  var claimExpression = (key) => `(${CLAIMS_SOURCE} ->> ${quoteLiteral(key)})`;
1420
2598
  var capabilityExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${CAPABILITIES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
2599
+ var roleExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${ROLES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
1421
2600
 
1422
2601
  // src/compiler/predicates.ts
1423
2602
  var SQL_COMPARE_OPS = {
@@ -1498,8 +2677,8 @@ var actionParamArg = (name) => `p_${name}`;
1498
2677
  function compileActionStatements(action, renderParam = actionParamArg) {
1499
2678
  return actionStatementSources(action.executes).map((statement) => compileStatement(statement, renderParam));
1500
2679
  }
1501
- function compileStatement(sql3, renderParam) {
1502
- const compiled = sql3.replace(TOKEN_PATTERN, (token, claimKey, capabilityKey, paramName) => {
2680
+ function compileStatement(sql4, renderParam) {
2681
+ const compiled = sql4.replace(TOKEN_PATTERN, (token, claimKey, capabilityKey, paramName) => {
1503
2682
  if (claimKey !== void 0) {
1504
2683
  if (!CLAIM_KEY_PATTERN.test(claimKey)) throw new OrmDefinitionError(`invalid claim key in executes: ${token}`);
1505
2684
  return claimExpression(claimKey);
@@ -1531,6 +2710,118 @@ function referencedExecuteCapabilities(action) {
1531
2710
  return keys;
1532
2711
  }
1533
2712
 
2713
+ // src/compiler/pred-ir-sql.ts
2714
+ function compilePredIrToSql(pred, options = {}) {
2715
+ switch (pred.kind) {
2716
+ case "const":
2717
+ return pred.value ? "TRUE" : "FALSE";
2718
+ case "and":
2719
+ return `(${pred.operands.map((operand) => compilePredIrToSql(operand, options)).join(" AND ")})`;
2720
+ case "or":
2721
+ return `(${pred.operands.map((operand) => compilePredIrToSql(operand, options)).join(" OR ")})`;
2722
+ case "not":
2723
+ return `(NOT ${compilePredIrToSql(pred.operand, options)})`;
2724
+ case "cmp":
2725
+ return compileCmp(pred.op, pred.column, pred.value, options);
2726
+ case "capability":
2727
+ return `(${capabilityExpression(pred.key)})`;
2728
+ case "role":
2729
+ return `(${roleExpression(pred.key)})`;
2730
+ case "exists":
2731
+ case "aggPredicate":
2732
+ case "dynamicFilter":
2733
+ throw new OrmDefinitionError(
2734
+ `synced view read policy cannot use '${pred.kind}' \u2014 use row filters and claim/capability/role checks only`
2735
+ );
2736
+ default: {
2737
+ const exhausted = pred;
2738
+ throw new OrmDefinitionError(`unsupported synced view predicate kind '${String(exhausted)}'`);
2739
+ }
2740
+ }
2741
+ }
2742
+ function physicalColumn(logical, options) {
2743
+ return options.columnNames?.[logical] ?? logical;
2744
+ }
2745
+ function compileCmp(op, column, value, options) {
2746
+ const physical = physicalColumn(column, options);
2747
+ const declared = options.columnTypes?.[physical] ?? options.columnTypes?.[column];
2748
+ const isEnum = typeof declared === "object" && declared !== null;
2749
+ const col = isEnum ? `${quoteIdentifier(physical)}::text` : quoteIdentifier(physical);
2750
+ const columnType = isEnum ? "text" : declared;
2751
+ switch (op) {
2752
+ case "eq":
2753
+ return compileEquality(col, value, columnType, false);
2754
+ case "neq":
2755
+ return compileEquality(col, value, columnType, true);
2756
+ case "gt":
2757
+ return `${col} > ${compileValue(value, columnType)}`;
2758
+ case "gte":
2759
+ return `${col} >= ${compileValue(value, columnType)}`;
2760
+ case "lt":
2761
+ return `${col} < ${compileValue(value, columnType)}`;
2762
+ case "lte":
2763
+ return `${col} <= ${compileValue(value, columnType)}`;
2764
+ case "in":
2765
+ return `${col} = ANY(${compileArrayValue(value, columnType)})`;
2766
+ case "nin":
2767
+ return `${col} <> ALL(${compileArrayValue(value, columnType)})`;
2768
+ case "like":
2769
+ return `${col} LIKE ${compileValue(value, columnType)}`;
2770
+ case "ilike":
2771
+ return `${col} ILIKE ${compileValue(value, columnType)}`;
2772
+ case "isNull":
2773
+ return value.kind === "literal" && value.value === true ? `${col} IS NULL` : `${col} IS NOT NULL`;
2774
+ case "contains":
2775
+ case "hasKey":
2776
+ throw new OrmDefinitionError(`synced view read policy cannot use json operator '${op}'`);
2777
+ default: {
2778
+ const exhausted = op;
2779
+ throw new OrmDefinitionError(`unsupported synced view comparison '${String(exhausted)}'`);
2780
+ }
2781
+ }
2782
+ }
2783
+ function compileEquality(col, value, columnType, negated, options) {
2784
+ if (value.kind === "literal" && value.value === null) {
2785
+ return negated ? `${col} IS NOT NULL` : `${col} IS NULL`;
2786
+ }
2787
+ const rhs = compileValue(value, columnType);
2788
+ return negated ? `${col} <> ${rhs}` : `${col} = ${rhs}`;
2789
+ }
2790
+ function compileValue(value, columnType, _options) {
2791
+ switch (value.kind) {
2792
+ case "literal": {
2793
+ const literal = value.value;
2794
+ if (literal === null) return "NULL";
2795
+ if (typeof literal === "string") return quoteLiteral(literal);
2796
+ if (typeof literal === "boolean") return literal ? "TRUE" : "FALSE";
2797
+ return String(literal);
2798
+ }
2799
+ case "claim": {
2800
+ const expression = claimExpression(value.key);
2801
+ return columnType && columnType !== "text" ? `(${expression})::${sqlTypeName(columnType)}` : expression;
2802
+ }
2803
+ case "variable":
2804
+ throw new OrmDefinitionError("synced view read policy cannot reference operation variables");
2805
+ default: {
2806
+ const exhausted = value;
2807
+ throw new OrmDefinitionError(`unsupported synced view value kind '${String(exhausted)}'`);
2808
+ }
2809
+ }
2810
+ }
2811
+ function compileArrayValue(value, columnType, _options) {
2812
+ if (value.kind !== "literal" || !Array.isArray(value.value)) {
2813
+ throw new OrmDefinitionError("synced view IN/NIN comparisons require literal arrays");
2814
+ }
2815
+ const entries = value.value.map((entry) => {
2816
+ if (entry === null) return "NULL";
2817
+ if (typeof entry === "string") return quoteLiteral(entry);
2818
+ if (typeof entry === "boolean") return entry ? "TRUE" : "FALSE";
2819
+ return String(entry);
2820
+ });
2821
+ const array = `ARRAY[${entries.join(", ")}]`;
2822
+ return columnType ? `${array}::${sqlTypeName(columnType)}[]` : array;
2823
+ }
2824
+
1534
2825
  // src/engine/actions.ts
1535
2826
  var ACTION_REQUIRES_DENIED_SQLSTATE = "ORMRQ";
1536
2827
  var ACTION_VALIDATES_FAILED_SQLSTATE = "ORMVL";
@@ -1552,8 +2843,8 @@ function actionPrepareChecks(definition) {
1552
2843
  const order = actionParamOrder(action);
1553
2844
  const signature = order.length > 0 ? ` (${order.map((param) => sqlTypeName(action.params[param].type)).join(", ")})` : "";
1554
2845
  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;
2846
+ compiled.forEach((sql4, index3) => {
2847
+ const checked = action.returns && index3 === compiled.length - 1 ? returnProjection(action.returns, sql4) : sql4;
1557
2848
  statements.push(`PREPARE ${quoteIdentifier(`orm_check_${name}_${index3}`)}${signature} AS ${checked}`);
1558
2849
  });
1559
2850
  }
@@ -1672,7 +2963,9 @@ var indexSchema = z.union([
1672
2963
  ]).transform((index3) => Array.isArray(index3) ? { columns: index3 } : index3);
1673
2964
  var policiesSchema = z.strictObject({
1674
2965
  read: parsedPredicateSchema.optional(),
1675
- write: parsedPredicateSchema.optional()
2966
+ write: parsedPredicateSchema.optional(),
2967
+ insert: parsedPredicateSchema.optional(),
2968
+ update: parsedPredicateSchema.optional()
1676
2969
  });
1677
2970
  var tableFileSchema = z.strictObject({
1678
2971
  namespace: namespaceSchema,
@@ -1708,7 +3001,6 @@ var policyName = (table, suffix) => `${table}_${suffix}`;
1708
3001
  var foreignKeyName = (table, link) => `fk_${table}_${link}`;
1709
3002
  function validateSyncedTable(table, ctx) {
1710
3003
  const forbidden = [
1711
- ["columns", "synced tables reflect their source; columns are not declared"],
1712
3004
  ["actions", "synced tables are read-only; actions are not allowed"],
1713
3005
  ["indexes", "synced tables are system-owned; indexes are not allowed"]
1714
3006
  ];
@@ -1718,6 +3010,39 @@ function validateSyncedTable(table, ctx) {
1718
3010
  if (table.policies?.write) {
1719
3011
  ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "write"] });
1720
3012
  }
3013
+ if (table.policies?.insert) {
3014
+ ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "insert"] });
3015
+ }
3016
+ if (table.policies?.update) {
3017
+ ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "update"] });
3018
+ }
3019
+ const columns = table.columns ?? {};
3020
+ const names = Object.keys(columns);
3021
+ if (names.length === 0) return;
3022
+ const primaryKeys = names.filter((name) => columns[name]?.primary_key);
3023
+ if (primaryKeys.length !== 1) {
3024
+ ctx.addIssue({ code: "custom", message: "exactly one column must be primary_key", path: ["columns"] });
3025
+ }
3026
+ validateRenames(columns, names, ctx);
3027
+ for (const name of names) {
3028
+ const column = columns[name];
3029
+ if (!column) continue;
3030
+ const cast = column.cast ? castParts(column.cast) : null;
3031
+ if (column.cast && cast?.column !== name) {
3032
+ ctx.addIssue({
3033
+ code: "custom",
3034
+ message: "cast must reference the column it is declared on",
3035
+ path: ["columns", name, "cast"]
3036
+ });
3037
+ }
3038
+ if (cast && cast.type !== column.type) {
3039
+ ctx.addIssue({
3040
+ code: "custom",
3041
+ message: `cast target '${cast.type}' must match the declared type '${column.type}'`,
3042
+ path: ["columns", name, "cast"]
3043
+ });
3044
+ }
3045
+ }
1721
3046
  }
1722
3047
  function validateRenames(columns, names, ctx) {
1723
3048
  const renameSources = /* @__PURE__ */ new Map();
@@ -1795,7 +3120,7 @@ function validateColumnTable(table, ctx) {
1795
3120
  function validateLinks(table, ctx) {
1796
3121
  const columnNames = Object.keys(table.columns ?? {});
1797
3122
  for (const [name, link] of Object.entries(table.links ?? {})) {
1798
- if (!table.source && !columnNames.includes(link.on)) {
3123
+ if (columnNames.length > 0 && !columnNames.includes(link.on)) {
1799
3124
  ctx.addIssue({ code: "custom", message: `link references unknown column '${link.on}'`, path: ["links", name, "on"] });
1800
3125
  }
1801
3126
  if (link.enforce && link.to.includes(".")) {
@@ -1868,10 +3193,10 @@ var TYPE_GROUPS = {
1868
3193
  };
1869
3194
  var columnTypeGroup = (type) => TYPE_GROUPS[type];
1870
3195
  function validatePolicyColumns(table, ctx) {
1871
- if (table.source) return;
1872
3196
  const columns = table.columns ?? {};
1873
3197
  const columnNames = Object.keys(columns);
1874
- for (const kind of ["read", "write"]) {
3198
+ if (columnNames.length === 0) return;
3199
+ for (const kind of ["read", "write", "insert", "update"]) {
1875
3200
  const predicate = table.policies?.[kind];
1876
3201
  if (!predicate) continue;
1877
3202
  for (const column of referencedColumns(predicate.ast)) {
@@ -1948,10 +3273,15 @@ function policyStatements(schema, table) {
1948
3273
  if (read) {
1949
3274
  statements.push(`CREATE POLICY ${policyName2("read")} ON ${qualified} FOR SELECT USING (${compilePredicate(read.ast, columnTypes)})`);
1950
3275
  }
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})`);
3276
+ const insertPolicy = table.policies?.insert ?? table.policies?.write;
3277
+ const updatePolicy = table.policies?.update ?? table.policies?.write;
3278
+ if (insertPolicy) {
3279
+ statements.push(
3280
+ `CREATE POLICY ${policyName2("insert")} ON ${qualified} FOR INSERT WITH CHECK (${compilePredicate(insertPolicy.ast, columnTypes)})`
3281
+ );
3282
+ }
3283
+ if (updatePolicy) {
3284
+ const predicate = compilePredicate(updatePolicy.ast, columnTypes);
1955
3285
  statements.push(
1956
3286
  `CREATE POLICY ${policyName2("update")} ON ${qualified} FOR UPDATE USING (${predicate}) WITH CHECK (${predicate})`
1957
3287
  );
@@ -1960,6 +3290,118 @@ function policyStatements(schema, table) {
1960
3290
  return statements;
1961
3291
  }
1962
3292
  var columnTables = (definition) => definition.tables.filter((table) => !table.source);
3293
+ var syncedTables = (definition) => definition.tables.filter((table) => !!table.source);
3294
+ var SYNCED_VIEW_EXECUTION_OWNER_ROLE = "orm_exec";
3295
+ function syncedViewStatements({
3296
+ definition,
3297
+ backings,
3298
+ readPolicyIrByTable,
3299
+ columnNamesByTable,
3300
+ columnTypesByTable,
3301
+ executionOwnerRole = SYNCED_VIEW_EXECUTION_OWNER_ROLE
3302
+ }) {
3303
+ const schema = quoteIdentifier(ormSchemaName(definition.namespace));
3304
+ const role = quoteIdentifier(consumerRoleName(definition.namespace));
3305
+ const owner = quoteIdentifier(executionOwnerRole);
3306
+ const backingMap = /* @__PURE__ */ new Map();
3307
+ for (const backing of backings) {
3308
+ const existing = backingMap.get(backing.table);
3309
+ if (existing) {
3310
+ throw new OrmDefinitionError(
3311
+ `duplicate synced backing for table "${backing.table}" ('${existing.physicalRelation}' and '${backing.physicalRelation}') \u2014 refusing to materialize an ambiguous authorization boundary`
3312
+ );
3313
+ }
3314
+ backingMap.set(backing.table, backing);
3315
+ }
3316
+ const statements = [];
3317
+ for (const table of syncedTables(definition)) {
3318
+ const backing = backingMap.get(table.table);
3319
+ if (!backing) continue;
3320
+ const viewName = `${schema}.${quoteIdentifier(table.table)}`;
3321
+ const physical = splitPhysicalRelation(backing.physicalRelation);
3322
+ const physicalTarget = `${quoteIdentifier(physical.schema)}.${quoteIdentifier(physical.table)}`;
3323
+ const columnTypes = mergeSyncedViewColumnTypes({
3324
+ table,
3325
+ canonicalColumnTypes: columnTypesByTable?.get(table.table),
3326
+ backingColumnTypes: backing.columnTypes,
3327
+ columnNames: columnNamesByTable?.get(table.table)
3328
+ });
3329
+ const readPolicyIr = readPolicyIrByTable?.get(table.table);
3330
+ const readPolicy = table.policies?.read;
3331
+ const whereClause = readPolicyIr ? ` WHERE ${compilePredIrToSql(readPolicyIr, {
3332
+ columnTypes,
3333
+ columnNames: columnNamesByTable?.get(table.table)
3334
+ })}` : readPolicy ? ` WHERE ${compilePredicate(readPolicy.ast, narrowToV1ColumnTypes(columnTypes))}` : " WHERE false";
3335
+ statements.push(`GRANT USAGE ON SCHEMA ${quoteIdentifier(physical.schema)} TO ${owner}`);
3336
+ statements.push(`GRANT SELECT ON ${physicalTarget} TO ${owner}`);
3337
+ statements.push(
3338
+ `CREATE OR REPLACE VIEW ${viewName} AS SELECT * FROM ${physicalTarget}${whereClause}`
3339
+ );
3340
+ statements.push(`GRANT SELECT ON ${viewName} TO ${role}`);
3341
+ }
3342
+ return statements;
3343
+ }
3344
+ function mergeSyncedViewColumnTypes({
3345
+ table,
3346
+ canonicalColumnTypes,
3347
+ backingColumnTypes,
3348
+ columnNames
3349
+ }) {
3350
+ const fromTable = {};
3351
+ for (const [name, def] of Object.entries(table.columns ?? {})) {
3352
+ if (def?.type) fromTable[name] = def.type;
3353
+ }
3354
+ if (columnNames) {
3355
+ for (const [logical, physical] of Object.entries(columnNames)) {
3356
+ const declared = fromTable[physical] ?? fromTable[logical];
3357
+ if (!declared) continue;
3358
+ fromTable[physical] = declared;
3359
+ fromTable[logical] = declared;
3360
+ }
3361
+ }
3362
+ const canonicalEnums = Object.fromEntries(
3363
+ Object.entries(canonicalColumnTypes ?? {}).filter(([, type]) => typeof type === "object")
3364
+ );
3365
+ return { ...fromTable, ...canonicalColumnTypes ?? {}, ...backingColumnTypes ?? {}, ...canonicalEnums };
3366
+ }
3367
+ function narrowToV1ColumnTypes(columnTypes) {
3368
+ return Object.fromEntries(
3369
+ Object.entries(columnTypes).map(([column, type]) => [column, typeof type === "object" ? "text" : type])
3370
+ );
3371
+ }
3372
+ function splitPhysicalRelation(physicalRelation) {
3373
+ const parts = physicalRelation.split(".");
3374
+ if (parts.length === 1 && parts[0]) return { schema: "public", table: parts[0] };
3375
+ if (parts.length === 2 && parts[0] && parts[1]) return { schema: parts[0], table: parts[1] };
3376
+ throw new OrmDefinitionError(
3377
+ `synced view physical relation must be "table" or "schema.table" (got "${physicalRelation}")`
3378
+ );
3379
+ }
3380
+ function syncedViewDropStatements({
3381
+ schemaName,
3382
+ tables
3383
+ }) {
3384
+ const schema = quoteIdentifier(schemaName);
3385
+ return tables.map(
3386
+ (table) => `DROP VIEW IF EXISTS ${schema}.${quoteIdentifier(table)} CASCADE`
3387
+ );
3388
+ }
3389
+ function syncedBackingRevokeStatements({
3390
+ backings,
3391
+ executionOwnerRole = SYNCED_VIEW_EXECUTION_OWNER_ROLE
3392
+ }) {
3393
+ const owner = quoteIdentifier(executionOwnerRole);
3394
+ const seen = /* @__PURE__ */ new Set();
3395
+ const statements = [];
3396
+ for (const backing of backings) {
3397
+ const physical = splitPhysicalRelation(backing.physicalRelation);
3398
+ const physicalTarget = `${quoteIdentifier(physical.schema)}.${quoteIdentifier(physical.table)}`;
3399
+ if (seen.has(physicalTarget)) continue;
3400
+ seen.add(physicalTarget);
3401
+ statements.push(`REVOKE SELECT ON ${physicalTarget} FROM ${owner}`);
3402
+ }
3403
+ return statements;
3404
+ }
1963
3405
  var FACTORIES = {
1964
3406
  uuid: (name) => uuid(name),
1965
3407
  text: (name) => text(name),
@@ -2153,99 +3595,16 @@ function applyMigrationHints(previous, definition) {
2153
3595
  }
2154
3596
  }
2155
3597
  }
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
- }
3598
+ return { statements, adjusted };
2220
3599
  }
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 };
3600
+ function findTable(snapshot, schema, table) {
3601
+ const tables = snapshot.tables ?? {};
3602
+ return Object.values(tables).find((entry) => entry.schema === schema && entry.name === table);
3603
+ }
3604
+ function snapshotTableNames(snapshot, schema) {
3605
+ const tables = snapshot.tables ?? {};
3606
+ return Object.values(tables).filter((entry) => entry.schema === schema).map((entry) => entry.name);
2246
3607
  }
2247
- var compileNamespace = (definition) => planMigration({ definition });
2248
- var previousId = (snapshot) => snapshot.id;
2249
3608
  var FACTORIES2 = {
2250
3609
  uuid: (name) => uuid(name),
2251
3610
  text: (name) => text(name),
@@ -2283,11 +3642,268 @@ function buildDrizzleEntitiesV2(definition) {
2283
3642
  imports[table.table] = drizzleTable;
2284
3643
  tables[table.table] = drizzleTable;
2285
3644
  }
3645
+ for (const view of definition.views ?? []) {
3646
+ const columns = Object.fromEntries(
3647
+ view.columns.map((column) => [column.columnName, buildViewColumnV2({ view: view.view, column, enums })])
3648
+ );
3649
+ imports[view.view] = schema.view(view.view, columns).as(
3650
+ sql.raw(
3651
+ rewriteSyncedSourceRelationsInViewQuery({
3652
+ query: view.query,
3653
+ canonical: definition,
3654
+ schemaName,
3655
+ dependencies: view.dependencies
3656
+ })
3657
+ )
3658
+ );
3659
+ }
2286
3660
  return { imports, tables, schemaName };
2287
3661
  }
3662
+ function rewriteSyncedSourceRelationsInViewQuery({
3663
+ query,
3664
+ canonical,
3665
+ schemaName,
3666
+ dependencies
3667
+ }) {
3668
+ const tablesBySource = /* @__PURE__ */ new Map();
3669
+ for (const table of canonical.tables) {
3670
+ if (!table.source) continue;
3671
+ tablesBySource.set(table.source, [...tablesBySource.get(table.source) ?? [], table.table]);
3672
+ }
3673
+ const declared = new Set(dependencies ?? []);
3674
+ const boundTableBySource = /* @__PURE__ */ new Map();
3675
+ for (const [source, tables] of tablesBySource) {
3676
+ if (tables.length === 1) {
3677
+ boundTableBySource.set(source, tables[0]);
3678
+ continue;
3679
+ }
3680
+ const [sourceSchema, sourceName] = source.split(".");
3681
+ if (!sourceSchema || !sourceName) continue;
3682
+ const referenced = sqlReferencesQualifiedRelation(query, sourceSchema, sourceName);
3683
+ if (!referenced) continue;
3684
+ const declaredMatches = tables.filter((table) => declared.has(table));
3685
+ if (declaredMatches.length === 1) {
3686
+ boundTableBySource.set(source, declaredMatches[0]);
3687
+ continue;
3688
+ }
3689
+ throw new OrmDefinitionError(
3690
+ `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`
3691
+ );
3692
+ }
3693
+ return transformSqlOutsideLiterals(query, (segment) => {
3694
+ let rewritten = segment;
3695
+ for (const table of canonical.tables) {
3696
+ if (!table.source) continue;
3697
+ const boundTable = boundTableBySource.get(table.source);
3698
+ if (boundTable === void 0 || boundTable !== table.table) continue;
3699
+ const [sourceSchema, sourceName] = table.source.split(".");
3700
+ if (!sourceSchema || !sourceName) continue;
3701
+ rewritten = replaceQualifiedRelation(
3702
+ rewritten,
3703
+ sourceSchema,
3704
+ sourceName,
3705
+ schemaName,
3706
+ table.table
3707
+ );
3708
+ }
3709
+ return rewritten;
3710
+ });
3711
+ }
3712
+ function transformSqlOutsideLiterals(sql4, transform) {
3713
+ const out = [];
3714
+ let state = { kind: "normal" };
3715
+ let i = 0;
3716
+ let codeStart = 0;
3717
+ const emitCode = (end) => {
3718
+ if (end > codeStart) out.push(transform(sql4.slice(codeStart, end)));
3719
+ codeStart = end;
3720
+ };
3721
+ while (i < sql4.length) {
3722
+ const c = sql4[i];
3723
+ const next = i + 1 < sql4.length ? sql4[i + 1] : "";
3724
+ switch (state.kind) {
3725
+ case "normal": {
3726
+ if (c === "-" && next === "-") {
3727
+ emitCode(i);
3728
+ state = { kind: "line-comment" };
3729
+ out.push("--");
3730
+ i += 2;
3731
+ codeStart = i;
3732
+ break;
3733
+ }
3734
+ if (c === "/" && next === "*") {
3735
+ emitCode(i);
3736
+ state = { kind: "block-comment", depth: 1 };
3737
+ out.push("/*");
3738
+ i += 2;
3739
+ codeStart = i;
3740
+ break;
3741
+ }
3742
+ if ((c === "E" || c === "e") && next === "'") {
3743
+ const prev = i > 0 ? sql4[i - 1] : "";
3744
+ if (!/[A-Za-z0-9_]/.test(prev)) {
3745
+ emitCode(i);
3746
+ state = { kind: "e-single" };
3747
+ out.push("E'");
3748
+ i += 2;
3749
+ codeStart = i;
3750
+ break;
3751
+ }
3752
+ }
3753
+ if (c === "'") {
3754
+ emitCode(i);
3755
+ state = { kind: "single" };
3756
+ out.push("'");
3757
+ i += 1;
3758
+ codeStart = i;
3759
+ break;
3760
+ }
3761
+ if (c === "$") {
3762
+ const prev = i > 0 ? sql4[i - 1] : "";
3763
+ if (!/[A-Za-z0-9_$]/.test(prev)) {
3764
+ const tagMatch = /^\$([A-Za-z_][A-Za-z0-9_]*)?\$/.exec(sql4.slice(i));
3765
+ if (tagMatch) {
3766
+ emitCode(i);
3767
+ state = { kind: "dollar", tag: tagMatch[1] ?? "" };
3768
+ out.push(tagMatch[0]);
3769
+ i += tagMatch[0].length;
3770
+ codeStart = i;
3771
+ break;
3772
+ }
3773
+ }
3774
+ }
3775
+ i += 1;
3776
+ break;
3777
+ }
3778
+ case "single": {
3779
+ if (c === "'" && next === "'") {
3780
+ out.push("''");
3781
+ i += 2;
3782
+ break;
3783
+ }
3784
+ if (c === "'") {
3785
+ out.push("'");
3786
+ state = { kind: "normal" };
3787
+ i += 1;
3788
+ codeStart = i;
3789
+ break;
3790
+ }
3791
+ out.push(c);
3792
+ i += 1;
3793
+ break;
3794
+ }
3795
+ case "e-single": {
3796
+ if (c === "\\" && i + 1 < sql4.length) {
3797
+ out.push(c, sql4[i + 1]);
3798
+ i += 2;
3799
+ break;
3800
+ }
3801
+ if (c === "'" && next === "'") {
3802
+ out.push("''");
3803
+ i += 2;
3804
+ break;
3805
+ }
3806
+ if (c === "'") {
3807
+ out.push("'");
3808
+ state = { kind: "normal" };
3809
+ i += 1;
3810
+ codeStart = i;
3811
+ break;
3812
+ }
3813
+ out.push(c);
3814
+ i += 1;
3815
+ break;
3816
+ }
3817
+ case "dollar": {
3818
+ const closer = `$${state.tag}$`;
3819
+ if (sql4.startsWith(closer, i)) {
3820
+ out.push(closer);
3821
+ state = { kind: "normal" };
3822
+ i += closer.length;
3823
+ codeStart = i;
3824
+ break;
3825
+ }
3826
+ out.push(c);
3827
+ i += 1;
3828
+ break;
3829
+ }
3830
+ case "line-comment": {
3831
+ if (c === "\n") {
3832
+ out.push("\n");
3833
+ state = { kind: "normal" };
3834
+ i += 1;
3835
+ codeStart = i;
3836
+ break;
3837
+ }
3838
+ out.push(c);
3839
+ i += 1;
3840
+ break;
3841
+ }
3842
+ case "block-comment": {
3843
+ if (c === "/" && next === "*") {
3844
+ out.push("/*");
3845
+ state = { kind: "block-comment", depth: state.depth + 1 };
3846
+ i += 2;
3847
+ break;
3848
+ }
3849
+ if (c === "*" && next === "/") {
3850
+ out.push("*/");
3851
+ const newDepth = state.depth - 1;
3852
+ state = newDepth === 0 ? { kind: "normal" } : { kind: "block-comment", depth: newDepth };
3853
+ i += 2;
3854
+ if (state.kind === "normal") codeStart = i;
3855
+ break;
3856
+ }
3857
+ out.push(c);
3858
+ i += 1;
3859
+ break;
3860
+ }
3861
+ }
3862
+ }
3863
+ emitCode(sql4.length);
3864
+ return out.join("");
3865
+ }
3866
+ function escapeRegExp(value) {
3867
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3868
+ }
3869
+ function sqlReferencesQualifiedRelation(sql4, sourceSchema, sourceName) {
3870
+ let referenced = false;
3871
+ transformSqlOutsideLiterals(sql4, (segment) => {
3872
+ if (qualifiedRelationPattern(sourceSchema, sourceName).test(segment)) referenced = true;
3873
+ return segment;
3874
+ });
3875
+ return referenced;
3876
+ }
3877
+ function qualifiedRelationPattern(sourceSchema, sourceName) {
3878
+ const schemaPattern = `(?:"${escapeRegExp(sourceSchema)}"|${escapeRegExp(sourceSchema)})`;
3879
+ const namePattern = `(?:"${escapeRegExp(sourceName)}"|${escapeRegExp(sourceName)})`;
3880
+ return new RegExp(`(?<![\\w."])${schemaPattern}\\.${namePattern}(?![\\w"])`, "g");
3881
+ }
3882
+ function replaceQualifiedRelation(sql4, sourceSchema, sourceName, boundSchema, boundTable) {
3883
+ return sql4.replace(qualifiedRelationPattern(sourceSchema, sourceName), `"${boundSchema}"."${boundTable}"`);
3884
+ }
2288
3885
  function generateSnapshotV2(definition, previousId3) {
2289
3886
  const { imports, schemaName } = buildDrizzleEntitiesV2(definition);
2290
- return generateDrizzleJson(imports, previousId3, [schemaName]);
3887
+ const snapshot = generateDrizzleJson(imports, previousId3, [schemaName]);
3888
+ if ((definition.views?.length ?? 0) > 0) {
3889
+ const extended = snapshot;
3890
+ extended._sequenceOrm = {
3891
+ viewDependencies: Object.fromEntries(
3892
+ (definition.views ?? []).map((view) => [view.view, [...view.dependencies].sort()])
3893
+ )
3894
+ };
3895
+ }
3896
+ return snapshot;
3897
+ }
3898
+ function buildViewColumnV2({
3899
+ view,
3900
+ column,
3901
+ enums
3902
+ }) {
3903
+ const factory = resolveColumnFactory(view, column, enums);
3904
+ let builder = factory(column.columnName);
3905
+ if (!column.nullable) builder = builder.notNull();
3906
+ return builder;
2291
3907
  }
2292
3908
  function buildTable2(schema, table, built, enums, byName) {
2293
3909
  const primaryKeyColumns = table.primaryKey ?? [];
@@ -2531,15 +4147,379 @@ async function planMigrationV2({
2531
4147
  }) {
2532
4148
  const schema = ormSchemaName(canonical.namespace);
2533
4149
  const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
4150
+ assertValidViewDependencies(canonical);
2534
4151
  const hints = applyMigrationHintsV2(previous, canonical);
2535
- const snapshot = generateSnapshotV2(canonical, previousId2(previous));
2536
- const ddl = await diffStatements(hints.adjusted, snapshot);
4152
+ const snapshot = generateSnapshotV2(canonical, previousId(previous));
4153
+ const baseDdl = await diffStatements(withoutViews(hints.adjusted), withoutViews(snapshot));
4154
+ const previousViews = snapshotViews(hints.adjusted);
4155
+ const currentViews = snapshotViews(snapshot);
4156
+ const previousDependencies = snapshotViewDependencies(hints.adjusted);
4157
+ const currentDependencies = snapshotViewDependencies(snapshot);
4158
+ const affected = affectedViewNames({
4159
+ previousViews,
4160
+ currentViews,
4161
+ previousDependencies,
4162
+ currentDependencies,
4163
+ baseChanged: hints.statements.length > 0 || baseDdl.length > 0
4164
+ });
4165
+ const dropOrder = topologicalViewNames(
4166
+ new Set([...affected].filter((name) => previousViews.has(name))),
4167
+ previousDependencies
4168
+ ).reverse();
4169
+ const createOrder = topologicalViewNames(
4170
+ new Set([...affected].filter((name) => currentViews.has(name))),
4171
+ currentDependencies
4172
+ );
4173
+ const dropDdl = await generatedViewStatements({
4174
+ snapshot: hints.adjusted,
4175
+ names: new Set(dropOrder),
4176
+ mode: "drop",
4177
+ schema
4178
+ });
4179
+ const createDdl = await generatedViewStatements({
4180
+ snapshot,
4181
+ names: new Set(createOrder),
4182
+ mode: "create",
4183
+ schema
4184
+ });
4185
+ const currentViewDefinitions = new Map((canonical.views ?? []).map((view) => [view.view, view]));
2537
4186
  const statements = [
4187
+ ...dropOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(dropDdl, name, schema, "DROP") })),
2538
4188
  ...hints.statements.map((statement) => ({ kind: "hint", sql: statement })),
2539
- ...ddl.map((statement) => ({ kind: "ddl", sql: statement.replace(/;\s*$/, "") }))
4189
+ ...baseDdl.map((statement) => ({ kind: "ddl", sql: statement.replace(/;\s*$/, "") })),
4190
+ ...createOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(createDdl, name, schema, "CREATE") })),
4191
+ ...createOrder.map((name) => {
4192
+ const view = currentViewDefinitions.get(name);
4193
+ if (!view) throw new OrmDefinitionError(`missing canonical definition for view '${name}'`);
4194
+ return { kind: "ddl", sql: validateViewCatalogStatement({ canonical, view, schema }) };
4195
+ })
2540
4196
  ];
2541
4197
  return { namespace: canonical.namespace, schema, statements, snapshot };
2542
4198
  }
4199
+ function validateViewCatalogStatement({
4200
+ canonical,
4201
+ view,
4202
+ schema
4203
+ }) {
4204
+ const qualified = `${schema}.${view.view}`;
4205
+ const expectedColumns = sqlTextArray(view.columns.map((column) => column.columnName));
4206
+ const expectedTypes = `ARRAY[${view.columns.map((column) => `${viewTypeRegtype(column, schema)}::oid`).join(", ")}]::oid[]`;
4207
+ const relationIdentity = /* @__PURE__ */ new Map();
4208
+ for (const table of canonical.tables) {
4209
+ relationIdentity.set(table.table, `${schema}.${table.table}`);
4210
+ }
4211
+ for (const candidate of canonical.views ?? []) {
4212
+ relationIdentity.set(candidate.view, `${schema}.${candidate.view}`);
4213
+ }
4214
+ const expectedDependencies = sqlTextArray(
4215
+ view.dependencies.map((dependency) => {
4216
+ const physical = relationIdentity.get(dependency);
4217
+ if (!physical) throw new OrmDefinitionError(`view '${view.view}' depends on unknown relation '${dependency}'`);
4218
+ return physical;
4219
+ }).sort()
4220
+ );
4221
+ const allowedFunctions = sqlTextArray(VIEW_QUERY_FUNCTION_ALLOWLIST);
4222
+ return `DO $sequence_orm_validate_view$
4223
+ DECLARE
4224
+ actual_columns text[];
4225
+ actual_types oid[];
4226
+ actual_dependencies text[];
4227
+ BEGIN
4228
+ SELECT
4229
+ array_agg(attribute.attname ORDER BY attribute.attnum),
4230
+ array_agg(attribute.atttypid ORDER BY attribute.attnum)
4231
+ INTO actual_columns, actual_types
4232
+ FROM pg_catalog.pg_attribute AS attribute
4233
+ WHERE attribute.attrelid = pg_catalog.to_regclass(${quoteLiteral(qualified)})
4234
+ AND attribute.attnum > 0
4235
+ AND NOT attribute.attisdropped;
4236
+
4237
+ IF actual_columns IS DISTINCT FROM ${expectedColumns} THEN
4238
+ RAISE EXCEPTION 'managed view ${qualified} columns do not match its declaration: expected %, got %', ${expectedColumns}, actual_columns
4239
+ USING ERRCODE = '42804';
4240
+ END IF;
4241
+ IF actual_types IS DISTINCT FROM ${expectedTypes} THEN
4242
+ RAISE EXCEPTION 'managed view ${qualified} types do not match its declaration: expected %, got %', ${expectedTypes}, actual_types
4243
+ USING ERRCODE = '42804';
4244
+ END IF;
4245
+
4246
+ SELECT coalesce(array_agg(found.name ORDER BY found.name), ARRAY[]::text[])
4247
+ INTO actual_dependencies
4248
+ FROM (
4249
+ SELECT DISTINCT dependency_namespace.nspname || '.' || dependency_relation.relname AS name
4250
+ FROM pg_catalog.pg_rewrite AS rewrite
4251
+ JOIN pg_catalog.pg_depend AS dependency
4252
+ ON dependency.classid = 'pg_catalog.pg_rewrite'::regclass
4253
+ AND dependency.objid = rewrite.oid
4254
+ AND dependency.refclassid = 'pg_catalog.pg_class'::regclass
4255
+ JOIN pg_catalog.pg_class AS dependency_relation ON dependency_relation.oid = dependency.refobjid
4256
+ JOIN pg_catalog.pg_namespace AS dependency_namespace ON dependency_namespace.oid = dependency_relation.relnamespace
4257
+ WHERE rewrite.ev_class = pg_catalog.to_regclass(${quoteLiteral(qualified)})
4258
+ AND dependency.refobjid <> rewrite.ev_class
4259
+ AND dependency_relation.relkind IN ('r', 'p', 'v', 'm', 'f')
4260
+ ) AS found;
4261
+
4262
+ IF actual_dependencies IS DISTINCT FROM ${expectedDependencies} THEN
4263
+ RAISE EXCEPTION 'managed view ${qualified} dependencies do not match its declaration: expected %, got %', ${expectedDependencies}, actual_dependencies
4264
+ USING ERRCODE = '2BP01';
4265
+ END IF;
4266
+
4267
+ IF EXISTS (
4268
+ SELECT 1
4269
+ FROM pg_catalog.pg_rewrite AS rewrite
4270
+ JOIN pg_catalog.pg_depend AS dependency
4271
+ ON dependency.classid = 'pg_catalog.pg_rewrite'::regclass
4272
+ AND dependency.objid = rewrite.oid
4273
+ AND dependency.refclassid = 'pg_catalog.pg_proc'::regclass
4274
+ JOIN pg_catalog.pg_proc AS dependency_function ON dependency_function.oid = dependency.refobjid
4275
+ JOIN pg_catalog.pg_namespace AS function_namespace ON function_namespace.oid = dependency_function.pronamespace
4276
+ WHERE rewrite.ev_class = pg_catalog.to_regclass(${quoteLiteral(qualified)})
4277
+ AND (
4278
+ function_namespace.nspname <> 'pg_catalog'
4279
+ OR dependency_function.proname <> ALL (${allowedFunctions})
4280
+ )
4281
+ ) THEN
4282
+ RAISE EXCEPTION 'managed view ${qualified} may only call explicitly allowlisted side-effect-free pg_catalog functions'
4283
+ USING ERRCODE = '42501';
4284
+ END IF;
4285
+
4286
+ IF EXISTS (
4287
+ SELECT 1
4288
+ FROM pg_catalog.pg_rewrite AS rewrite
4289
+ JOIN pg_catalog.pg_depend AS dependency
4290
+ ON dependency.classid = 'pg_catalog.pg_rewrite'::regclass
4291
+ AND dependency.objid = rewrite.oid
4292
+ AND dependency.refclassid = 'pg_catalog.pg_operator'::regclass
4293
+ JOIN pg_catalog.pg_operator AS dependency_operator ON dependency_operator.oid = dependency.refobjid
4294
+ JOIN pg_catalog.pg_namespace AS operator_namespace ON operator_namespace.oid = dependency_operator.oprnamespace
4295
+ WHERE rewrite.ev_class = pg_catalog.to_regclass(${quoteLiteral(qualified)})
4296
+ AND operator_namespace.nspname <> 'pg_catalog'
4297
+ ) THEN
4298
+ RAISE EXCEPTION 'managed view ${qualified} may only use pg_catalog operators'
4299
+ USING ERRCODE = '42501';
4300
+ END IF;
4301
+ END
4302
+ $sequence_orm_validate_view$`;
4303
+ }
4304
+ function sqlTextArray(values) {
4305
+ return values.length === 0 ? "ARRAY[]::text[]" : `ARRAY[${values.map((value) => quoteLiteral(value)).join(", ")}]::text[]`;
4306
+ }
4307
+ function viewTypeRegtype(column, schema) {
4308
+ if (typeof column.type === "string") return `${quoteLiteral(sqlTypeName(column.type))}::regtype`;
4309
+ return `${quoteLiteral(`${quoteIdentifier(schema)}.${quoteIdentifier(column.type.enum)}`)}::regtype`;
4310
+ }
4311
+ var previousId = (snapshot) => snapshot.id;
4312
+ function snapshotViews(snapshot) {
4313
+ const views = snapshot.views ?? {};
4314
+ return new Map(Object.values(views).map((view) => [view.name, view]));
4315
+ }
4316
+ function snapshotViewDependencies(snapshot) {
4317
+ const metadata = snapshot._sequenceOrm;
4318
+ return metadata?.viewDependencies ?? {};
4319
+ }
4320
+ function withoutViews(snapshot) {
4321
+ const clone = structuredClone(snapshot);
4322
+ clone.views = {};
4323
+ delete clone._sequenceOrm;
4324
+ return clone;
4325
+ }
4326
+ function affectedViewNames({
4327
+ previousViews,
4328
+ currentViews,
4329
+ previousDependencies,
4330
+ currentDependencies,
4331
+ baseChanged
4332
+ }) {
4333
+ const allNames = /* @__PURE__ */ new Set([...previousViews.keys(), ...currentViews.keys()]);
4334
+ if (baseChanged) return allNames;
4335
+ const affected = new Set(
4336
+ [...allNames].filter((name) => {
4337
+ const before = previousViews.get(name);
4338
+ const after = currentViews.get(name);
4339
+ return before === void 0 || after === void 0 || stableJson(before) !== stableJson(after);
4340
+ })
4341
+ );
4342
+ let grew = true;
4343
+ while (grew) {
4344
+ grew = false;
4345
+ for (const name of allNames) {
4346
+ if (affected.has(name)) continue;
4347
+ const dependencies = /* @__PURE__ */ new Set([...previousDependencies[name] ?? [], ...currentDependencies[name] ?? []]);
4348
+ if ([...dependencies].some((dependency) => affected.has(dependency))) {
4349
+ affected.add(name);
4350
+ grew = true;
4351
+ }
4352
+ }
4353
+ }
4354
+ return affected;
4355
+ }
4356
+ function topologicalViewNames(names, dependencies) {
4357
+ const remaining = new Set(names);
4358
+ const ordered = [];
4359
+ while (remaining.size > 0) {
4360
+ const ready = [...remaining].filter((name) => (dependencies[name] ?? []).every((dependency) => !remaining.has(dependency))).sort();
4361
+ if (ready.length === 0) {
4362
+ throw new OrmDefinitionError(`view dependencies form a cycle among: ${[...remaining].sort().join(", ")}`);
4363
+ }
4364
+ for (const name of ready) {
4365
+ ordered.push(name);
4366
+ remaining.delete(name);
4367
+ }
4368
+ }
4369
+ return ordered;
4370
+ }
4371
+ async function generatedViewStatements({
4372
+ snapshot,
4373
+ names,
4374
+ mode,
4375
+ schema
4376
+ }) {
4377
+ if (names.size === 0) return /* @__PURE__ */ new Map();
4378
+ const without = structuredClone(snapshot);
4379
+ const holder = without;
4380
+ const views = holder.views ?? {};
4381
+ holder.views = Object.fromEntries(Object.entries(views).filter(([, view]) => !names.has(view.name)));
4382
+ const generated = mode === "drop" ? await diffStatements(snapshot, without) : await diffStatements(without, snapshot);
4383
+ const statements = /* @__PURE__ */ new Map();
4384
+ for (const name of names) {
4385
+ const qualified = `${quoteIdentifier(schema)}.${quoteIdentifier(name)}`;
4386
+ const prefix = mode === "drop" ? "DROP VIEW" : "CREATE VIEW";
4387
+ const statement = generated.find((candidate) => candidate.trimStart().startsWith(`${prefix} ${qualified}`));
4388
+ if (statement) statements.set(name, statement.replace(/;\s*$/, ""));
4389
+ }
4390
+ return statements;
4391
+ }
4392
+ function requiredViewStatement(statements, name, schema, operation) {
4393
+ const statement = statements.get(name);
4394
+ if (!statement) {
4395
+ throw new OrmDefinitionError(`drizzle-kit did not generate ${operation} VIEW for '${schema}.${name}'`);
4396
+ }
4397
+ return statement;
4398
+ }
4399
+ function assertValidViewDependencies(canonical) {
4400
+ const views = canonical.views ?? [];
4401
+ const known = /* @__PURE__ */ new Set([...canonical.tables.map((table) => table.table), ...views.map((view) => view.view)]);
4402
+ for (const view of views) {
4403
+ for (const dependency of view.dependencies) {
4404
+ if (!known.has(dependency)) {
4405
+ throw new OrmDefinitionError(`view '${view.view}' depends on unknown relation '${dependency}'`);
4406
+ }
4407
+ }
4408
+ }
4409
+ topologicalViewNames(
4410
+ new Set(views.map((view) => view.view)),
4411
+ Object.fromEntries(views.map((view) => [view.view, view.dependencies]))
4412
+ );
4413
+ }
4414
+
4415
+ // src/engine/converge.ts
4416
+ var OWNED_STATEMENT = new RegExp(
4417
+ [
4418
+ String.raw`^\s*(CREATE|DROP)\s+POLICY\b`,
4419
+ String.raw`^\s*(GRANT|REVOKE)\b`,
4420
+ String.raw`^\s*(CREATE|DROP|ALTER)\s+ROLE\b`,
4421
+ String.raw`^\s*(CREATE|DROP)\s+(OR\s+REPLACE\s+)?VIEW\b`,
4422
+ // Only ever appears in an RLS toggle (ALTER TABLE … [ENABLE|FORCE] ROW LEVEL
4423
+ // SECURITY) — a three-word phrase, never an identifier.
4424
+ String.raw`\bROW LEVEL SECURITY\b`
4425
+ ].join("|"),
4426
+ "i"
4427
+ );
4428
+ var V2_OWNED_STATEMENT = new RegExp(
4429
+ [
4430
+ String.raw`^\s*(CREATE|DROP)\s+POLICY\b`,
4431
+ String.raw`^\s*(GRANT|REVOKE)\b`,
4432
+ String.raw`^\s*(CREATE|DROP|ALTER)\s+ROLE\b`,
4433
+ String.raw`\bROW LEVEL SECURITY\b`
4434
+ ].join("|"),
4435
+ "i"
4436
+ );
4437
+ function isOwnedStatement(statement) {
4438
+ return OWNED_STATEMENT.test(statement);
4439
+ }
4440
+ function isOwnedStatementV2(statement) {
4441
+ return V2_OWNED_STATEMENT.test(statement);
4442
+ }
4443
+ async function pendingKitStatements({
4444
+ definition,
4445
+ db
4446
+ }) {
4447
+ return pendingStatements({ entities: buildDrizzleEntities(definition), db, owned: OWNED_STATEMENT });
4448
+ }
4449
+ async function pendingKitStatementsV2({
4450
+ canonical,
4451
+ db
4452
+ }) {
4453
+ return pendingStatements({ entities: buildDrizzleEntitiesV2(canonical), db, owned: V2_OWNED_STATEMENT });
4454
+ }
4455
+ async function pendingStatements({
4456
+ entities,
4457
+ db,
4458
+ owned
4459
+ }) {
4460
+ const { imports, schemaName } = entities;
4461
+ const { statementsToExecute } = await withExitGuard(
4462
+ () => pushSchema(imports, shimExecute(db), [schemaName])
4463
+ );
4464
+ return statementsToExecute.filter((statement) => !owned.test(statement));
4465
+ }
4466
+ function shimExecute(db) {
4467
+ const execute = db.execute.bind(db);
4468
+ return {
4469
+ execute: async (query) => {
4470
+ const result = await execute(query);
4471
+ if (result && typeof result === "object" && "rows" in result) return result;
4472
+ return { rows: result };
4473
+ }
4474
+ };
4475
+ }
4476
+ var exitGuardChain = Promise.resolve();
4477
+ function withExitGuard(run) {
4478
+ const next = exitGuardChain.then(() => guardedRun(run));
4479
+ exitGuardChain = next.then(
4480
+ () => void 0,
4481
+ () => void 0
4482
+ );
4483
+ return next;
4484
+ }
4485
+ async function guardedRun(run) {
4486
+ const originalExit = process.exit;
4487
+ process.exit = ((code) => {
4488
+ throw new Error(`drizzle-kit attempted process.exit(${code ?? 0}) during pushSchema introspection`);
4489
+ });
4490
+ try {
4491
+ return await run();
4492
+ } finally {
4493
+ process.exit = originalExit;
4494
+ }
4495
+ }
4496
+
4497
+ // src/engine/index.ts
4498
+ var STATEMENT_KINDS = ["hint", "ddl", "security", "function", "synced_view"];
4499
+ async function planMigration({
4500
+ definition,
4501
+ previousSnapshot
4502
+ }) {
4503
+ const schema = ormSchemaName(definition.namespace);
4504
+ const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(definition.namespace);
4505
+ const hints = applyMigrationHints(previous, definition);
4506
+ const snapshot = generateSnapshot(definition, previousId2(previous));
4507
+ const ddl = await diffStatements(hints.adjusted, snapshot);
4508
+ const policyDrops = policyDropStatements(schema, snapshotTableNames(previous, schema));
4509
+ const statements = [
4510
+ ...policyDrops.map((sql4) => ({ kind: "security", sql: sql4 })),
4511
+ ...hints.statements.map((sql4) => ({ kind: "hint", sql: sql4 })),
4512
+ ...ddl.map((sql4) => ({ kind: "ddl", sql: sql4.replace(/;\s*$/, "") })),
4513
+ ...securityStatements(definition).map((sql4) => ({ kind: "security", sql: sql4 })),
4514
+ // PREPARE each action's executes first (plpgsql bodies don't check columns
4515
+ // at CREATE), then create the functions. Both reference the tables the DDL
4516
+ // above creates, so they close the plan.
4517
+ ...actionPrepareChecks(definition).map((sql4) => ({ kind: "function", sql: sql4 })),
4518
+ ...actionFunctionStatements(definition).map((sql4) => ({ kind: "function", sql: sql4 }))
4519
+ ];
4520
+ return { namespace: definition.namespace, schema, statements, snapshot };
4521
+ }
4522
+ var compileNamespace = (definition) => planMigration({ definition });
2543
4523
  var previousId2 = (snapshot) => snapshot.id;
2544
4524
  var IR_VERSION = 1;
2545
4525
  var CLAIMS_CONTRACT = {
@@ -2678,9 +4658,23 @@ var onConflictIrSchema = z.union([
2678
4658
  ]);
2679
4659
  var mutationRootSchema = z.object({
2680
4660
  kind: z.literal("mutation"),
2681
- op: z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk"]),
4661
+ /**
4662
+ * CRUD verbs plus the structured copy-on-write ops. `cowOverride` copies the
4663
+ * synced row into the overlay (INSERT…SELECT, ON CONFLICT DO NOTHING) then
4664
+ * applies `set` — both statements generated by the executor from this
4665
+ * metadata, never authored SQL. `cowRevert` deletes the override so the
4666
+ * coalesce view falls back to synced. Both are gated on a matching synced
4667
+ * row and carry the write policy in `where` like update/delete.
4668
+ */
4669
+ op: z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
2682
4670
  alias: z.string(),
2683
4671
  table: z.string(),
4672
+ /** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
4673
+ cowSynced: z.string().optional(),
4674
+ /** cowOverride: logical properties copied INSERT…SELECT from synced (pk included). */
4675
+ cowCopyColumns: z.array(z.string()).optional(),
4676
+ /** cowOverride: insert-policy WITH CHECK evaluated over the copied rows. */
4677
+ cowCopyCheck: predIrSchema.optional(),
2684
4678
  /** insert/insertOne: rows come from a slot ($object/$objects) or literals. */
2685
4679
  objects: z.union([
2686
4680
  z.object({ slot: z.string() }),
@@ -2701,19 +4695,11 @@ var mutationRootSchema = z.object({
2701
4695
  affectedRows: z.boolean().default(false),
2702
4696
  includeIf: includeIfSchema.optional()
2703
4697
  });
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
4698
  var rootIrSchema = z.discriminatedUnion("kind", [
2712
4699
  selectRootSchema,
2713
4700
  byPkRootSchema,
2714
4701
  aggregateRootSchema,
2715
- mutationRootSchema,
2716
- actionRootSchema
4702
+ mutationRootSchema
2717
4703
  ]);
2718
4704
  var INPUT_REF_KINDS = [
2719
4705
  "boolExp",
@@ -2745,7 +4731,7 @@ var compiledOperationSchema = z.object({
2745
4731
  irVersion: z.literal(IR_VERSION),
2746
4732
  name: z.string(),
2747
4733
  opHash: z.string().regex(/^sha256:[0-9a-f]{64}$/),
2748
- kind: z.enum(["query", "mutation", "action"]),
4734
+ kind: z.enum(["query", "mutation"]),
2749
4735
  requires: z.object({
2750
4736
  capability: z.string().optional(),
2751
4737
  role: z.string().optional()
@@ -2759,13 +4745,24 @@ var tableMetaIrSchema = z.object({
2759
4745
  table: z.string(),
2760
4746
  schema: z.string(),
2761
4747
  typename: z.string(),
4748
+ /**
4749
+ * True when statements over this relation evaluate a claims-gated synced
4750
+ * view: synced twins (`source: gold.*`) materialize as views whose baked
4751
+ * policy reads `current_setting('request.jwt.claims')`, and namespace views
4752
+ * inherit the gate transitively through their declared dependencies. The
4753
+ * executor must stamp that GUC (transaction-scoped) before any statement
4754
+ * touching such a relation, or the synced arm yields zero rows regardless
4755
+ * of the compiled predicates.
4756
+ */
4757
+ claimsGated: z.boolean().default(false),
2762
4758
  columns: z.array(z.object({
2763
4759
  name: z.string(),
2764
4760
  columnName: z.string(),
2765
4761
  type: z.union([z.enum(COLUMN_TYPES), z.object({ enum: z.string(), values: z.array(z.string()) })]),
2766
4762
  nullable: z.boolean()
2767
4763
  })),
2768
- primaryKey: z.array(z.string()).min(1),
4764
+ /** Empty for keyless read-only views; keyed tables and views list logical columns. */
4765
+ primaryKey: z.array(z.string()),
2769
4766
  links: z.array(z.object({
2770
4767
  name: z.string(),
2771
4768
  kind: z.enum(["toOne", "toMany", "toPlatformUser"]),
@@ -2932,6 +4929,8 @@ function validateCapabilityRefs(files, declared) {
2932
4929
  const referenced = [
2933
4930
  ...table.policies?.read ? referencedCapabilities(table.policies.read.ast) : [],
2934
4931
  ...table.policies?.write ? referencedCapabilities(table.policies.write.ast) : [],
4932
+ ...table.policies?.insert ? referencedCapabilities(table.policies.insert.ast) : [],
4933
+ ...table.policies?.update ? referencedCapabilities(table.policies.update.ast) : [],
2935
4934
  ...Object.values(table.actions ?? {}).flatMap((action) => [
2936
4935
  ...action.requires ? referencedCapabilities(action.requires.ast) : [],
2937
4936
  ...referencedExecuteCapabilities(action)
@@ -3089,7 +5088,7 @@ async function readMigrationSql(dir, entry) {
3089
5088
  throw new OrmDefinitionError("missing migration file", path);
3090
5089
  });
3091
5090
  }
3092
- var checksumOf = (text3) => createHash("sha256").update(text3).digest("hex");
5091
+ var checksumOf = (text4) => createHash("sha256").update(text4).digest("hex");
3093
5092
  var migrationName = (ordinal, contentHash) => `${pad(ordinal)}_${contentHash.slice(0, 12)}`;
3094
5093
  function renderMigrationSql({
3095
5094
  name,
@@ -3115,12 +5114,12 @@ async function writeMigration({
3115
5114
  const tip = workspace.journal.entries.at(-1);
3116
5115
  const ordinal = (tip?.ordinal ?? 0) + 1;
3117
5116
  const name = migrationName(ordinal, contentHash);
3118
- const sql3 = renderMigrationSql({ name, namespace: workspace.journal.namespace, contentHash, plan });
5117
+ const sql4 = renderMigrationSql({ name, namespace: workspace.journal.namespace, contentHash, plan });
3119
5118
  const metaDir = join(dir, MIGRATIONS_DIRNAME, META_DIRNAME);
3120
5119
  await mkdir(metaDir, { recursive: true });
3121
5120
  const snapshotText = `${JSON.stringify(plan.snapshot, null, 2)}
3122
5121
  `;
3123
- await writeFile(join(dir, MIGRATIONS_DIRNAME, `${name}.sql`), sql3);
5122
+ await writeFile(join(dir, MIGRATIONS_DIRNAME, `${name}.sql`), sql4);
3124
5123
  await writeFile(join(metaDir, `${pad(ordinal)}.snapshot.json`), snapshotText);
3125
5124
  const journal = {
3126
5125
  ...workspace.journal,
@@ -3131,7 +5130,7 @@ async function writeMigration({
3131
5130
  name,
3132
5131
  content_hash: contentHash,
3133
5132
  previous_content_hash: tip?.content_hash ?? null,
3134
- checksum: checksumOf(sql3),
5133
+ checksum: checksumOf(sql4),
3135
5134
  snapshot_checksum: checksumOf(snapshotText),
3136
5135
  destructive
3137
5136
  }
@@ -3246,8 +5245,8 @@ async function checkWorkspace({
3246
5245
  let previousId3;
3247
5246
  let previousHash = null;
3248
5247
  for (const entry of workspace.journal.entries) {
3249
- const sql3 = await readMigrationSql(dir, entry);
3250
- if (checksumOf(sql3) !== entry.checksum) {
5248
+ const sql4 = await readMigrationSql(dir, entry);
5249
+ if (checksumOf(sql4) !== entry.checksum) {
3251
5250
  log(`${MIGRATIONS_DIRNAME}/${entry.name}.sql does not match its journal checksum \u2014 committed migrations must not be edited`);
3252
5251
  return 1;
3253
5252
  }
@@ -3287,7 +5286,7 @@ async function checkWorkspace({
3287
5286
  log(`migrations in sync \u2014 ${workspace.journal.entries.length} committed, tip ${tip.name}`);
3288
5287
  return 0;
3289
5288
  }
3290
- var firstLine = (sql3) => sql3.split("\n")[0] ?? sql3;
5289
+ var firstLine = (sql4) => sql4.split("\n")[0] ?? sql4;
3291
5290
 
3292
5291
  // src/migrations/from-yaml.ts
3293
5292
  function convertV1DefinitionToCanonical(definition) {
@@ -3319,7 +5318,7 @@ function convertTable(table) {
3319
5318
  description: table.description,
3320
5319
  source: table.source,
3321
5320
  columns: columns.map(([name, column]) => convertColumn(name, column)),
3322
- // Synced tables reflect their source — no columns, no primary key.
5321
+ // Synced tables may declare a typed column/PK contract; untyped remain empty.
3323
5322
  primaryKey: primaryKey2.length > 0 ? primaryKey2 : void 0,
3324
5323
  links: Object.entries(table.links ?? {}).map(([name, link]) => convertLink(table.table, name, link)),
3325
5324
  indexes: (table.indexes ?? []).map((index3) => ({
@@ -3332,7 +5331,10 @@ function convertTable(table) {
3332
5331
  read: table.policies?.read?.source,
3333
5332
  write: table.policies?.write?.source
3334
5333
  },
3335
- actions: Object.entries(table.actions ?? {}).map(([name, action]) => convertAction(table.table, name, action))
5334
+ // v2 has no action surface (writes are mutations); the transition sweep
5335
+ // drops the plpgsql action_* functions, so carrying the metadata would be
5336
+ // dead weight. droppedV1Actions() surfaces them for the emitter's TODO.
5337
+ actions: []
3336
5338
  };
3337
5339
  }
3338
5340
  function convertColumn(name, column) {
@@ -3368,25 +5370,6 @@ function convertLink(table, name, link) {
3368
5370
  description: link.description
3369
5371
  };
3370
5372
  }
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
5373
 
3391
5374
  // src/migrations/v2.ts
3392
5375
  var SHA256_HEX = /^[0-9a-f]{64}$/;
@@ -3409,7 +5392,7 @@ function buildRebaseJournalEntry({ namespace, previousHash, newHash, ordinal, sn
3409
5392
  throw new OrmDefinitionError(`rebase ordinal must be an integer \u2265 2 (a rebase sits on an existing journal tip); got ${ordinal}`);
3410
5393
  }
3411
5394
  const name = migrationName(ordinal, newHash);
3412
- const sql3 = renderRebaseMigrationSql({ name, namespace, previousHash, newHash });
5395
+ const sql4 = renderRebaseMigrationSql({ name, namespace, previousHash, newHash });
3413
5396
  const snapshotText = `${JSON.stringify(snapshot, null, 2)}
3414
5397
  `;
3415
5398
  const entry = {
@@ -3417,11 +5400,11 @@ function buildRebaseJournalEntry({ namespace, previousHash, newHash, ordinal, sn
3417
5400
  name,
3418
5401
  content_hash: newHash,
3419
5402
  previous_content_hash: previousHash,
3420
- checksum: checksumOf(sql3),
5403
+ checksum: checksumOf(sql4),
3421
5404
  snapshot_checksum: checksumOf(snapshotText),
3422
5405
  destructive: false
3423
5406
  };
3424
- return { name, entry, sql: sql3, snapshotText };
5407
+ return { name, entry, sql: sql4, snapshotText };
3425
5408
  }
3426
5409
  function renderRebaseMigrationSql({
3427
5410
  name,
@@ -3494,8 +5477,9 @@ function canonicalPolicies(value) {
3494
5477
  if (value === null || typeof value !== "object") return void 0;
3495
5478
  const record = value;
3496
5479
  const policies = {};
3497
- if (typeof record["read"] === "string") policies["read"] = record["read"];
3498
- if (typeof record["write"] === "string") policies["write"] = record["write"];
5480
+ for (const kind of ["read", "write", "insert", "update"]) {
5481
+ if (typeof record[kind] === "string") policies[kind] = record[kind];
5482
+ }
3499
5483
  return Object.keys(policies).length > 0 ? policies : void 0;
3500
5484
  }
3501
5485
  function canonicalActions(value) {
@@ -3518,6 +5502,6 @@ function canonicalActions(value) {
3518
5502
  );
3519
5503
  }
3520
5504
 
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 };
5505
+ 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
5506
  //# sourceMappingURL=index.js.map
3523
5507
  //# sourceMappingURL=index.js.map