@sequenceholdings/orm 2.0.0 → 2.1.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 (50) hide show
  1. package/README.md +57 -2
  2. package/dist/authoring.cjs +122 -15
  3. package/dist/authoring.cjs.map +1 -1
  4. package/dist/authoring.d.cts +20 -3
  5. package/dist/authoring.d.ts +20 -3
  6. package/dist/authoring.js +122 -15
  7. package/dist/authoring.js.map +1 -1
  8. package/dist/canonical.cjs +83 -9
  9. package/dist/canonical.cjs.map +1 -1
  10. package/dist/canonical.d.cts +47 -1
  11. package/dist/canonical.d.ts +47 -1
  12. package/dist/canonical.js +83 -10
  13. package/dist/canonical.js.map +1 -1
  14. package/dist/cli.cjs +721 -132
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.js +723 -134
  17. package/dist/cli.js.map +1 -1
  18. package/dist/client.cjs.map +1 -1
  19. package/dist/client.js.map +1 -1
  20. package/dist/compiler.cjs +482 -96
  21. package/dist/compiler.cjs.map +1 -1
  22. package/dist/compiler.d.cts +34 -2
  23. package/dist/compiler.d.ts +34 -2
  24. package/dist/compiler.js +483 -97
  25. package/dist/compiler.js.map +1 -1
  26. package/dist/index.cjs +414 -70
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +266 -9
  29. package/dist/index.d.ts +266 -9
  30. package/dist/index.js +404 -72
  31. package/dist/index.js.map +1 -1
  32. package/dist/ir.cjs +146 -12
  33. package/dist/ir.cjs.map +1 -1
  34. package/dist/ir.d.cts +161 -5
  35. package/dist/ir.d.ts +161 -5
  36. package/dist/ir.js +144 -13
  37. package/dist/ir.js.map +1 -1
  38. package/dist/{naming-CTvWSRTf.d.ts → naming-DCn94LJ4.d.ts} +15 -1
  39. package/dist/{naming-DRyinU4u.d.cts → naming-DPglwzkB.d.cts} +15 -1
  40. package/dist/predicate-v2.cjs +130 -11
  41. package/dist/predicate-v2.cjs.map +1 -1
  42. package/dist/predicate-v2.js +130 -11
  43. package/dist/predicate-v2.js.map +1 -1
  44. package/dist/sdl.cjs +460 -13
  45. package/dist/sdl.cjs.map +1 -1
  46. package/dist/sdl.d.cts +1 -1
  47. package/dist/sdl.d.ts +1 -1
  48. package/dist/sdl.js +459 -14
  49. package/dist/sdl.js.map +1 -1
  50. package/package.json +3 -3
package/dist/cli.cjs CHANGED
@@ -39,9 +39,34 @@ var PredicateParseError = class extends Error {
39
39
  }
40
40
  name = "PredicateParseError";
41
41
  };
42
+
43
+ // src/constants.ts
44
+ var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
45
+ var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
46
+ var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
47
+ var MAX_IDENTIFIER_LENGTH = 63;
48
+ var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
49
+ var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
50
+ var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
51
+ var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
52
+ var ORM_SCHEMA_PREFIX = "orm_";
53
+ var CONSUMER_ROLE_SUFFIX = "_consumer";
54
+ var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
55
+ var consumerRoleName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}${CONSUMER_ROLE_SUFFIX}`;
56
+ var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
57
+ var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
58
+ var HASH_DOMAIN = "sequence-orm-definitions\0";
59
+ var MAX_DEFINITION_FILE_BYTES = 1024 * 1024;
60
+ var MAX_GENERATED_SDL_BYTES = 16 * 1024 * 1024;
61
+ var ORM_REGISTRATION_MAX_BYTES = 64 * 1024 * 1024;
62
+ var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
63
+ var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
64
+
65
+ // src/canonical/v2.ts
42
66
  var CANONICAL_FORMAT = 2;
43
67
  var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
44
68
  var identifier = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
69
+ var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
45
70
  var COLUMN_TYPES = [
46
71
  "uuid",
47
72
  "text",
@@ -62,18 +87,33 @@ var canonicalEnumSchema = zod.z.object({
62
87
  var canonicalColumnSchema = zod.z.object({
63
88
  /** Property name in TS (camelCase); columnName is the Postgres name. */
64
89
  name: zod.z.string().min(1).max(63),
65
- columnName: identifier,
90
+ columnName: columnIdentifier,
66
91
  type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier })]),
67
92
  nullable: zod.z.boolean().default(true),
68
93
  unique: zod.z.boolean().default(false),
69
94
  /** Literal default, or one of the allowlisted SQL functions. */
70
95
  default: zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null()]).optional(),
71
96
  defaultFn: zod.z.enum(["gen_random_uuid()", "now()"]).optional(),
97
+ /**
98
+ * Stamp this column from the database clock on UPDATE (`now()`). The
99
+ * column stays insertable (defaults fill gaps) but is omitted from
100
+ * SetInput / settable so callers cannot backdate it.
101
+ */
102
+ onUpdateFn: zod.z.enum(["now()"]).optional(),
72
103
  description: zod.z.string().max(500).optional(),
73
104
  /** Migration hints — same semantics as v1. */
74
- renamedFrom: identifier.optional(),
105
+ renamedFrom: columnIdentifier.optional(),
75
106
  cast: zod.z.string().max(200).optional(),
76
107
  deprecated: zod.z.boolean().default(false)
108
+ }).superRefine((column, ctx) => {
109
+ if (column.onUpdateFn === void 0) return;
110
+ if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
111
+ ctx.addIssue({
112
+ code: "custom",
113
+ path: ["onUpdateFn"],
114
+ message: `onUpdateFn '${column.onUpdateFn}' is only valid on timestamptz/date columns`
115
+ });
116
+ }
77
117
  });
78
118
  var canonicalLinkSchema = zod.z.object({
79
119
  name: zod.z.string().min(1).max(63),
@@ -81,15 +121,15 @@ var canonicalLinkSchema = zod.z.object({
81
121
  /** Target table name (namespace-local). Absent for toPlatformUser. */
82
122
  target: identifier.optional(),
83
123
  /** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
84
- on: identifier,
124
+ on: columnIdentifier,
85
125
  /** Referenced column on the target (default: target's PK / platform user `sub`). */
86
- references: identifier.optional(),
126
+ references: columnIdentifier.optional(),
87
127
  /** enforce: true emits a real FK constraint (same-namespace toOne only). */
88
128
  enforce: zod.z.boolean().default(false),
89
129
  description: zod.z.string().max(500).optional()
90
130
  });
91
131
  var canonicalIndexSchema = zod.z.object({
92
- columns: zod.z.array(identifier).min(1),
132
+ columns: zod.z.array(columnIdentifier).min(1),
93
133
  unique: zod.z.boolean().default(false),
94
134
  name: identifier.optional()
95
135
  });
@@ -337,6 +377,13 @@ var canonicalCowMutationSchema = zod.z.object({
337
377
  }
338
378
  }
339
379
  });
380
+ var canonicalForeignKeySchema = zod.z.object({
381
+ name: identifier,
382
+ columns: zod.z.array(columnIdentifier).min(1).max(8),
383
+ target: identifier,
384
+ references: zod.z.array(columnIdentifier).min(1).max(8),
385
+ deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional()
386
+ }).strict();
340
387
  var canonicalTableSchema = zod.z.object({
341
388
  table: identifier,
342
389
  description: zod.z.string().max(2e3).optional(),
@@ -345,7 +392,7 @@ var canonicalTableSchema = zod.z.object({
345
392
  /** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
346
393
  source: zod.z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
347
394
  columns: zod.z.array(canonicalColumnSchema).default([]),
348
- primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
395
+ primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
349
396
  links: zod.z.array(canonicalLinkSchema).default([]),
350
397
  indexes: zod.z.array(canonicalIndexSchema).default([]),
351
398
  policies: canonicalPoliciesSchema.default({}),
@@ -356,14 +403,16 @@ var canonicalTableSchema = zod.z.object({
356
403
  */
357
404
  actions: zod.z.array(canonicalActionSchema).default([]),
358
405
  /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
359
- cowMutations: zod.z.array(canonicalCowMutationSchema).optional()
406
+ cowMutations: zod.z.array(canonicalCowMutationSchema).optional(),
407
+ /** Omitted by old definitions, preserving their existing content hashes. */
408
+ foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional()
360
409
  });
361
410
  var canonicalViewSchema = zod.z.object({
362
411
  view: identifier,
363
412
  description: zod.z.string().max(2e3).optional(),
364
413
  columns: zod.z.array(canonicalColumnSchema).min(1),
365
414
  /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
366
- primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
415
+ primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
367
416
  links: zod.z.array(canonicalLinkSchema).default([]),
368
417
  policies: zod.z.object({ read: policyExpressionSchema }).strict(),
369
418
  query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
@@ -376,6 +425,9 @@ var canonicalViewSchema = zod.z.object({
376
425
  if (column.default !== void 0 || column.defaultFn !== void 0) {
377
426
  ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare a default` });
378
427
  }
428
+ if (column.onUpdateFn !== void 0) {
429
+ ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare onUpdateFn` });
430
+ }
379
431
  if (column.unique) {
380
432
  ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare unique` });
381
433
  }
@@ -409,6 +461,45 @@ var canonicalDefinitionSchema = zod.z.object({
409
461
  ctx.addIssue({ code: "custom", path: ["tables", index3, "table"], message: `duplicate relation '${table.table}'` });
410
462
  }
411
463
  seen.add(table.table);
464
+ if (table.source !== void 0) {
465
+ for (const [columnIndex, column] of table.columns.entries()) {
466
+ if (column.onUpdateFn !== void 0) {
467
+ ctx.addIssue({
468
+ code: "custom",
469
+ path: ["tables", index3, "columns", columnIndex],
470
+ message: `synced table '${table.table}' column '${column.name}' cannot declare onUpdateFn`
471
+ });
472
+ }
473
+ }
474
+ }
475
+ }
476
+ const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
477
+ for (const [tableIndex, table] of definition.tables.entries()) {
478
+ const names = /* @__PURE__ */ new Set([
479
+ ...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
480
+ ...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
481
+ ...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
482
+ ]);
483
+ for (const [index3, fk] of (table.foreignKeys ?? []).entries()) {
484
+ const fail2 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index3], message });
485
+ if (names.has(fk.name)) fail2(`duplicate foreign key '${fk.name}'`);
486
+ names.add(fk.name);
487
+ const target = fkTables.get(fk.target);
488
+ if (table.source || !target || target.source) {
489
+ fail2("foreign keys must connect owned tables in the same namespace");
490
+ continue;
491
+ }
492
+ if (fk.columns.length !== fk.references.length) fail2("foreign key columns and references must have the same number of columns");
493
+ if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail2("foreign key columns must not repeat");
494
+ for (const [position, name] of fk.columns.entries()) {
495
+ const sourceColumn = table.columns.find((column) => column.columnName === name);
496
+ const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
497
+ if (!sourceColumn || !targetColumn) fail2("foreign key references an unknown column");
498
+ else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail2("foreign key column types must match");
499
+ }
500
+ const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index4) => index4.unique).map((index4) => index4.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
501
+ if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column)))) fail2("foreign key target columns must form a unique key");
502
+ }
412
503
  }
413
504
  const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
414
505
  for (const [tableIndex, table] of definition.tables.entries()) {
@@ -504,7 +595,7 @@ var canonicalDefinitionSchema = zod.z.object({
504
595
  const visiting = /* @__PURE__ */ new Set();
505
596
  const visited = /* @__PURE__ */ new Set();
506
597
  let cycleReported = false;
507
- const visit = (name, path) => {
598
+ const visit2 = (name, path) => {
508
599
  if (visited.has(name) || cycleReported) return;
509
600
  if (visiting.has(name)) {
510
601
  cycleReported = true;
@@ -518,12 +609,12 @@ var canonicalDefinitionSchema = zod.z.object({
518
609
  visiting.add(name);
519
610
  const view = viewByName.get(name);
520
611
  for (const dependency of view?.dependencies ?? []) {
521
- if (viewByName.has(dependency)) visit(dependency, [...path, name]);
612
+ if (viewByName.has(dependency)) visit2(dependency, [...path, name]);
522
613
  }
523
614
  visiting.delete(name);
524
615
  visited.add(name);
525
616
  };
526
- for (const view of views) visit(view.view, []);
617
+ for (const view of views) visit2(view.view, []);
527
618
  });
528
619
  function canonicalRelations(definition) {
529
620
  const views = (definition.views ?? []).map((view) => ({
@@ -544,12 +635,13 @@ function normalizeCanonical(definition) {
544
635
  ...definition,
545
636
  enums: sortByName(definition.enums),
546
637
  tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
547
- const { cowMutations, ...rest } = t2;
638
+ const { cowMutations, foreignKeys, ...rest } = t2;
548
639
  return {
549
640
  ...rest,
550
641
  links: sortByName(t2.links),
551
642
  indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
552
643
  actions: sortByName(t2.actions),
644
+ ...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
553
645
  // Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
554
646
  ...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
555
647
  };
@@ -564,25 +656,6 @@ function normalizeCanonical(definition) {
564
656
  }
565
657
  return normalized;
566
658
  }
567
-
568
- // src/constants.ts
569
- var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
570
- var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
571
- var MAX_IDENTIFIER_LENGTH = 63;
572
- var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
573
- var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
574
- var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
575
- var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
576
- var ORM_SCHEMA_PREFIX = "orm_";
577
- var CONSUMER_ROLE_SUFFIX = "_consumer";
578
- var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
579
- var consumerRoleName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}${CONSUMER_ROLE_SUFFIX}`;
580
- var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
581
- var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
582
- var HASH_DOMAIN = "sequence-orm-definitions\0";
583
- var MAX_DEFINITION_FILE_BYTES = 1024 * 1024;
584
- var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
585
- var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
586
659
  function stableJson(value) {
587
660
  if (value === null || typeof value !== "object") return JSON.stringify(value);
588
661
  if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
@@ -1008,6 +1081,22 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1008
1081
  } else if (columnLevelPk.length > 0) {
1009
1082
  candidate.primaryKey = columnLevelPk;
1010
1083
  }
1084
+ if (table.config.foreignKeys?.length) {
1085
+ if (table.source) throw new OrmDefinitionError(`synced table "${table.tableName}" cannot enforce foreign keys`, table.file);
1086
+ candidate.foreignKeys = table.config.foreignKeys.map((fk) => {
1087
+ const target = resolveRelationRef({ ref: fk.target, fail: (message) => {
1088
+ throw new OrmDefinitionError(message, table.file);
1089
+ } });
1090
+ if (!isSeqTable(target) || !relationsByName.has(target.tableName)) throw new OrmDefinitionError("foreign key target must be a table in this namespace", table.file);
1091
+ return {
1092
+ name: fk.name,
1093
+ columns: fk.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: `foreign key "${fk.name}" column` })),
1094
+ target: target.tableName,
1095
+ references: fk.references.map((ref) => resolveColumnRef({ relation: target, ref, role: `foreign key "${fk.name}" reference` })),
1096
+ ...fk.deferrable ? { deferrable: fk.deferrable } : {}
1097
+ };
1098
+ });
1099
+ }
1011
1100
  for (const index3 of table.config.indexes ?? []) {
1012
1101
  const entry = {
1013
1102
  columns: index3.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
@@ -1060,6 +1149,12 @@ function buildCanonicalView({
1060
1149
  if (column.default !== void 0 || column.defaultFn !== void 0) {
1061
1150
  throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare a default`, view.file);
1062
1151
  }
1152
+ if (column.onUpdateFn !== void 0) {
1153
+ throw new OrmDefinitionError(
1154
+ `view "${view.viewName}": column "${propertyName}" cannot declare onUpdateFn`,
1155
+ view.file
1156
+ );
1157
+ }
1063
1158
  if (column.unique) {
1064
1159
  throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare unique`, view.file);
1065
1160
  }
@@ -1224,6 +1319,10 @@ async function loadNamespaceFromDir(dir, options = {}) {
1224
1319
  fsCache: false,
1225
1320
  ...options.ormModulePath !== void 0 ? {
1226
1321
  alias: {
1322
+ // The public /drizzle barrel re-exports this pinned dependency.
1323
+ // Explicitly map it before the root alias: otherwise jiti appends
1324
+ // "/drizzle" to the overridden module's filename.
1325
+ "@sequenceholdings/orm/drizzle": module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))).resolve("drizzle-orm"),
1227
1326
  "@sequenceholdings/orm": options.ormModulePath,
1228
1327
  "@sequenceholdings/orm/authoring": options.ormModulePath
1229
1328
  }
@@ -1437,6 +1536,7 @@ var ORDER_BY_ENUM_VALUES = [
1437
1536
  "DESC_NULLS_LAST"
1438
1537
  ];
1439
1538
  var PLATFORM_USER_TYPENAME = "PlatformUser";
1539
+ var DATE_BUCKET_ENUM = "DateBucket";
1440
1540
  function tableNames(table) {
1441
1541
  const root = camelCase(table);
1442
1542
  const rootPascal = pascalCase(table);
@@ -1469,7 +1569,12 @@ function tableNames(table) {
1469
1569
  sumFields: `${type}SumFields`,
1470
1570
  avgFields: `${type}AvgFields`,
1471
1571
  aggregateBoolExp: `${type}AggregateBoolExp`,
1472
- aggregateBoolExpCount: `${type}AggregateBoolExpCount`
1572
+ aggregateBoolExpCount: `${type}AggregateBoolExpCount`,
1573
+ aggregateGroupsRoot: `${root}AggregateGroups`,
1574
+ groupByColumnEnum: `${type}GroupByColumn`,
1575
+ groupByInput: `${type}GroupBy`,
1576
+ groupKey: `${type}GroupKey`,
1577
+ aggregateGroup: `${type}AggregateGroup`
1473
1578
  };
1474
1579
  }
1475
1580
 
@@ -1502,7 +1607,18 @@ ${body}
1502
1607
  }
1503
1608
  `;
1504
1609
  }
1505
- var IR_VERSION = 1;
1610
+ var IR_VERSION = 2;
1611
+ var LEGACY_IR_VERSION = 1;
1612
+ var irVersionSchema = zod.z.union([zod.z.literal(LEGACY_IR_VERSION), zod.z.literal(IR_VERSION)]);
1613
+ var operationVersionSchema = zod.z.object({ irVersion: irVersionSchema });
1614
+ function requiredArtifactIrVersion(operations) {
1615
+ let required = LEGACY_IR_VERSION;
1616
+ for (const operation of operations) {
1617
+ const { irVersion } = operationVersionSchema.parse(operation);
1618
+ if (irVersion > required) required = irVersion;
1619
+ }
1620
+ return required;
1621
+ }
1506
1622
  var CLAIMS_CONTRACT = {
1507
1623
  sub: "string",
1508
1624
  org: "string?",
@@ -1629,6 +1745,33 @@ var aggregateRootSchema = zod.z.object({
1629
1745
  nodes: selectionIrSchema.optional(),
1630
1746
  includeIf: includeIfSchema.optional()
1631
1747
  });
1748
+ var DATE_BUCKETS = ["year", "quarter", "month", "week", "day", "hour"];
1749
+ var groupByKeyIrSchema = zod.z.object({
1750
+ column: zod.z.string(),
1751
+ bucket: zod.z.enum(DATE_BUCKETS).optional()
1752
+ });
1753
+ var aggregateGroupsRootSchema = zod.z.object({
1754
+ kind: zod.z.literal("aggregateGroups"),
1755
+ alias: zod.z.string(),
1756
+ table: zod.z.string(),
1757
+ /** Composed at compile time: AND(policy, documentWhere?, dynamicFilter?). */
1758
+ where: predIrSchema,
1759
+ groupBy: zod.z.array(groupByKeyIrSchema).min(1),
1760
+ /**
1761
+ * The `key { … }` projection: which group columns appear in each entry's key
1762
+ * object, in selection order. A subset of `groupBy` (the compiler rejects a
1763
+ * key field that is not a group key — it would be indeterminate within the
1764
+ * group). Empty when the document selects only `aggregate`.
1765
+ */
1766
+ key: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })).default([]),
1767
+ count: zod.z.object({ columns: zod.z.array(zod.z.string()).optional(), distinct: zod.z.boolean().optional() }).optional(),
1768
+ sum: zod.z.array(zod.z.string()).optional(),
1769
+ avg: zod.z.array(zod.z.string()).optional(),
1770
+ /** Bounds the GROUP count, not the scanned rows — see MAX_GROUPS. */
1771
+ limit: limitIrSchema,
1772
+ offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
1773
+ includeIf: includeIfSchema.optional()
1774
+ });
1632
1775
  var writeValuesSchema = zod.z.union([
1633
1776
  zod.z.object({ slot: zod.z.string() }),
1634
1777
  zod.z.object({ entries: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })) })
@@ -1674,12 +1817,15 @@ var mutationRootSchema = zod.z.object({
1674
1817
  insertCheck: predIrSchema.optional(),
1675
1818
  returning: selectionIrSchema.nullable(),
1676
1819
  affectedRows: zod.z.boolean().default(false),
1820
+ /** v2: abort the transaction when the actual write count differs, regardless of selection. */
1821
+ assertAffectedRows: zod.z.number().int().min(0).max(2147483647).optional(),
1677
1822
  includeIf: includeIfSchema.optional()
1678
1823
  });
1679
1824
  var rootIrSchema = zod.z.discriminatedUnion("kind", [
1680
1825
  selectRootSchema,
1681
1826
  byPkRootSchema,
1682
1827
  aggregateRootSchema,
1828
+ aggregateGroupsRootSchema,
1683
1829
  mutationRootSchema
1684
1830
  ]);
1685
1831
  var INPUT_REF_KINDS = [
@@ -1709,7 +1855,7 @@ var variableIrSchema = zod.z.object({
1709
1855
  defaultValue: variableDefaultIrSchema.optional()
1710
1856
  });
1711
1857
  var compiledOperationSchema = zod.z.object({
1712
- irVersion: zod.z.literal(IR_VERSION),
1858
+ irVersion: irVersionSchema,
1713
1859
  name: zod.z.string(),
1714
1860
  opHash: zod.z.string().regex(/^sha256:[0-9a-f]{64}$/),
1715
1861
  kind: zod.z.enum(["query", "mutation"]),
@@ -1721,6 +1867,13 @@ var compiledOperationSchema = zod.z.object({
1721
1867
  /** Every table the op touches — data-token scoping + audit. */
1722
1868
  tableSet: zod.z.array(zod.z.string()).min(1),
1723
1869
  roots: zod.z.array(rootIrSchema).min(1)
1870
+ }).superRefine((operation, ctx) => {
1871
+ for (const [index3, root] of operation.roots.entries()) {
1872
+ if (root.kind !== "mutation" || root.assertAffectedRows === void 0) continue;
1873
+ if (operation.irVersion < 2 || operation.kind !== "mutation" || root.includeIf !== void 0) {
1874
+ ctx.addIssue({ code: "custom", path: ["roots", index3], message: "affected-row assertions require unconditional mutation roots and IR v2" });
1875
+ }
1876
+ }
1724
1877
  });
1725
1878
  var tableMetaIrSchema = zod.z.object({
1726
1879
  table: zod.z.string(),
@@ -1757,18 +1910,29 @@ var tableMetaIrSchema = zod.z.object({
1757
1910
  insertable: zod.z.array(zod.z.string()),
1758
1911
  settable: zod.z.array(zod.z.string()),
1759
1912
  appendable: zod.z.array(zod.z.string()),
1913
+ /**
1914
+ * Logical columns stamped with Postgres `now()` on UPDATE (canonical
1915
+ * `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
1916
+ */
1917
+ touchOnUpdate: zod.z.array(zod.z.string()).optional(),
1760
1918
  constraints: zod.z.array(zod.z.object({ name: zod.z.string(), columns: zod.z.array(zod.z.string()) })),
1761
1919
  /** Depth/node caps for dynamic filter trees on this table. */
1762
1920
  dynamicFilterMaxDepth: zod.z.number().int().positive().default(6),
1763
1921
  dynamicFilterMaxNodes: zod.z.number().int().positive().default(64)
1764
1922
  });
1765
1923
  var compiledArtifactSchema = zod.z.object({
1766
- irVersion: zod.z.literal(IR_VERSION),
1924
+ irVersion: irVersionSchema,
1767
1925
  namespace: zod.z.string(),
1768
1926
  schemaHash: zod.z.string(),
1769
1927
  opsHash: zod.z.string(),
1770
1928
  tables: zod.z.record(zod.z.string(), tableMetaIrSchema),
1771
1929
  operations: zod.z.record(zod.z.string(), compiledOperationSchema)
1930
+ }).superRefine((artifact, ctx) => {
1931
+ for (const [name, operation] of Object.entries(artifact.operations)) {
1932
+ if (operation.irVersion > artifact.irVersion) {
1933
+ ctx.addIssue({ code: "custom", path: ["operations", name, "irVersion"], message: "operation requires a newer IR version than its artifact" });
1934
+ }
1935
+ }
1772
1936
  });
1773
1937
  var IrVersionError = class extends Error {
1774
1938
  constructor(found) {
@@ -1779,19 +1943,19 @@ var IrVersionError = class extends Error {
1779
1943
  function normalizeIr(raw) {
1780
1944
  if (!raw || typeof raw !== "object") throw new IrVersionError(void 0);
1781
1945
  const version = raw.irVersion;
1782
- if (version !== IR_VERSION) throw new IrVersionError(version);
1946
+ if (version !== LEGACY_IR_VERSION && version !== IR_VERSION) throw new IrVersionError(version);
1783
1947
  return compiledArtifactSchema.parse(raw);
1784
1948
  }
1785
1949
 
1786
1950
  // src/predicate-v2/ast.ts
1787
1951
  var POLICY_COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
1788
- function walkPolicy(node, visit) {
1789
- visit(node);
1952
+ function walkPolicy(node, visit2) {
1953
+ visit2(node);
1790
1954
  if (node.kind === "and" || node.kind === "or") {
1791
- for (const operand of node.operands) walkPolicy(operand, visit);
1955
+ for (const operand of node.operands) walkPolicy(operand, visit2);
1792
1956
  }
1793
- if (node.kind === "not") walkPolicy(node.operand, visit);
1794
- if (node.kind === "exists") walkPolicy(node.where, visit);
1957
+ if (node.kind === "not") walkPolicy(node.operand, visit2);
1958
+ if (node.kind === "exists") walkPolicy(node.where, visit2);
1795
1959
  }
1796
1960
 
1797
1961
  // src/predicate-v2/errors.ts
@@ -2297,21 +2461,21 @@ function andPred(...preds) {
2297
2461
  if (operands.length === 1) return operands[0];
2298
2462
  return { kind: "and", operands };
2299
2463
  }
2300
- function walkPred(pred, visit) {
2301
- visit(pred);
2464
+ function walkPred(pred, visit2) {
2465
+ visit2(pred);
2302
2466
  switch (pred.kind) {
2303
2467
  case "and":
2304
2468
  case "or":
2305
- for (const operand of pred.operands) walkPred(operand, visit);
2469
+ for (const operand of pred.operands) walkPred(operand, visit2);
2306
2470
  return;
2307
2471
  case "not":
2308
- walkPred(pred.operand, visit);
2472
+ walkPred(pred.operand, visit2);
2309
2473
  return;
2310
2474
  case "exists":
2311
- walkPred(pred.where, visit);
2475
+ walkPred(pred.where, visit2);
2312
2476
  return;
2313
2477
  case "aggPredicate":
2314
- if (pred.where) walkPred(pred.where, visit);
2478
+ if (pred.where) walkPred(pred.where, visit2);
2315
2479
  return;
2316
2480
  default:
2317
2481
  return;
@@ -2328,12 +2492,12 @@ function predContainsExists(pred) {
2328
2492
  // src/compiler-gql/inject.ts
2329
2493
  function collectPinnedColumns(writePolicy) {
2330
2494
  const key = (pin) => `${pin.column}\0${pin.claim}`;
2331
- const visit = (node) => {
2495
+ const visit2 = (node) => {
2332
2496
  if (node.kind === "and") {
2333
2497
  const merged = [];
2334
2498
  const seen2 = /* @__PURE__ */ new Set();
2335
2499
  for (const operand of node.operands) {
2336
- for (const pin of visit(operand)) {
2500
+ for (const pin of visit2(operand)) {
2337
2501
  if (seen2.has(key(pin))) continue;
2338
2502
  seen2.add(key(pin));
2339
2503
  merged.push(pin);
@@ -2344,9 +2508,9 @@ function collectPinnedColumns(writePolicy) {
2344
2508
  if (node.kind === "or") {
2345
2509
  const [first, ...rest] = node.operands;
2346
2510
  if (first === void 0) return [];
2347
- let common = visit(first);
2511
+ let common = visit2(first);
2348
2512
  for (const operand of rest) {
2349
- const branch = new Set(visit(operand).map(key));
2513
+ const branch = new Set(visit2(operand).map(key));
2350
2514
  common = common.filter((pin) => branch.has(key(pin)));
2351
2515
  if (common.length === 0) break;
2352
2516
  }
@@ -2358,7 +2522,7 @@ function collectPinnedColumns(writePolicy) {
2358
2522
  return [];
2359
2523
  };
2360
2524
  const seen = /* @__PURE__ */ new Set();
2361
- return visit(writePolicy).filter((pin) => {
2525
+ return visit2(writePolicy).filter((pin) => {
2362
2526
  if (seen.has(pin.column)) return false;
2363
2527
  seen.add(pin.column);
2364
2528
  return true;
@@ -2452,6 +2616,11 @@ var PolicyInjector = class {
2452
2616
  where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table)),
2453
2617
  ...root.nodes ? { nodes: this.injectSelection(root.nodes, root.table) } : {}
2454
2618
  };
2619
+ case "aggregateGroups":
2620
+ return {
2621
+ ...root,
2622
+ where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table))
2623
+ };
2455
2624
  case "mutation":
2456
2625
  return this.injectMutation(root);
2457
2626
  }
@@ -2544,20 +2713,33 @@ function collectTableSet(roots, ctx) {
2544
2713
  };
2545
2714
  for (const root of roots) {
2546
2715
  tables.add(root.table);
2547
- if (root.kind === "select" || root.kind === "byPk") {
2548
- visitPred(root.where, root.table);
2549
- visitSelection(root.selection, root.table);
2550
- }
2551
- if (root.kind === "aggregate") {
2552
- visitPred(root.where, root.table);
2553
- if (root.nodes) visitSelection(root.nodes, root.table);
2554
- }
2555
- if (root.kind === "mutation") {
2556
- if (root.cowSynced !== void 0) tables.add(root.cowSynced);
2557
- if (root.where) visitPred(root.where, root.table);
2558
- if (root.insertCheck) visitPred(root.insertCheck, root.table);
2559
- if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
2560
- if (root.returning) visitSelection(root.returning, root.table);
2716
+ switch (root.kind) {
2717
+ case "select":
2718
+ case "byPk":
2719
+ visitPred(root.where, root.table);
2720
+ visitSelection(root.selection, root.table);
2721
+ break;
2722
+ case "aggregate":
2723
+ visitPred(root.where, root.table);
2724
+ if (root.nodes) visitSelection(root.nodes, root.table);
2725
+ break;
2726
+ case "aggregateGroups":
2727
+ visitPred(root.where, root.table);
2728
+ break;
2729
+ case "mutation":
2730
+ if (root.cowSynced !== void 0) tables.add(root.cowSynced);
2731
+ if (root.where) visitPred(root.where, root.table);
2732
+ if (root.insertCheck) visitPred(root.insertCheck, root.table);
2733
+ if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
2734
+ if (root.returning) visitSelection(root.returning, root.table);
2735
+ break;
2736
+ default: {
2737
+ const exhausted = root;
2738
+ throw new DocumentCompileError(
2739
+ `internal: unhandled root kind in tableSet collection ${JSON.stringify(exhausted)}`,
2740
+ "internal"
2741
+ );
2742
+ }
2561
2743
  }
2562
2744
  }
2563
2745
  return [...tables].sort();
@@ -2575,7 +2757,12 @@ function typeNameForTable(table) {
2575
2757
  }
2576
2758
  function queryRootNames(table) {
2577
2759
  const names = tableNames(table);
2578
- return { select: names.selectRoot, byPk: names.byPkRoot, aggregate: names.aggregateRoot };
2760
+ return {
2761
+ select: names.selectRoot,
2762
+ byPk: names.byPkRoot,
2763
+ aggregate: names.aggregateRoot,
2764
+ aggregateGroups: names.aggregateGroupsRoot
2765
+ };
2579
2766
  }
2580
2767
  function mutationRootNames(table) {
2581
2768
  const names = tableNames(table);
@@ -2656,7 +2843,14 @@ function resolveColumnEntries(table, enums, diagnostics) {
2656
2843
  type = { enum: enumDef.name, values: [...enumDef.values] };
2657
2844
  }
2658
2845
  }
2659
- return { name: column.name, columnName: column.columnName, type, nullable: column.nullable, deprecated: column.deprecated };
2846
+ return {
2847
+ name: column.name,
2848
+ columnName: column.columnName,
2849
+ type,
2850
+ nullable: column.nullable,
2851
+ deprecated: column.deprecated,
2852
+ onUpdateFn: column.onUpdateFn !== void 0
2853
+ };
2660
2854
  });
2661
2855
  }
2662
2856
  function logicalColumn(entries, ref) {
@@ -2705,6 +2899,7 @@ function buildCompileContext(rawCanonical) {
2705
2899
  primaryKey2.push(resolved);
2706
2900
  }
2707
2901
  const active = entries.filter((c) => !c.deprecated);
2902
+ const touchOnUpdate = isView ? [] : active.filter((c) => c.onUpdateFn).map((c) => c.name);
2708
2903
  const constraints = [];
2709
2904
  if (primaryKey2.length > 0) constraints.push({ name: `${table.table}_pkey`, columns: [...primaryKey2] });
2710
2905
  for (const column of table.columns) {
@@ -2725,14 +2920,15 @@ function buildCompileContext(rawCanonical) {
2725
2920
  // Synced twins (and views reading them, resolved transitively below)
2726
2921
  // are claims-gated — see tableMetaIrSchema.claimsGated.
2727
2922
  claimsGated: relationIsClaimsGated(table.table),
2728
- columns: entries.map(({ deprecated: _deprecated, ...column }) => column),
2923
+ columns: entries.map(({ deprecated: _deprecated, onUpdateFn: _onUpdateFn, ...column }) => column),
2729
2924
  primaryKey: isView ? primaryKey2 : primaryKey2.length > 0 ? primaryKey2 : ["id"],
2730
2925
  links: [],
2731
2926
  filterable: entries.map((c) => c.name),
2732
2927
  orderable: entries.filter((c) => c.type !== "jsonb").map((c) => c.name),
2733
2928
  insertable: isView ? [] : active.map((c) => c.name),
2734
- settable: isView ? [] : active.map((c) => c.name),
2929
+ settable: isView ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
2735
2930
  appendable: isView ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
2931
+ ...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
2736
2932
  constraints: isView ? [] : constraints,
2737
2933
  dynamicFilterMaxDepth: 6,
2738
2934
  dynamicFilterMaxNodes: 64
@@ -2753,7 +2949,7 @@ function buildCompileContext(rawCanonical) {
2753
2949
  });
2754
2950
  continue;
2755
2951
  }
2756
- const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false })) : columnEntries.get(targetName);
2952
+ const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false, onUpdateFn: false })) : columnEntries.get(targetName);
2757
2953
  const onEntries = link.kind === "toMany" ? targetEntries : entries;
2758
2954
  const referencesEntries = link.kind === "toMany" ? entries : targetEntries;
2759
2955
  const on = logicalColumn(onEntries, link.on);
@@ -2850,6 +3046,9 @@ function buildCompileContext(rawCanonical) {
2850
3046
  queryRoots.set(roots.byPk, { kind: "byPk", table: table.table });
2851
3047
  }
2852
3048
  queryRoots.set(roots.aggregate, { kind: "aggregate", table: table.table });
3049
+ if (table.columns.some((column) => column.type !== "jsonb")) {
3050
+ queryRoots.set(roots.aggregateGroups, { kind: "aggregateGroups", table: table.table });
3051
+ }
2853
3052
  const hasWritePolicy = table.policies.write !== void 0 || table.policies.insert !== void 0 || table.policies.update !== void 0;
2854
3053
  if (!viewNames.has(table.table) && table.source === void 0 && hasWritePolicy) {
2855
3054
  for (const [op, field] of Object.entries(mutationRootNames(table.table))) {
@@ -2985,6 +3184,16 @@ function computeOpsHash(opHashes) {
2985
3184
  }
2986
3185
  var DEFAULT_LIMIT = 100;
2987
3186
  var MAX_LIMIT = 1e3;
3187
+ var MAX_GROUPS = 1e3;
3188
+ var DATE_BUCKET_BY_ENUM = {
3189
+ YEAR: "year",
3190
+ QUARTER: "quarter",
3191
+ MONTH: "month",
3192
+ WEEK: "week",
3193
+ DAY: "day",
3194
+ HOUR: "hour"
3195
+ };
3196
+ var BUCKETABLE_TYPES = /* @__PURE__ */ new Set(["timestamptz", "date"]);
2988
3197
  var BOOLEXP_OPS = {
2989
3198
  _eq: "eq",
2990
3199
  _neq: "neq",
@@ -3016,6 +3225,14 @@ function fail(message, code, candidates = []) {
3016
3225
  function argsByName(field) {
3017
3226
  return new Map((field.arguments ?? []).map((arg) => [arg.name.value, arg]));
3018
3227
  }
3228
+ function* aggregateBlockFields(selectionSet) {
3229
+ for (const node of selectionSet.selections) {
3230
+ if (node.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
3231
+ if (node.name.value === "__typename") continue;
3232
+ if (node.alias) fail(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
3233
+ yield node;
3234
+ }
3235
+ }
3019
3236
  function assertKnownArgs(field, allowed) {
3020
3237
  for (const arg of field.arguments ?? []) {
3021
3238
  if (!allowed.includes(arg.name.value)) {
@@ -3316,11 +3533,8 @@ var OperationLowerer = class {
3316
3533
  lowerAggregateSelection(selectionSet, table) {
3317
3534
  const result = {};
3318
3535
  let sawAggregate = false;
3319
- for (const node of selectionSet.selections) {
3320
- if (node.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
3536
+ for (const node of aggregateBlockFields(selectionSet)) {
3321
3537
  const name = node.name.value;
3322
- if (name === "__typename") continue;
3323
- if (node.alias) fail(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
3324
3538
  if (name === "nodes") {
3325
3539
  if (!node.selectionSet) fail("'nodes' needs a selection set", "invalid-aggregate");
3326
3540
  result.nodes = this.lowerSelection(node.selectionSet, table);
@@ -3330,48 +3544,116 @@ var OperationLowerer = class {
3330
3544
  if (sawAggregate) fail("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
3331
3545
  sawAggregate = true;
3332
3546
  if (!node.selectionSet) fail("'aggregate' needs a selection set", "invalid-aggregate");
3333
- for (const aggField of node.selectionSet.selections) {
3334
- if (aggField.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
3335
- const aggName = aggField.name.value;
3336
- if (aggName === "__typename") continue;
3337
- if (aggField.alias) fail(`aliases are not supported inside aggregate blocks ('${aggField.alias.value}')`, "invalid-aggregate");
3338
- if (aggName === "count") {
3339
- assertKnownArgs(aggField, ["columns", "distinct"]);
3340
- const args = argsByName(aggField);
3341
- const count = {};
3342
- const columns = args.get("columns");
3343
- if (columns) count.columns = this.lowerDistinctOn(columns.value, table);
3344
- const distinct = args.get("distinct");
3345
- if (distinct) {
3346
- if (distinct.value.kind !== graphql.Kind.BOOLEAN) fail("count(distinct:) must be a boolean literal", "invalid-aggregate");
3347
- count.distinct = distinct.value.value;
3348
- }
3349
- result.count = count;
3350
- continue;
3547
+ Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
3548
+ }
3549
+ if (!sawAggregate && !result.nodes) fail("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
3550
+ return result;
3551
+ }
3552
+ /**
3553
+ * The `{ key {…} aggregate {…} }` block of a grouped aggregate root. Same
3554
+ * `aggregate` sub-block as the ungrouped shape (shared via
3555
+ * `lowerAggregateFields`), but `key` replaces `nodes` — see the IR comment on
3556
+ * `aggregateGroupsRootSchema` for why grouped roots have no `nodes`.
3557
+ */
3558
+ lowerGroupedAggregateSelection(selectionSet, table, groupBy) {
3559
+ const groupColumns = new Set(groupBy.map((entry) => entry.column));
3560
+ const result = { key: [] };
3561
+ let sawAggregate = false;
3562
+ let sawKey = false;
3563
+ for (const node of aggregateBlockFields(selectionSet)) {
3564
+ const name = node.name.value;
3565
+ if (name === "key") {
3566
+ if (sawKey) fail("one key block per grouped aggregate", "invalid-aggregate");
3567
+ sawKey = true;
3568
+ if (!node.selectionSet) fail("'key' needs a selection set", "invalid-aggregate");
3569
+ result.key = this.lowerGroupKeySelection(node.selectionSet, table, groupColumns);
3570
+ continue;
3571
+ }
3572
+ if (name === "nodes") {
3573
+ fail("'nodes' is not available on a grouped aggregate \u2014 query the rows separately", "invalid-aggregate", ["key", "aggregate"]);
3574
+ }
3575
+ if (name !== "aggregate") fail(`unknown grouped-aggregate field '${name}'`, "invalid-aggregate", ["key", "aggregate"]);
3576
+ if (sawAggregate) fail("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
3577
+ sawAggregate = true;
3578
+ if (!node.selectionSet) fail("'aggregate' needs a selection set", "invalid-aggregate");
3579
+ Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
3580
+ }
3581
+ const rendersAggregate = result.count !== void 0 || (result.sum?.length ?? 0) > 0 || (result.avg?.length ?? 0) > 0;
3582
+ if (result.key.length === 0 && !rendersAggregate) {
3583
+ fail(
3584
+ "grouped aggregate needs at least one key column or an aggregate \u2014 a block selecting only __typename produces nothing",
3585
+ "invalid-aggregate",
3586
+ ["key", "aggregate"]
3587
+ );
3588
+ }
3589
+ return result;
3590
+ }
3591
+ /**
3592
+ * `key { month officer }` → the projected group columns. Every field must be
3593
+ * one of the query's own group keys: any other column is indeterminate
3594
+ * within a group, so selecting it is a document error rather than an
3595
+ * arbitrary pick.
3596
+ */
3597
+ lowerGroupKeySelection(selectionSet, table, groupColumns) {
3598
+ const key = [];
3599
+ for (const node of selectionSet.selections) {
3600
+ if (node.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
3601
+ const name = node.name.value;
3602
+ if (name === "__typename") continue;
3603
+ if (!this.column(table, name)) {
3604
+ fail(`unknown key column '${name}' on '${table}'`, "unknown-column", [...groupColumns]);
3605
+ }
3606
+ if (!groupColumns.has(name)) {
3607
+ fail(
3608
+ `key column '${name}' is not one of this query's groupBy keys \u2014 it has no single value within a group`,
3609
+ "invalid-group-key",
3610
+ [...groupColumns]
3611
+ );
3612
+ }
3613
+ key.push({ alias: node.alias?.value ?? name, column: name });
3614
+ }
3615
+ return key;
3616
+ }
3617
+ /** The `count` / `sum {…}` / `avg {…}` fields inside an `aggregate { … }` block. */
3618
+ lowerAggregateFields(selectionSet, table) {
3619
+ const result = {};
3620
+ for (const aggField of aggregateBlockFields(selectionSet)) {
3621
+ const aggName = aggField.name.value;
3622
+ if (aggName === "count") {
3623
+ assertKnownArgs(aggField, ["columns", "distinct"]);
3624
+ const args = argsByName(aggField);
3625
+ const count = {};
3626
+ const columns = args.get("columns");
3627
+ if (columns) count.columns = this.lowerDistinctOn(columns.value, table);
3628
+ const distinct = args.get("distinct");
3629
+ if (distinct) {
3630
+ if (distinct.value.kind !== graphql.Kind.BOOLEAN) fail("count(distinct:) must be a boolean literal", "invalid-aggregate");
3631
+ count.distinct = distinct.value.value;
3351
3632
  }
3352
- if (aggName === "sum" || aggName === "avg") {
3353
- if (!aggField.selectionSet) fail(`'${aggName}' needs a column selection`, "invalid-aggregate");
3354
- const columns = [];
3355
- for (const columnField of aggField.selectionSet.selections) {
3356
- if (columnField.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
3357
- if (columnField.name.value === "__typename") continue;
3358
- const column = this.column(table, columnField.name.value);
3359
- if (!column || typeof column.type !== "string" || !NUMERIC_TYPES.has(column.type)) {
3360
- fail(
3361
- `'${aggName}' supports numeric columns only, got '${columnField.name.value}' on '${table}'`,
3362
- "invalid-aggregate",
3363
- this.meta(table).columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type)).map((c) => c.name)
3364
- );
3365
- }
3366
- columns.push(columnField.name.value);
3633
+ result.count = count;
3634
+ continue;
3635
+ }
3636
+ if (aggName === "sum" || aggName === "avg") {
3637
+ if (!aggField.selectionSet) fail(`'${aggName}' needs a column selection`, "invalid-aggregate");
3638
+ const columns = [];
3639
+ for (const columnField of aggField.selectionSet.selections) {
3640
+ if (columnField.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
3641
+ if (columnField.name.value === "__typename") continue;
3642
+ const column = this.column(table, columnField.name.value);
3643
+ if (!column || typeof column.type !== "string" || !NUMERIC_TYPES.has(column.type)) {
3644
+ fail(
3645
+ `'${aggName}' supports numeric columns only, got '${columnField.name.value}' on '${table}'`,
3646
+ "invalid-aggregate",
3647
+ this.meta(table).columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type)).map((c) => c.name)
3648
+ );
3367
3649
  }
3368
- result[aggName] = columns;
3369
- continue;
3650
+ columns.push(columnField.name.value);
3370
3651
  }
3371
- fail(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
3652
+ result[aggName] = columns;
3653
+ continue;
3372
3654
  }
3655
+ fail(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
3373
3656
  }
3374
- if (!sawAggregate && !result.nodes) fail("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
3375
3657
  return result;
3376
3658
  }
3377
3659
  // -------------------------------------------------------------------------
@@ -3381,19 +3663,49 @@ var OperationLowerer = class {
3381
3663
  const roots = [];
3382
3664
  for (const node of operation.selectionSet.selections) {
3383
3665
  if (node.kind !== graphql.Kind.FIELD) fail("fragments are not supported at the operation root", "invalid-root");
3666
+ if (node.selectionSet) graphql.visit(node.selectionSet, {
3667
+ Directive(directive) {
3668
+ if (directive.name.value === "assertAffectedRows") {
3669
+ fail("@assertAffectedRows is supported on mutation roots only", "invalid-affected-row-assertion");
3670
+ }
3671
+ }
3672
+ });
3673
+ const assertion = this.parseAffectedRowAssertion(node, kind);
3384
3674
  const gate = this.parseRootDirectives(node);
3385
3675
  if (gate.omit) continue;
3386
3676
  const root = kind === "query" ? this.lowerQueryRoot(node) : this.lowerMutationRoot(node);
3387
- roots.push(gate.includeIf ? { ...root, includeIf: gate.includeIf } : root);
3677
+ const guarded = root.kind === "mutation" && assertion !== void 0 ? { ...root, assertAffectedRows: assertion } : root;
3678
+ roots.push(gate.includeIf ? { ...guarded, includeIf: gate.includeIf } : guarded);
3388
3679
  }
3389
3680
  if (roots.length === 0) fail("operation has no roots (all statically skipped?)", "invalid-root");
3390
3681
  return roots;
3391
3682
  }
3683
+ parseAffectedRowAssertion(node, kind) {
3684
+ const assertions = node.directives?.filter((directive) => directive.name.value === "assertAffectedRows") ?? [];
3685
+ if (!assertions.length) return void 0;
3686
+ if (kind !== "mutation" || assertions.length !== 1) {
3687
+ fail("@assertAffectedRows is supported once per mutation root", "invalid-affected-row-assertion");
3688
+ }
3689
+ if (node.directives?.some((directive) => ["include", "skip"].includes(directive.name.value))) {
3690
+ fail("an @assertAffectedRows root cannot use @include or @skip", "invalid-affected-row-assertion");
3691
+ }
3692
+ const args = assertions[0].arguments;
3693
+ const arg = args?.[0];
3694
+ if (args?.length !== 1 || arg?.name.value !== "exactly" || arg.value.kind !== graphql.Kind.INT) {
3695
+ fail("@assertAffectedRows(exactly:) requires a document-authored nonnegative Int literal", "invalid-affected-row-assertion");
3696
+ }
3697
+ const count = Number(arg.value.value);
3698
+ if (!Number.isInteger(count) || count < 0 || count > 2147483647) {
3699
+ fail("@assertAffectedRows(exactly:) must be between 0 and 2147483647", "invalid-affected-row-assertion");
3700
+ }
3701
+ return count;
3702
+ }
3392
3703
  parseRootDirectives(node) {
3393
3704
  let includeIf;
3394
3705
  let omit = false;
3395
3706
  for (const directive of node.directives ?? []) {
3396
3707
  const name = directive.name.value;
3708
+ if (name === "assertAffectedRows") continue;
3397
3709
  if (name !== "include" && name !== "skip") {
3398
3710
  fail(`directive @${name} is not supported on fields`, "unknown-directive", ["include", "skip"]);
3399
3711
  }
@@ -3442,6 +3754,9 @@ var OperationLowerer = class {
3442
3754
  const pk = this.lowerPkArgs(node, table);
3443
3755
  return { kind: "byPk", alias, table, selection: this.lowerSelection(node.selectionSet, table), pk, where: CONST_TRUE };
3444
3756
  }
3757
+ if (rootInfo.kind === "aggregateGroups") {
3758
+ return this.lowerAggregateGroupsRoot(node, { alias, table });
3759
+ }
3445
3760
  assertKnownArgs(node, ["where"]);
3446
3761
  if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
3447
3762
  const whereArg = args.get("where");
@@ -3454,6 +3769,108 @@ var OperationLowerer = class {
3454
3769
  ...aggregate
3455
3770
  };
3456
3771
  }
3772
+ /** `<t>AggregateGroups(where:, groupBy:, limit:, offset:)` → an aggregateGroups root. */
3773
+ lowerAggregateGroupsRoot(node, { alias, table }) {
3774
+ assertKnownArgs(node, ["where", "groupBy", "limit", "offset"]);
3775
+ if (!node.selectionSet) fail(`'${node.name.value}' needs a selection set`, "invalid-selection");
3776
+ const args = argsByName(node);
3777
+ const groupByArg = args.get("groupBy");
3778
+ if (!groupByArg) fail(`'${node.name.value}' requires a 'groupBy' argument`, "missing-group-by");
3779
+ const whereArg = args.get("where");
3780
+ const groupBy = this.lowerGroupBy(groupByArg.value, table);
3781
+ const aggregate = this.lowerGroupedAggregateSelection(node.selectionSet, table, groupBy);
3782
+ const offset = this.lowerOffset(args.get("offset")?.value);
3783
+ return {
3784
+ kind: "aggregateGroups",
3785
+ alias,
3786
+ table,
3787
+ where: whereArg ? this.lowerBoolExp(whereArg.value, table) : CONST_TRUE,
3788
+ groupBy,
3789
+ key: aggregate.key,
3790
+ ...aggregate.count ? { count: aggregate.count } : {},
3791
+ ...aggregate.sum ? { sum: aggregate.sum } : {},
3792
+ ...aggregate.avg ? { avg: aggregate.avg } : {},
3793
+ limit: this.lowerGroupLimit(args.get("limit")?.value),
3794
+ ...offset ? { offset } : {}
3795
+ };
3796
+ }
3797
+ /**
3798
+ * `groupBy: [{ column: startedAt, bucket: MONTH }, …]` → GroupByKeyIr[].
3799
+ *
3800
+ * Document-authored only: a whole-argument variable is rejected rather than
3801
+ * becoming a §4f slot. Group keys choose which expressions reach GROUP BY,
3802
+ * and dynamic-input.ts refuses aggregation shapes from client input on the
3803
+ * same reasoning — `where` stays dynamic, which is what runtime filtering
3804
+ * actually needs.
3805
+ */
3806
+ lowerGroupBy(node, table) {
3807
+ if (node.kind === graphql.Kind.VARIABLE) {
3808
+ fail(
3809
+ `groupBy on '${table}' must be written in the document \u2014 it cannot come from a variable`,
3810
+ "dynamic-group-by"
3811
+ );
3812
+ }
3813
+ const items = node.kind === graphql.Kind.LIST ? node.values : [node];
3814
+ if (items.length === 0) fail(`groupBy on '${table}' needs at least one key`, "invalid-group-by");
3815
+ const seen = /* @__PURE__ */ new Set();
3816
+ return items.map((item) => {
3817
+ if (item.kind !== graphql.Kind.OBJECT) {
3818
+ fail(`groupBy entries on '${table}' must be objects like { column: foo }`, "invalid-group-by");
3819
+ }
3820
+ const fields = new Map(item.fields.map((field) => [field.name.value, field.value]));
3821
+ for (const key of fields.keys()) {
3822
+ if (key !== "column" && key !== "bucket") {
3823
+ fail(`unknown groupBy field '${key}' on '${table}'`, "invalid-group-by", ["column", "bucket"]);
3824
+ }
3825
+ }
3826
+ const columnNode = fields.get("column");
3827
+ if (!columnNode) fail(`groupBy entries on '${table}' need a 'column'`, "invalid-group-by");
3828
+ if (columnNode.kind !== graphql.Kind.ENUM) {
3829
+ fail(`groupBy column on '${table}' must be a group-by column enum value`, "invalid-group-by");
3830
+ }
3831
+ const column = this.column(table, columnNode.value);
3832
+ if (!column) {
3833
+ fail(`unknown groupBy column '${columnNode.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
3834
+ }
3835
+ if (column.type === "jsonb") {
3836
+ fail(`groupBy column '${table}.${columnNode.value}' is jsonb and cannot be a group key`, "invalid-group-by");
3837
+ }
3838
+ const bucketNode = fields.get("bucket");
3839
+ let bucket;
3840
+ if (bucketNode) {
3841
+ if (bucketNode.kind !== graphql.Kind.ENUM) {
3842
+ fail(`groupBy bucket on '${table}.${columnNode.value}' must be a DateBucket enum value`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
3843
+ }
3844
+ bucket = DATE_BUCKET_BY_ENUM[bucketNode.value];
3845
+ if (!bucket) {
3846
+ fail(`unknown DateBucket '${bucketNode.value}'`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
3847
+ }
3848
+ if (typeof column.type !== "string" || !BUCKETABLE_TYPES.has(column.type)) {
3849
+ fail(
3850
+ `groupBy bucket is only valid on date/timestamptz columns \u2014 '${table}.${columnNode.value}' is ${typeof column.type === "string" ? column.type : "an enum"}`,
3851
+ "invalid-group-by"
3852
+ );
3853
+ }
3854
+ }
3855
+ if (seen.has(columnNode.value)) {
3856
+ fail(`duplicate groupBy column '${columnNode.value}' on '${table}'`, "invalid-group-by");
3857
+ }
3858
+ seen.add(columnNode.value);
3859
+ return bucket ? { column: columnNode.value, bucket } : { column: columnNode.value };
3860
+ });
3861
+ }
3862
+ /** Like lowerLimit, but bounded by MAX_GROUPS and defaulting to it. */
3863
+ lowerGroupLimit(node) {
3864
+ const base = { default: MAX_GROUPS, max: MAX_GROUPS };
3865
+ if (!node) return base;
3866
+ if (node.kind === graphql.Kind.VARIABLE) return { ...base, variable: node.name.value };
3867
+ if (node.kind === graphql.Kind.INT) {
3868
+ const value = Number(node.value);
3869
+ if (value < 1) fail("limit must be a positive integer", "invalid-limit");
3870
+ return { ...base, value: Math.min(value, MAX_GROUPS) };
3871
+ }
3872
+ fail("limit must be an integer literal or a variable", "invalid-limit");
3873
+ }
3457
3874
  /** byPk-style pk arguments: one argument per primary-key column, exactly. */
3458
3875
  lowerPkArgs(node, table) {
3459
3876
  const meta = this.meta(table);
@@ -3963,7 +4380,7 @@ function compileNamespace({ canonical, sdl, documents }) {
3963
4380
  const loweredRoots = lowerOperation({ restricted: { ...restricted, definition: flattened }, ctx });
3964
4381
  const { roots, tableSet } = injectPolicies({ roots: loweredRoots, ctx });
3965
4382
  operations[restricted.name] = {
3966
- irVersion: IR_VERSION,
4383
+ irVersion: roots.some((root) => root.kind === "mutation" && root.assertAffectedRows !== void 0) ? IR_VERSION : LEGACY_IR_VERSION,
3967
4384
  name: restricted.name,
3968
4385
  opHash: computeOpHash({ definition: flattened, schemaHash }),
3969
4386
  kind: restricted.kind,
@@ -3990,7 +4407,7 @@ function compileNamespace({ canonical, sdl, documents }) {
3990
4407
  }
3991
4408
  if (diagnostics.some((d) => d.severity === "error")) return { ok: false, diagnostics };
3992
4409
  const artifact = {
3993
- irVersion: IR_VERSION,
4410
+ irVersion: requiredArtifactIrVersion(Object.values(operations)),
3994
4411
  namespace: ctx.namespace,
3995
4412
  schemaHash,
3996
4413
  opsHash: computeOpsHash(Object.values(operations).map((op) => op.opHash)),
@@ -4190,12 +4607,12 @@ function castParts(cast) {
4190
4607
 
4191
4608
  // src/predicate/ast.ts
4192
4609
  var COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
4193
- function walkPredicate(predicate, visit) {
4194
- visit(predicate);
4610
+ function walkPredicate(predicate, visit2) {
4611
+ visit2(predicate);
4195
4612
  if (predicate.kind === "and" || predicate.kind === "or") {
4196
- for (const operand of predicate.operands) walkPredicate(operand, visit);
4613
+ for (const operand of predicate.operands) walkPredicate(operand, visit2);
4197
4614
  }
4198
- if (predicate.kind === "not") walkPredicate(predicate.operand, visit);
4615
+ if (predicate.kind === "not") walkPredicate(predicate.operand, visit2);
4199
4616
  }
4200
4617
  function predicateOperands(predicate) {
4201
4618
  const operands = [];
@@ -5170,7 +5587,7 @@ var FACTORIES2 = {
5170
5587
  var SCHEMA_IMPORT_KEY2 = "$schema";
5171
5588
  var enumImportKey = (name) => `$enum_${name}`;
5172
5589
  var columnTablesV2 = (definition) => definition.tables.filter((table) => !table.source);
5173
- function buildDrizzleEntitiesV2(definition) {
5590
+ function buildDrizzleEntitiesV2(definition, options = {}) {
5174
5591
  const schemaName = ormSchemaName(definition.namespace);
5175
5592
  const schema = pgCore.pgSchema(schemaName);
5176
5593
  const imports = { [SCHEMA_IMPORT_KEY2]: schema };
@@ -5185,11 +5602,19 @@ function buildDrizzleEntitiesV2(definition) {
5185
5602
  }
5186
5603
  const byName = new Map(definition.tables.map((table) => [table.table, table]));
5187
5604
  for (const table of sortByEnforcedLinks2(columnTablesV2(definition))) {
5188
- const drizzleTable = buildTable2(schema, table, built, enums, byName);
5605
+ const drizzleTable = buildTable2(schema, table, built, enums, byName, options.introspection ?? false);
5189
5606
  built.set(table.table, drizzleTable);
5190
5607
  imports[table.table] = drizzleTable;
5191
5608
  tables[table.table] = drizzleTable;
5192
5609
  }
5610
+ if (options.introspection) {
5611
+ for (const table of definition.tables.filter((table2) => table2.source)) {
5612
+ const columns = Object.fromEntries(
5613
+ table.columns.map((column) => [column.columnName, buildViewColumnV2({ view: table.table, column, enums })])
5614
+ );
5615
+ imports[table.table] = schema.view(table.table, columns).existing();
5616
+ }
5617
+ }
5193
5618
  for (const view of definition.views ?? []) {
5194
5619
  const columns = Object.fromEntries(
5195
5620
  view.columns.map((column) => [column.columnName, buildViewColumnV2({ view: view.view, column, enums })])
@@ -5433,9 +5858,10 @@ function replaceQualifiedRelation(sql4, sourceSchema, sourceName, boundSchema, b
5433
5858
  function generateSnapshotV2(definition, previousId3) {
5434
5859
  const { imports, schemaName } = buildDrizzleEntitiesV2(definition);
5435
5860
  const snapshot = api.generateDrizzleJson(imports, previousId3, [schemaName]);
5436
- if ((definition.views?.length ?? 0) > 0) {
5861
+ if ((definition.views?.length ?? 0) > 0 || definition.tables.some((table) => table.foreignKeys?.length)) {
5437
5862
  const extended = snapshot;
5438
5863
  extended._sequenceOrm = {
5864
+ ...definition.tables.some((table) => table.foreignKeys?.length) ? { foreignKeys: Object.fromEntries(definition.tables.flatMap((table) => (table.foreignKeys ?? []).map((fk) => [`${table.table}.${fk.name}`, structuredClone(fk)]))) } : {},
5439
5865
  viewDependencies: Object.fromEntries(
5440
5866
  (definition.views ?? []).map((view) => [view.view, [...view.dependencies].sort()])
5441
5867
  )
@@ -5453,7 +5879,7 @@ function buildViewColumnV2({
5453
5879
  if (!column.nullable) builder = builder.notNull();
5454
5880
  return builder;
5455
5881
  }
5456
- function buildTable2(schema, table, built, enums, byName) {
5882
+ function buildTable2(schema, table, built, enums, byName, introspection) {
5457
5883
  const primaryKeyColumns2 = table.primaryKey ?? [];
5458
5884
  if (primaryKeyColumns2.length === 0) {
5459
5885
  throw new OrmDefinitionError(`table '${table.table}' declares no primaryKey`);
@@ -5469,13 +5895,22 @@ function buildTable2(schema, table, built, enums, byName) {
5469
5895
  const columns = Object.fromEntries(
5470
5896
  table.columns.map((column) => [
5471
5897
  column.columnName,
5472
- buildColumnV2({ table: table.table, column, singlePrimaryKey, compositeMembers, enums })
5898
+ buildColumnV2({ table: table.table, column, singlePrimaryKey, compositeMembers, enums, introspection })
5473
5899
  ])
5474
5900
  );
5475
5901
  const drizzleTable = schema.table(table.table, columns, (self) => [
5476
5902
  ...buildCompositePrimaryKey(table, primaryKeyColumns2, self),
5477
5903
  ...buildIndexes2(table, self),
5478
- ...buildForeignKeys2(table, self, built, byName)
5904
+ ...buildForeignKeys2(table, self, built, byName),
5905
+ ...(table.foreignKeys ?? []).map((fk) => {
5906
+ const target = fk.target === table.table ? self : built.get(fk.target);
5907
+ if (!target) throw new OrmDefinitionError(`foreign key '${fk.name}' has no target '${fk.target}'`);
5908
+ return pgCore.foreignKey({
5909
+ name: fk.name,
5910
+ columns: fk.columns.map((column) => self[column]),
5911
+ foreignColumns: fk.references.map((column) => target[column])
5912
+ });
5913
+ })
5479
5914
  ]);
5480
5915
  return drizzleTable.enableRLS();
5481
5916
  }
@@ -5492,7 +5927,8 @@ function buildColumnV2({
5492
5927
  column,
5493
5928
  singlePrimaryKey,
5494
5929
  compositeMembers,
5495
- enums
5930
+ enums,
5931
+ introspection
5496
5932
  }) {
5497
5933
  const factory = resolveColumnFactory(table, column, enums);
5498
5934
  let builder = factory(column.columnName);
@@ -5513,7 +5949,7 @@ function buildColumnV2({
5513
5949
  }
5514
5950
  builder = builder.default(drizzleOrm.sql.raw(column.defaultFn));
5515
5951
  } else if (column.default !== void 0 && column.default !== null) {
5516
- builder = builder.default(column.default);
5952
+ builder = introspection && column.type === "jsonb" && typeof column.default === "string" ? builder.default(drizzleOrm.sql.raw(`${quoteLiteral(column.default)}::jsonb`)) : builder.default(column.default);
5517
5953
  }
5518
5954
  return builder;
5519
5955
  }
@@ -5698,7 +6134,8 @@ async function planMigrationV2({
5698
6134
  assertValidViewDependencies(canonical);
5699
6135
  const hints = applyMigrationHintsV2(previous, canonical);
5700
6136
  const snapshot = generateSnapshotV2(canonical, previousId(previous));
5701
- const baseDdl = await diffStatements(withoutViews(hints.adjusted), withoutViews(snapshot));
6137
+ const generatedBaseDdl = await diffStatements(withoutViews(hints.adjusted), withoutViews(snapshot));
6138
+ const baseDdl = orderForeignKeyAdditions(canonical, generatedBaseDdl);
5702
6139
  const previousViews = snapshotViews(hints.adjusted);
5703
6140
  const currentViews = snapshotViews(snapshot);
5704
6141
  const previousDependencies = snapshotViewDependencies(hints.adjusted);
@@ -5735,6 +6172,7 @@ async function planMigrationV2({
5735
6172
  ...dropOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(dropDdl, name, schema, "DROP") })),
5736
6173
  ...hints.statements.map((statement) => ({ kind: "hint", sql: statement })),
5737
6174
  ...baseDdl.map((statement) => ({ kind: "ddl", sql: statement.replace(/;\s*$/, "") })),
6175
+ ...foreignKeyTimingStatements(canonical, previous, baseDdl).map((sql4) => ({ kind: "ddl", sql: sql4 })),
5738
6176
  ...createOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(createDdl, name, schema, "CREATE") })),
5739
6177
  ...createOrder.map((name) => {
5740
6178
  const view = currentViewDefinitions.get(name);
@@ -5744,6 +6182,29 @@ async function planMigrationV2({
5744
6182
  ];
5745
6183
  return { namespace: canonical.namespace, schema, statements, snapshot };
5746
6184
  }
6185
+ function orderForeignKeyAdditions(canonical, statements) {
6186
+ if (!canonical.tables.some((table) => table.foreignKeys?.length)) return [...statements];
6187
+ const addsForeignKey = (statement) => /^ALTER TABLE[\s\S]* ADD CONSTRAINT[\s\S]* FOREIGN KEY\b/.test(statement);
6188
+ return [...statements.filter((statement) => !addsForeignKey(statement)), ...statements.filter(addsForeignKey)];
6189
+ }
6190
+ function foreignKeyTimingStatements(canonical, previous, baseDdl) {
6191
+ const prior = previous._sequenceOrm?.foreignKeys ?? {};
6192
+ const result = [];
6193
+ for (const table of canonical.tables) {
6194
+ for (const fk of table.foreignKeys ?? []) {
6195
+ const before = prior[`${table.table}.${fk.name}`];
6196
+ const recreated = baseDdl.some((statement) => statement.includes(`ALTER TABLE ${quoteIdentifier(ormSchemaName(canonical.namespace))}.${quoteIdentifier(table.table)} ADD CONSTRAINT ${quoteIdentifier(fk.name)}`));
6197
+ if (fk.deferrable ? !before || before.deferrable !== fk.deferrable || recreated : before?.deferrable && !recreated) {
6198
+ result.push(foreignKeyTimingSql(ormSchemaName(canonical.namespace), table.table, fk.name, fk.deferrable));
6199
+ }
6200
+ }
6201
+ }
6202
+ return result;
6203
+ }
6204
+ function foreignKeyTimingSql(schema, table, name, timing) {
6205
+ const clause = timing === void 0 ? "NOT DEFERRABLE" : timing === "initially-deferred" ? "DEFERRABLE INITIALLY DEFERRED" : "DEFERRABLE INITIALLY IMMEDIATE";
6206
+ return `ALTER TABLE ${quoteIdentifier(schema)}.${quoteIdentifier(table)} ALTER CONSTRAINT ${quoteIdentifier(name)} ${clause}`;
6207
+ }
5747
6208
  function validateViewCatalogStatement({
5748
6209
  canonical,
5749
6210
  view,
@@ -6864,8 +7325,12 @@ function generateSdl(canonical, options = {}) {
6864
7325
  '"""Gates an operation behind a platform capability or custom role \u2014 checked by the data plane before execution."""\ndirective @requires(capability: String, role: String) on QUERY | MUTATION'
6865
7326
  );
6866
7327
  blocks.push("directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT");
7328
+ blocks.push(
7329
+ '"""Abort the entire mutation unless this root affects exactly the document-authored number of rows. Mutation roots only; cannot be conditional."""\ndirective @assertAffectedRows(exactly: Int!) on FIELD'
7330
+ );
6867
7331
  for (const scalar2 of CUSTOM_SCALARS) blocks.push(`scalar ${scalar2}`);
6868
7332
  blocks.push(renderEnum(ORDER_BY_ENUM, [...ORDER_BY_ENUM_VALUES]));
7333
+ blocks.push(renderEnum(DATE_BUCKET_ENUM, DATE_BUCKETS.map((bucket) => bucket.toUpperCase())));
6869
7334
  for (const enumName of [...usedEnums].sort()) {
6870
7335
  const entry = enumByName.get(enumName);
6871
7336
  if (!entry) continue;
@@ -6895,9 +7360,24 @@ function generateSdl(canonical, options = {}) {
6895
7360
  blocks.push(renderQueryRoot(tables, namesByTable));
6896
7361
  const mutationRoot = renderMutationRoot(tables, namesByTable);
6897
7362
  if (mutationRoot !== null) blocks.push(mutationRoot);
7363
+ assertNoDuplicateTypeNames(blocks);
6898
7364
  return `${blocks.join("\n\n")}
6899
7365
  `;
6900
7366
  }
7367
+ var TYPE_DECLARATION_RE = /^(?:type|input|enum|scalar|interface|union)\s+([_A-Za-z][_0-9A-Za-z]*)/;
7368
+ function assertNoDuplicateTypeNames(blocks) {
7369
+ const seen = /* @__PURE__ */ new Set();
7370
+ for (const block of blocks) {
7371
+ const declared = TYPE_DECLARATION_RE.exec(block)?.[1];
7372
+ if (declared === void 0) continue;
7373
+ if (seen.has(declared)) {
7374
+ throw new SdlGenerationError(
7375
+ `Generated schema declares "${declared}" twice. A canonical enum or table name is PascalCased into this GraphQL type name and collides with another declaration (for example an enum named "date_bucket" renders as "DateBucket", the shared bucket enum). Rename the offending enum or table.`
7376
+ );
7377
+ }
7378
+ seen.add(declared);
7379
+ }
7380
+ }
6901
7381
  function assertEnumValue(value, context) {
6902
7382
  if (!GRAPHQL_NAME_RE2.test(value) || RESERVED_ENUM_VALUES.has(value)) {
6903
7383
  throw new SdlGenerationError(
@@ -6986,7 +7466,8 @@ function renderTableBlocks(ctx) {
6986
7466
  renderSelectColumnEnum(ctx),
6987
7467
  renderOrderByInput(ctx),
6988
7468
  renderBoolExp(ctx),
6989
- ...renderAggregateTypes(ctx)
7469
+ ...renderAggregateTypes(ctx),
7470
+ ...renderGroupByTypes(ctx)
6990
7471
  ];
6991
7472
  if (ctx.emitAggregateBoolExp) blocks.push(...renderAggregateBoolExp(ctx));
6992
7473
  const pk = primaryKeyColumns(table);
@@ -7123,6 +7604,32 @@ ${sumLines.join("\n")}
7123
7604
  }
7124
7605
  return blocks;
7125
7606
  }
7607
+ function groupableColumns(table) {
7608
+ return table.columns.filter((column) => column.type !== "jsonb");
7609
+ }
7610
+ function renderGroupByTypes(ctx) {
7611
+ const { table, names } = ctx;
7612
+ const groupable = groupableColumns(table);
7613
+ if (groupable.length === 0) return [];
7614
+ const keyLines = groupable.map((column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`);
7615
+ return [
7616
+ renderEnum(names.groupByColumnEnum, groupable.map((column) => fieldName(column.name))),
7617
+ `input ${names.groupByInput} {
7618
+ column: ${names.groupByColumnEnum}!
7619
+ bucket: ${DATE_BUCKET_ENUM}
7620
+ }`,
7621
+ `type ${names.groupKey} {
7622
+ ${keyLines.join("\n")}
7623
+ }`,
7624
+ `type ${names.aggregateGroup} {
7625
+ key: ${names.groupKey}!
7626
+ aggregate: ${names.aggregateFields}
7627
+ }`
7628
+ ];
7629
+ }
7630
+ function groupRootArgs(names) {
7631
+ return `where: ${names.boolExp}, groupBy: [${names.groupByInput}!]!, limit: Int, offset: Int`;
7632
+ }
7126
7633
  function renderAggregateBoolExp(ctx) {
7127
7634
  const { names } = ctx;
7128
7635
  return [
@@ -7148,6 +7655,9 @@ function constraintValues(table) {
7148
7655
  }
7149
7656
  return [...values].sort();
7150
7657
  }
7658
+ function clientSettableColumns(table) {
7659
+ return table.columns.filter((column) => column.onUpdateFn === void 0);
7660
+ }
7151
7661
  function renderWriteBlocks(ctx, access) {
7152
7662
  const { table, names, pinned } = ctx;
7153
7663
  const blocks = [];
@@ -7169,7 +7679,7 @@ ${insertLines.join("\n")}
7169
7679
  }`);
7170
7680
  }
7171
7681
  if (access.update) {
7172
- const setLines = table.columns.map(
7682
+ const setLines = clientSettableColumns(table).map(
7173
7683
  (column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
7174
7684
  );
7175
7685
  blocks.push(`input ${names.setInput} {
@@ -7196,7 +7706,7 @@ ${incLines.join("\n")}
7196
7706
  if (access.insert && allowsOnConflict(table) && constraints.length > 0) {
7197
7707
  for (const value of constraints) assertEnumValue(value, `constraint enum for table "${table.table}"`);
7198
7708
  blocks.push(renderEnum(names.constraintEnum, constraints));
7199
- const updateColumns = table.columns.map((column) => fieldName(column.name));
7709
+ const updateColumns = clientSettableColumns(table).map((column) => fieldName(column.name));
7200
7710
  blocks.push(renderEnum(names.updateColumnEnum, updateColumns));
7201
7711
  blocks.push(
7202
7712
  `input ${names.onConflict} {
@@ -7228,6 +7738,9 @@ function renderQueryRoot(tables, namesByTable) {
7228
7738
  lines.push(` ${names.byPkRoot}(${args}): ${names.typename}`);
7229
7739
  }
7230
7740
  lines.push(` ${names.aggregateRoot}(${rootArgs(names)}): ${names.aggregate}!`);
7741
+ if (groupableColumns(table).length > 0) {
7742
+ lines.push(` ${names.aggregateGroupsRoot}(${groupRootArgs(names)}): [${names.aggregateGroup}!]!`);
7743
+ }
7231
7744
  }
7232
7745
  lines.push("}");
7233
7746
  return lines.join("\n");
@@ -7296,6 +7809,40 @@ function renderCowMutationFields(table, names) {
7296
7809
  }
7297
7810
  return lines;
7298
7811
  }
7812
+ var ORM_MIGRATION_EXPORT_FILE = "orm.migration.json";
7813
+ var ORM_MIGRATION_EXPORT_MAX_BYTES = ORM_REGISTRATION_MAX_BYTES;
7814
+ var hash = zod.z.string().regex(/^sha256:[0-9a-f]{64}$/);
7815
+ var ormMigrationPayloadSchema = zod.z.object({
7816
+ canonical_definition: canonicalDefinitionSchema,
7817
+ documents: zod.z.array(zod.z.object({ path: zod.z.string().min(1).max(2048), contents: zod.z.string().max(1e6) }).strict()).max(5e3),
7818
+ client_manifest: zod.z.object({ ops_hash: hash, operations: zod.z.record(zod.z.string(), hash) }).strict(),
7819
+ sdl: zod.z.string().refine((value) => Buffer.byteLength(value, "utf8") <= MAX_GENERATED_SDL_BYTES, {
7820
+ message: `sdl exceeds ${MAX_GENERATED_SDL_BYTES} bytes`
7821
+ }),
7822
+ schema_snapshot: zod.z.json(),
7823
+ // The existing ORM committed-migration validator and replay engine remain
7824
+ // authoritative. This envelope hashes the provenance, never authorizes SQL.
7825
+ migration: zod.z.json().optional()
7826
+ }).strict();
7827
+ zod.z.object({
7828
+ schema_version: zod.z.literal(1),
7829
+ schema_hash: hash,
7830
+ payload: ormMigrationPayloadSchema,
7831
+ checksum: zod.z.string().regex(/^[0-9a-f]{64}$/)
7832
+ }).strict();
7833
+ function checksum(value) {
7834
+ return crypto.createHash("sha256").update(stableJson(value)).digest("hex");
7835
+ }
7836
+ function createOrmMigrationExport({ schemaHash, payload }) {
7837
+ const contents = { schema_version: 1, schema_hash: hash.parse(schemaHash), payload: ormMigrationPayloadSchema.parse(payload) };
7838
+ return { ...contents, checksum: checksum(contents) };
7839
+ }
7840
+ function renderOrmMigrationExport(exported) {
7841
+ const text4 = `${JSON.stringify(exported, null, 2)}
7842
+ `;
7843
+ if (Buffer.byteLength(text4, "utf8") > ORM_MIGRATION_EXPORT_MAX_BYTES) throw new Error("ORM migration export exceeds the size limit");
7844
+ return text4;
7845
+ }
7299
7846
 
7300
7847
  // src/cli.ts
7301
7848
  var BASE_URL_ENV = "SEQUENCE_ORM_BASE_URL";
@@ -7304,6 +7851,8 @@ var ORM_MODULE_PATH_ENV = "SEQUENCE_ORM_MODULE_PATH";
7304
7851
  var USAGE = `usage:
7305
7852
  seq-studio orm init <dir> scaffold a namespace package (v2: TypeScript schema + GraphQL documents)
7306
7853
  seq-studio orm generate [dir] write schema.graphql + operations.manifest.json + typePolicies.gen.ts (+ consumer graphql-codegen when codegen.ts is present)
7854
+ seq-studio orm generate [dir] --migration-export [--check]
7855
+ export reviewed ORM inputs for a coordinated pipeline migration; --check verifies without writing
7307
7856
  seq-studio orm validate [dir] parse + validate definitions and verify committed migrations are in sync
7308
7857
  seq-studio orm plan [dir] -e <env> offline dry run: compile definitions and diff against the registry (no database, no writes)
7309
7858
  seq-studio orm apply [dir] -e <env> author the migration, register, apply, and refresh the generated outputs (the everyday command)
@@ -7318,6 +7867,8 @@ var USAGE = `usage:
7318
7867
  v2 = sequence.config.ts + schema/*.ts + graphql/**; v1 (legacy) = one <table>.yaml per table.
7319
7868
  Environments: see \`seq-studio envs list\` (discovered after you authenticate).
7320
7869
  Authenticate with: seq-studio login
7870
+ Execute endpoint rollout: SEQUENCE_ORM_ATLAS_EXECUTE_V2=1 opts into /api/data/v2;
7871
+ otherwise execute uses /_data/v2. Server rollout and authorization still apply.
7321
7872
  `;
7322
7873
  var tokenProvider = null;
7323
7874
  function setTokenProvider(provider) {
@@ -7532,6 +8083,12 @@ function printDiagnostics(diagnostics) {
7532
8083
  }
7533
8084
  async function generateCommand(rest) {
7534
8085
  const dir = positional(rest) ?? ".";
8086
+ const migrationExport = rest.includes("--migration-export");
8087
+ const check = rest.includes("--check");
8088
+ if (check && !migrationExport) {
8089
+ console.error("generate --check requires --migration-export");
8090
+ return 1;
8091
+ }
7535
8092
  if (await detectProjectFormat(dir) === "v1") {
7536
8093
  console.error(`generate needs a v2 project (${NAMESPACE_CONFIG_FILE}) \u2014 convert with: seq-studio orm migrate-from-yaml ${dir}`);
7537
8094
  return 1;
@@ -7539,6 +8096,36 @@ async function generateCommand(rest) {
7539
8096
  const compiled = await compileV2Project(dir);
7540
8097
  printDiagnostics(compiled.ok ? compiled.project.diagnostics : compiled.diagnostics);
7541
8098
  if (!compiled.ok) return 1;
8099
+ if (migrationExport) {
8100
+ if (await validateV2Command(dir) !== 0) return 1;
8101
+ const { loaded: loaded2, sdl, documents, manifest: manifest2 } = compiled.project;
8102
+ const workspace = await readMigrationWorkspace(dir, loaded2.canonical.namespace);
8103
+ const migration = await loadCommittedTipMigrationV2({ dir, loaded: loaded2, workspace });
8104
+ const exported = createOrmMigrationExport({ schemaHash: manifest2.schemaHash, payload: {
8105
+ canonical_definition: loaded2.canonical,
8106
+ documents,
8107
+ sdl,
8108
+ client_manifest: { ops_hash: manifest2.opsHash, operations: Object.fromEntries(Object.entries(manifest2.operations).map(([name, operation]) => [name, operation.opHash])) },
8109
+ schema_snapshot: workspace.tipSnapshot,
8110
+ ...migration ? { migration } : {}
8111
+ } });
8112
+ const text4 = renderOrmMigrationExport(exported);
8113
+ const path$1 = path.join(dir, ORM_MIGRATION_EXPORT_FILE);
8114
+ if (check) {
8115
+ const committed = await promises.readFile(path$1, "utf8").catch((error) => {
8116
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return void 0;
8117
+ throw error;
8118
+ });
8119
+ if (committed !== text4) {
8120
+ console.error(`${ORM_MIGRATION_EXPORT_FILE} is missing or stale; run seq-studio orm generate ${dir} --migration-export and commit it`);
8121
+ return 1;
8122
+ }
8123
+ console.log(`${ORM_MIGRATION_EXPORT_FILE} matches the current definitions, operations, and committed migration`);
8124
+ return 0;
8125
+ }
8126
+ await promises.writeFile(path$1, text4);
8127
+ console.log(`wrote ${ORM_MIGRATION_EXPORT_FILE} (${exported.checksum.slice(0, 12)})`);
8128
+ }
7542
8129
  await writeGenerateOutputs(dir, compiled.project);
7543
8130
  const { loaded, manifest } = compiled.project;
7544
8131
  console.log(
@@ -7654,9 +8241,10 @@ async function executeV2Command(rest) {
7654
8241
  );
7655
8242
  }
7656
8243
  const variables = await readExecuteVariables(variablesFile);
8244
+ const executePrefix = process.env["SEQUENCE_ORM_ATLAS_EXECUTE_V2"] === "1" ? "/api/data/v2" : "/_data/v2";
7657
8245
  const response = await apiRequestStrict(
7658
8246
  "POST",
7659
- `/_data/v2/${loaded.canonical.namespace}`,
8247
+ `${executePrefix}/${loaded.canonical.namespace}`,
7660
8248
  { op: selected.hash, variables }
7661
8249
  );
7662
8250
  console.log(JSON.stringify(response, null, 2));
@@ -8171,6 +8759,7 @@ function columnProperty(table, column, singlePrimaryKey) {
8171
8759
  }
8172
8760
  chain += `.default(${JSON.stringify(column.default)})`;
8173
8761
  }
8762
+ if (column.onUpdateFn !== void 0) chain += `.onUpdate(${quote(column.onUpdateFn)})`;
8174
8763
  if (column.description !== void 0) chain += `.description(${quote(column.description)})`;
8175
8764
  if (column.renamedFrom !== void 0) chain += `.renamedFrom(${quote(column.renamedFrom)})`;
8176
8765
  if (column.cast !== void 0) chain += `.cast(${quote(column.cast)})`;