@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.js CHANGED
@@ -6,8 +6,8 @@ import { createJiti } from 'jiti';
6
6
  import { createHash } from 'crypto';
7
7
  import { z } from 'zod';
8
8
  import { sql } from 'drizzle-orm';
9
- import { pgSchema, PgDialect, QueryBuilder, getViewConfig, extractUsedTable, primaryKey, uniqueIndex, index, foreignKey, jsonb, date, timestamp, boolean, doublePrecision, numeric, bigint, integer, text, uuid } from 'drizzle-orm/pg-core';
10
- import { buildSchema, parse, validate, Kind, valueFromASTUntyped, print } from 'graphql';
9
+ import { pgSchema, PgDialect, QueryBuilder, getViewConfig, extractUsedTable, foreignKey, primaryKey, uniqueIndex, index, jsonb, date, timestamp, boolean, doublePrecision, numeric, bigint, integer, text, uuid } from 'drizzle-orm/pg-core';
10
+ import { buildSchema, parse, validate, Kind, valueFromASTUntyped, visit, print } from 'graphql';
11
11
  import { generateDrizzleJson, upPgSnapshot, generateMigration } from 'drizzle-kit/api';
12
12
  import { load } from 'js-yaml';
13
13
  import { capabilityPinsSchema, customRolesBlockSchema, validateCapabilityPins } from '@sequenceholdings/custom-roles-manifest';
@@ -36,9 +36,34 @@ var PredicateParseError = class extends Error {
36
36
  }
37
37
  name = "PredicateParseError";
38
38
  };
39
+
40
+ // src/constants.ts
41
+ var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
42
+ var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
43
+ var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
44
+ var MAX_IDENTIFIER_LENGTH = 63;
45
+ var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
46
+ var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
47
+ var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
48
+ var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
49
+ var ORM_SCHEMA_PREFIX = "orm_";
50
+ var CONSUMER_ROLE_SUFFIX = "_consumer";
51
+ var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
52
+ var consumerRoleName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}${CONSUMER_ROLE_SUFFIX}`;
53
+ var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
54
+ var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
55
+ var HASH_DOMAIN = "sequence-orm-definitions\0";
56
+ var MAX_DEFINITION_FILE_BYTES = 1024 * 1024;
57
+ var MAX_GENERATED_SDL_BYTES = 16 * 1024 * 1024;
58
+ var ORM_REGISTRATION_MAX_BYTES = 64 * 1024 * 1024;
59
+ var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
60
+ var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
61
+
62
+ // src/canonical/v2.ts
39
63
  var CANONICAL_FORMAT = 2;
40
64
  var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
41
65
  var identifier = z.string().min(1).max(63).regex(IDENTIFIER_RE);
66
+ var columnIdentifier = z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
42
67
  var COLUMN_TYPES = [
43
68
  "uuid",
44
69
  "text",
@@ -59,18 +84,33 @@ var canonicalEnumSchema = z.object({
59
84
  var canonicalColumnSchema = z.object({
60
85
  /** Property name in TS (camelCase); columnName is the Postgres name. */
61
86
  name: z.string().min(1).max(63),
62
- columnName: identifier,
87
+ columnName: columnIdentifier,
63
88
  type: z.union([columnTypeSchema, z.object({ enum: identifier })]),
64
89
  nullable: z.boolean().default(true),
65
90
  unique: z.boolean().default(false),
66
91
  /** Literal default, or one of the allowlisted SQL functions. */
67
92
  default: z.union([z.string(), z.number(), z.boolean(), z.null()]).optional(),
68
93
  defaultFn: z.enum(["gen_random_uuid()", "now()"]).optional(),
94
+ /**
95
+ * Stamp this column from the database clock on UPDATE (`now()`). The
96
+ * column stays insertable (defaults fill gaps) but is omitted from
97
+ * SetInput / settable so callers cannot backdate it.
98
+ */
99
+ onUpdateFn: z.enum(["now()"]).optional(),
69
100
  description: z.string().max(500).optional(),
70
101
  /** Migration hints — same semantics as v1. */
71
- renamedFrom: identifier.optional(),
102
+ renamedFrom: columnIdentifier.optional(),
72
103
  cast: z.string().max(200).optional(),
73
104
  deprecated: z.boolean().default(false)
105
+ }).superRefine((column, ctx) => {
106
+ if (column.onUpdateFn === void 0) return;
107
+ if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
108
+ ctx.addIssue({
109
+ code: "custom",
110
+ path: ["onUpdateFn"],
111
+ message: `onUpdateFn '${column.onUpdateFn}' is only valid on timestamptz/date columns`
112
+ });
113
+ }
74
114
  });
75
115
  var canonicalLinkSchema = z.object({
76
116
  name: z.string().min(1).max(63),
@@ -78,15 +118,15 @@ var canonicalLinkSchema = z.object({
78
118
  /** Target table name (namespace-local). Absent for toPlatformUser. */
79
119
  target: identifier.optional(),
80
120
  /** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
81
- on: identifier,
121
+ on: columnIdentifier,
82
122
  /** Referenced column on the target (default: target's PK / platform user `sub`). */
83
- references: identifier.optional(),
123
+ references: columnIdentifier.optional(),
84
124
  /** enforce: true emits a real FK constraint (same-namespace toOne only). */
85
125
  enforce: z.boolean().default(false),
86
126
  description: z.string().max(500).optional()
87
127
  });
88
128
  var canonicalIndexSchema = z.object({
89
- columns: z.array(identifier).min(1),
129
+ columns: z.array(columnIdentifier).min(1),
90
130
  unique: z.boolean().default(false),
91
131
  name: identifier.optional()
92
132
  });
@@ -334,6 +374,13 @@ var canonicalCowMutationSchema = z.object({
334
374
  }
335
375
  }
336
376
  });
377
+ var canonicalForeignKeySchema = z.object({
378
+ name: identifier,
379
+ columns: z.array(columnIdentifier).min(1).max(8),
380
+ target: identifier,
381
+ references: z.array(columnIdentifier).min(1).max(8),
382
+ deferrable: z.enum(["initially-immediate", "initially-deferred"]).optional()
383
+ }).strict();
337
384
  var canonicalTableSchema = z.object({
338
385
  table: identifier,
339
386
  description: z.string().max(2e3).optional(),
@@ -342,7 +389,7 @@ var canonicalTableSchema = z.object({
342
389
  /** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
343
390
  source: z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
344
391
  columns: z.array(canonicalColumnSchema).default([]),
345
- primaryKey: z.array(identifier).min(1).max(8).optional(),
392
+ primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
346
393
  links: z.array(canonicalLinkSchema).default([]),
347
394
  indexes: z.array(canonicalIndexSchema).default([]),
348
395
  policies: canonicalPoliciesSchema.default({}),
@@ -353,14 +400,16 @@ var canonicalTableSchema = z.object({
353
400
  */
354
401
  actions: z.array(canonicalActionSchema).default([]),
355
402
  /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
356
- cowMutations: z.array(canonicalCowMutationSchema).optional()
403
+ cowMutations: z.array(canonicalCowMutationSchema).optional(),
404
+ /** Omitted by old definitions, preserving their existing content hashes. */
405
+ foreignKeys: z.array(canonicalForeignKeySchema).max(100).optional()
357
406
  });
358
407
  var canonicalViewSchema = z.object({
359
408
  view: identifier,
360
409
  description: z.string().max(2e3).optional(),
361
410
  columns: z.array(canonicalColumnSchema).min(1),
362
411
  /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
363
- primaryKey: z.array(identifier).min(1).max(8).optional(),
412
+ primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
364
413
  links: z.array(canonicalLinkSchema).default([]),
365
414
  policies: z.object({ read: policyExpressionSchema }).strict(),
366
415
  query: z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
@@ -373,6 +422,9 @@ var canonicalViewSchema = z.object({
373
422
  if (column.default !== void 0 || column.defaultFn !== void 0) {
374
423
  ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare a default` });
375
424
  }
425
+ if (column.onUpdateFn !== void 0) {
426
+ ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare onUpdateFn` });
427
+ }
376
428
  if (column.unique) {
377
429
  ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare unique` });
378
430
  }
@@ -406,6 +458,45 @@ var canonicalDefinitionSchema = z.object({
406
458
  ctx.addIssue({ code: "custom", path: ["tables", index3, "table"], message: `duplicate relation '${table.table}'` });
407
459
  }
408
460
  seen.add(table.table);
461
+ if (table.source !== void 0) {
462
+ for (const [columnIndex, column] of table.columns.entries()) {
463
+ if (column.onUpdateFn !== void 0) {
464
+ ctx.addIssue({
465
+ code: "custom",
466
+ path: ["tables", index3, "columns", columnIndex],
467
+ message: `synced table '${table.table}' column '${column.name}' cannot declare onUpdateFn`
468
+ });
469
+ }
470
+ }
471
+ }
472
+ }
473
+ const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
474
+ for (const [tableIndex, table] of definition.tables.entries()) {
475
+ const names = /* @__PURE__ */ new Set([
476
+ ...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
477
+ ...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
478
+ ...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
479
+ ]);
480
+ for (const [index3, fk] of (table.foreignKeys ?? []).entries()) {
481
+ const fail2 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index3], message });
482
+ if (names.has(fk.name)) fail2(`duplicate foreign key '${fk.name}'`);
483
+ names.add(fk.name);
484
+ const target = fkTables.get(fk.target);
485
+ if (table.source || !target || target.source) {
486
+ fail2("foreign keys must connect owned tables in the same namespace");
487
+ continue;
488
+ }
489
+ if (fk.columns.length !== fk.references.length) fail2("foreign key columns and references must have the same number of columns");
490
+ if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail2("foreign key columns must not repeat");
491
+ for (const [position, name] of fk.columns.entries()) {
492
+ const sourceColumn = table.columns.find((column) => column.columnName === name);
493
+ const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
494
+ if (!sourceColumn || !targetColumn) fail2("foreign key references an unknown column");
495
+ else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail2("foreign key column types must match");
496
+ }
497
+ const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index4) => index4.unique).map((index4) => index4.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
498
+ 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");
499
+ }
409
500
  }
410
501
  const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
411
502
  for (const [tableIndex, table] of definition.tables.entries()) {
@@ -501,7 +592,7 @@ var canonicalDefinitionSchema = z.object({
501
592
  const visiting = /* @__PURE__ */ new Set();
502
593
  const visited = /* @__PURE__ */ new Set();
503
594
  let cycleReported = false;
504
- const visit = (name, path) => {
595
+ const visit2 = (name, path) => {
505
596
  if (visited.has(name) || cycleReported) return;
506
597
  if (visiting.has(name)) {
507
598
  cycleReported = true;
@@ -515,12 +606,12 @@ var canonicalDefinitionSchema = z.object({
515
606
  visiting.add(name);
516
607
  const view = viewByName.get(name);
517
608
  for (const dependency of view?.dependencies ?? []) {
518
- if (viewByName.has(dependency)) visit(dependency, [...path, name]);
609
+ if (viewByName.has(dependency)) visit2(dependency, [...path, name]);
519
610
  }
520
611
  visiting.delete(name);
521
612
  visited.add(name);
522
613
  };
523
- for (const view of views) visit(view.view, []);
614
+ for (const view of views) visit2(view.view, []);
524
615
  });
525
616
  function canonicalRelations(definition) {
526
617
  const views = (definition.views ?? []).map((view) => ({
@@ -541,12 +632,13 @@ function normalizeCanonical(definition) {
541
632
  ...definition,
542
633
  enums: sortByName(definition.enums),
543
634
  tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
544
- const { cowMutations, ...rest } = t2;
635
+ const { cowMutations, foreignKeys, ...rest } = t2;
545
636
  return {
546
637
  ...rest,
547
638
  links: sortByName(t2.links),
548
639
  indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
549
640
  actions: sortByName(t2.actions),
641
+ ...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
550
642
  // Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
551
643
  ...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
552
644
  };
@@ -561,25 +653,6 @@ function normalizeCanonical(definition) {
561
653
  }
562
654
  return normalized;
563
655
  }
564
-
565
- // src/constants.ts
566
- var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
567
- var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
568
- var MAX_IDENTIFIER_LENGTH = 63;
569
- var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
570
- var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
571
- var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
572
- var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
573
- var ORM_SCHEMA_PREFIX = "orm_";
574
- var CONSUMER_ROLE_SUFFIX = "_consumer";
575
- var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
576
- var consumerRoleName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}${CONSUMER_ROLE_SUFFIX}`;
577
- var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
578
- var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
579
- var HASH_DOMAIN = "sequence-orm-definitions\0";
580
- var MAX_DEFINITION_FILE_BYTES = 1024 * 1024;
581
- var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
582
- var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
583
656
  function stableJson(value) {
584
657
  if (value === null || typeof value !== "object") return JSON.stringify(value);
585
658
  if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
@@ -1005,6 +1078,22 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1005
1078
  } else if (columnLevelPk.length > 0) {
1006
1079
  candidate.primaryKey = columnLevelPk;
1007
1080
  }
1081
+ if (table.config.foreignKeys?.length) {
1082
+ if (table.source) throw new OrmDefinitionError(`synced table "${table.tableName}" cannot enforce foreign keys`, table.file);
1083
+ candidate.foreignKeys = table.config.foreignKeys.map((fk) => {
1084
+ const target = resolveRelationRef({ ref: fk.target, fail: (message) => {
1085
+ throw new OrmDefinitionError(message, table.file);
1086
+ } });
1087
+ if (!isSeqTable(target) || !relationsByName.has(target.tableName)) throw new OrmDefinitionError("foreign key target must be a table in this namespace", table.file);
1088
+ return {
1089
+ name: fk.name,
1090
+ columns: fk.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: `foreign key "${fk.name}" column` })),
1091
+ target: target.tableName,
1092
+ references: fk.references.map((ref) => resolveColumnRef({ relation: target, ref, role: `foreign key "${fk.name}" reference` })),
1093
+ ...fk.deferrable ? { deferrable: fk.deferrable } : {}
1094
+ };
1095
+ });
1096
+ }
1008
1097
  for (const index3 of table.config.indexes ?? []) {
1009
1098
  const entry = {
1010
1099
  columns: index3.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
@@ -1057,6 +1146,12 @@ function buildCanonicalView({
1057
1146
  if (column.default !== void 0 || column.defaultFn !== void 0) {
1058
1147
  throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare a default`, view.file);
1059
1148
  }
1149
+ if (column.onUpdateFn !== void 0) {
1150
+ throw new OrmDefinitionError(
1151
+ `view "${view.viewName}": column "${propertyName}" cannot declare onUpdateFn`,
1152
+ view.file
1153
+ );
1154
+ }
1060
1155
  if (column.unique) {
1061
1156
  throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare unique`, view.file);
1062
1157
  }
@@ -1221,6 +1316,10 @@ async function loadNamespaceFromDir(dir, options = {}) {
1221
1316
  fsCache: false,
1222
1317
  ...options.ormModulePath !== void 0 ? {
1223
1318
  alias: {
1319
+ // The public /drizzle barrel re-exports this pinned dependency.
1320
+ // Explicitly map it before the root alias: otherwise jiti appends
1321
+ // "/drizzle" to the overridden module's filename.
1322
+ "@sequenceholdings/orm/drizzle": createRequire(import.meta.url).resolve("drizzle-orm"),
1224
1323
  "@sequenceholdings/orm": options.ormModulePath,
1225
1324
  "@sequenceholdings/orm/authoring": options.ormModulePath
1226
1325
  }
@@ -1434,6 +1533,7 @@ var ORDER_BY_ENUM_VALUES = [
1434
1533
  "DESC_NULLS_LAST"
1435
1534
  ];
1436
1535
  var PLATFORM_USER_TYPENAME = "PlatformUser";
1536
+ var DATE_BUCKET_ENUM = "DateBucket";
1437
1537
  function tableNames(table) {
1438
1538
  const root = camelCase(table);
1439
1539
  const rootPascal = pascalCase(table);
@@ -1466,7 +1566,12 @@ function tableNames(table) {
1466
1566
  sumFields: `${type}SumFields`,
1467
1567
  avgFields: `${type}AvgFields`,
1468
1568
  aggregateBoolExp: `${type}AggregateBoolExp`,
1469
- aggregateBoolExpCount: `${type}AggregateBoolExpCount`
1569
+ aggregateBoolExpCount: `${type}AggregateBoolExpCount`,
1570
+ aggregateGroupsRoot: `${root}AggregateGroups`,
1571
+ groupByColumnEnum: `${type}GroupByColumn`,
1572
+ groupByInput: `${type}GroupBy`,
1573
+ groupKey: `${type}GroupKey`,
1574
+ aggregateGroup: `${type}AggregateGroup`
1470
1575
  };
1471
1576
  }
1472
1577
 
@@ -1499,7 +1604,18 @@ ${body}
1499
1604
  }
1500
1605
  `;
1501
1606
  }
1502
- var IR_VERSION = 1;
1607
+ var IR_VERSION = 2;
1608
+ var LEGACY_IR_VERSION = 1;
1609
+ var irVersionSchema = z.union([z.literal(LEGACY_IR_VERSION), z.literal(IR_VERSION)]);
1610
+ var operationVersionSchema = z.object({ irVersion: irVersionSchema });
1611
+ function requiredArtifactIrVersion(operations) {
1612
+ let required = LEGACY_IR_VERSION;
1613
+ for (const operation of operations) {
1614
+ const { irVersion } = operationVersionSchema.parse(operation);
1615
+ if (irVersion > required) required = irVersion;
1616
+ }
1617
+ return required;
1618
+ }
1503
1619
  var CLAIMS_CONTRACT = {
1504
1620
  sub: "string",
1505
1621
  org: "string?",
@@ -1626,6 +1742,33 @@ var aggregateRootSchema = z.object({
1626
1742
  nodes: selectionIrSchema.optional(),
1627
1743
  includeIf: includeIfSchema.optional()
1628
1744
  });
1745
+ var DATE_BUCKETS = ["year", "quarter", "month", "week", "day", "hour"];
1746
+ var groupByKeyIrSchema = z.object({
1747
+ column: z.string(),
1748
+ bucket: z.enum(DATE_BUCKETS).optional()
1749
+ });
1750
+ var aggregateGroupsRootSchema = z.object({
1751
+ kind: z.literal("aggregateGroups"),
1752
+ alias: z.string(),
1753
+ table: z.string(),
1754
+ /** Composed at compile time: AND(policy, documentWhere?, dynamicFilter?). */
1755
+ where: predIrSchema,
1756
+ groupBy: z.array(groupByKeyIrSchema).min(1),
1757
+ /**
1758
+ * The `key { … }` projection: which group columns appear in each entry's key
1759
+ * object, in selection order. A subset of `groupBy` (the compiler rejects a
1760
+ * key field that is not a group key — it would be indeterminate within the
1761
+ * group). Empty when the document selects only `aggregate`.
1762
+ */
1763
+ key: z.array(z.object({ alias: z.string(), column: z.string() })).default([]),
1764
+ count: z.object({ columns: z.array(z.string()).optional(), distinct: z.boolean().optional() }).optional(),
1765
+ sum: z.array(z.string()).optional(),
1766
+ avg: z.array(z.string()).optional(),
1767
+ /** Bounds the GROUP count, not the scanned rows — see MAX_GROUPS. */
1768
+ limit: limitIrSchema,
1769
+ offset: z.object({ variable: z.string().optional(), value: z.number().int().optional() }).optional(),
1770
+ includeIf: includeIfSchema.optional()
1771
+ });
1629
1772
  var writeValuesSchema = z.union([
1630
1773
  z.object({ slot: z.string() }),
1631
1774
  z.object({ entries: z.array(z.object({ column: z.string(), value: valueIrSchema })) })
@@ -1671,12 +1814,15 @@ var mutationRootSchema = z.object({
1671
1814
  insertCheck: predIrSchema.optional(),
1672
1815
  returning: selectionIrSchema.nullable(),
1673
1816
  affectedRows: z.boolean().default(false),
1817
+ /** v2: abort the transaction when the actual write count differs, regardless of selection. */
1818
+ assertAffectedRows: z.number().int().min(0).max(2147483647).optional(),
1674
1819
  includeIf: includeIfSchema.optional()
1675
1820
  });
1676
1821
  var rootIrSchema = z.discriminatedUnion("kind", [
1677
1822
  selectRootSchema,
1678
1823
  byPkRootSchema,
1679
1824
  aggregateRootSchema,
1825
+ aggregateGroupsRootSchema,
1680
1826
  mutationRootSchema
1681
1827
  ]);
1682
1828
  var INPUT_REF_KINDS = [
@@ -1706,7 +1852,7 @@ var variableIrSchema = z.object({
1706
1852
  defaultValue: variableDefaultIrSchema.optional()
1707
1853
  });
1708
1854
  var compiledOperationSchema = z.object({
1709
- irVersion: z.literal(IR_VERSION),
1855
+ irVersion: irVersionSchema,
1710
1856
  name: z.string(),
1711
1857
  opHash: z.string().regex(/^sha256:[0-9a-f]{64}$/),
1712
1858
  kind: z.enum(["query", "mutation"]),
@@ -1718,6 +1864,13 @@ var compiledOperationSchema = z.object({
1718
1864
  /** Every table the op touches — data-token scoping + audit. */
1719
1865
  tableSet: z.array(z.string()).min(1),
1720
1866
  roots: z.array(rootIrSchema).min(1)
1867
+ }).superRefine((operation, ctx) => {
1868
+ for (const [index3, root] of operation.roots.entries()) {
1869
+ if (root.kind !== "mutation" || root.assertAffectedRows === void 0) continue;
1870
+ if (operation.irVersion < 2 || operation.kind !== "mutation" || root.includeIf !== void 0) {
1871
+ ctx.addIssue({ code: "custom", path: ["roots", index3], message: "affected-row assertions require unconditional mutation roots and IR v2" });
1872
+ }
1873
+ }
1721
1874
  });
1722
1875
  var tableMetaIrSchema = z.object({
1723
1876
  table: z.string(),
@@ -1754,18 +1907,29 @@ var tableMetaIrSchema = z.object({
1754
1907
  insertable: z.array(z.string()),
1755
1908
  settable: z.array(z.string()),
1756
1909
  appendable: z.array(z.string()),
1910
+ /**
1911
+ * Logical columns stamped with Postgres `now()` on UPDATE (canonical
1912
+ * `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
1913
+ */
1914
+ touchOnUpdate: z.array(z.string()).optional(),
1757
1915
  constraints: z.array(z.object({ name: z.string(), columns: z.array(z.string()) })),
1758
1916
  /** Depth/node caps for dynamic filter trees on this table. */
1759
1917
  dynamicFilterMaxDepth: z.number().int().positive().default(6),
1760
1918
  dynamicFilterMaxNodes: z.number().int().positive().default(64)
1761
1919
  });
1762
1920
  var compiledArtifactSchema = z.object({
1763
- irVersion: z.literal(IR_VERSION),
1921
+ irVersion: irVersionSchema,
1764
1922
  namespace: z.string(),
1765
1923
  schemaHash: z.string(),
1766
1924
  opsHash: z.string(),
1767
1925
  tables: z.record(z.string(), tableMetaIrSchema),
1768
1926
  operations: z.record(z.string(), compiledOperationSchema)
1927
+ }).superRefine((artifact, ctx) => {
1928
+ for (const [name, operation] of Object.entries(artifact.operations)) {
1929
+ if (operation.irVersion > artifact.irVersion) {
1930
+ ctx.addIssue({ code: "custom", path: ["operations", name, "irVersion"], message: "operation requires a newer IR version than its artifact" });
1931
+ }
1932
+ }
1769
1933
  });
1770
1934
  var IrVersionError = class extends Error {
1771
1935
  constructor(found) {
@@ -1776,19 +1940,19 @@ var IrVersionError = class extends Error {
1776
1940
  function normalizeIr(raw) {
1777
1941
  if (!raw || typeof raw !== "object") throw new IrVersionError(void 0);
1778
1942
  const version = raw.irVersion;
1779
- if (version !== IR_VERSION) throw new IrVersionError(version);
1943
+ if (version !== LEGACY_IR_VERSION && version !== IR_VERSION) throw new IrVersionError(version);
1780
1944
  return compiledArtifactSchema.parse(raw);
1781
1945
  }
1782
1946
 
1783
1947
  // src/predicate-v2/ast.ts
1784
1948
  var POLICY_COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
1785
- function walkPolicy(node, visit) {
1786
- visit(node);
1949
+ function walkPolicy(node, visit2) {
1950
+ visit2(node);
1787
1951
  if (node.kind === "and" || node.kind === "or") {
1788
- for (const operand of node.operands) walkPolicy(operand, visit);
1952
+ for (const operand of node.operands) walkPolicy(operand, visit2);
1789
1953
  }
1790
- if (node.kind === "not") walkPolicy(node.operand, visit);
1791
- if (node.kind === "exists") walkPolicy(node.where, visit);
1954
+ if (node.kind === "not") walkPolicy(node.operand, visit2);
1955
+ if (node.kind === "exists") walkPolicy(node.where, visit2);
1792
1956
  }
1793
1957
 
1794
1958
  // src/predicate-v2/errors.ts
@@ -2294,21 +2458,21 @@ function andPred(...preds) {
2294
2458
  if (operands.length === 1) return operands[0];
2295
2459
  return { kind: "and", operands };
2296
2460
  }
2297
- function walkPred(pred, visit) {
2298
- visit(pred);
2461
+ function walkPred(pred, visit2) {
2462
+ visit2(pred);
2299
2463
  switch (pred.kind) {
2300
2464
  case "and":
2301
2465
  case "or":
2302
- for (const operand of pred.operands) walkPred(operand, visit);
2466
+ for (const operand of pred.operands) walkPred(operand, visit2);
2303
2467
  return;
2304
2468
  case "not":
2305
- walkPred(pred.operand, visit);
2469
+ walkPred(pred.operand, visit2);
2306
2470
  return;
2307
2471
  case "exists":
2308
- walkPred(pred.where, visit);
2472
+ walkPred(pred.where, visit2);
2309
2473
  return;
2310
2474
  case "aggPredicate":
2311
- if (pred.where) walkPred(pred.where, visit);
2475
+ if (pred.where) walkPred(pred.where, visit2);
2312
2476
  return;
2313
2477
  default:
2314
2478
  return;
@@ -2325,12 +2489,12 @@ function predContainsExists(pred) {
2325
2489
  // src/compiler-gql/inject.ts
2326
2490
  function collectPinnedColumns(writePolicy) {
2327
2491
  const key = (pin) => `${pin.column}\0${pin.claim}`;
2328
- const visit = (node) => {
2492
+ const visit2 = (node) => {
2329
2493
  if (node.kind === "and") {
2330
2494
  const merged = [];
2331
2495
  const seen2 = /* @__PURE__ */ new Set();
2332
2496
  for (const operand of node.operands) {
2333
- for (const pin of visit(operand)) {
2497
+ for (const pin of visit2(operand)) {
2334
2498
  if (seen2.has(key(pin))) continue;
2335
2499
  seen2.add(key(pin));
2336
2500
  merged.push(pin);
@@ -2341,9 +2505,9 @@ function collectPinnedColumns(writePolicy) {
2341
2505
  if (node.kind === "or") {
2342
2506
  const [first, ...rest] = node.operands;
2343
2507
  if (first === void 0) return [];
2344
- let common = visit(first);
2508
+ let common = visit2(first);
2345
2509
  for (const operand of rest) {
2346
- const branch = new Set(visit(operand).map(key));
2510
+ const branch = new Set(visit2(operand).map(key));
2347
2511
  common = common.filter((pin) => branch.has(key(pin)));
2348
2512
  if (common.length === 0) break;
2349
2513
  }
@@ -2355,7 +2519,7 @@ function collectPinnedColumns(writePolicy) {
2355
2519
  return [];
2356
2520
  };
2357
2521
  const seen = /* @__PURE__ */ new Set();
2358
- return visit(writePolicy).filter((pin) => {
2522
+ return visit2(writePolicy).filter((pin) => {
2359
2523
  if (seen.has(pin.column)) return false;
2360
2524
  seen.add(pin.column);
2361
2525
  return true;
@@ -2449,6 +2613,11 @@ var PolicyInjector = class {
2449
2613
  where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table)),
2450
2614
  ...root.nodes ? { nodes: this.injectSelection(root.nodes, root.table) } : {}
2451
2615
  };
2616
+ case "aggregateGroups":
2617
+ return {
2618
+ ...root,
2619
+ where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table))
2620
+ };
2452
2621
  case "mutation":
2453
2622
  return this.injectMutation(root);
2454
2623
  }
@@ -2541,20 +2710,33 @@ function collectTableSet(roots, ctx) {
2541
2710
  };
2542
2711
  for (const root of roots) {
2543
2712
  tables.add(root.table);
2544
- if (root.kind === "select" || root.kind === "byPk") {
2545
- visitPred(root.where, root.table);
2546
- visitSelection(root.selection, root.table);
2547
- }
2548
- if (root.kind === "aggregate") {
2549
- visitPred(root.where, root.table);
2550
- if (root.nodes) visitSelection(root.nodes, root.table);
2551
- }
2552
- if (root.kind === "mutation") {
2553
- if (root.cowSynced !== void 0) tables.add(root.cowSynced);
2554
- if (root.where) visitPred(root.where, root.table);
2555
- if (root.insertCheck) visitPred(root.insertCheck, root.table);
2556
- if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
2557
- if (root.returning) visitSelection(root.returning, root.table);
2713
+ switch (root.kind) {
2714
+ case "select":
2715
+ case "byPk":
2716
+ visitPred(root.where, root.table);
2717
+ visitSelection(root.selection, root.table);
2718
+ break;
2719
+ case "aggregate":
2720
+ visitPred(root.where, root.table);
2721
+ if (root.nodes) visitSelection(root.nodes, root.table);
2722
+ break;
2723
+ case "aggregateGroups":
2724
+ visitPred(root.where, root.table);
2725
+ break;
2726
+ case "mutation":
2727
+ if (root.cowSynced !== void 0) tables.add(root.cowSynced);
2728
+ if (root.where) visitPred(root.where, root.table);
2729
+ if (root.insertCheck) visitPred(root.insertCheck, root.table);
2730
+ if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
2731
+ if (root.returning) visitSelection(root.returning, root.table);
2732
+ break;
2733
+ default: {
2734
+ const exhausted = root;
2735
+ throw new DocumentCompileError(
2736
+ `internal: unhandled root kind in tableSet collection ${JSON.stringify(exhausted)}`,
2737
+ "internal"
2738
+ );
2739
+ }
2558
2740
  }
2559
2741
  }
2560
2742
  return [...tables].sort();
@@ -2572,7 +2754,12 @@ function typeNameForTable(table) {
2572
2754
  }
2573
2755
  function queryRootNames(table) {
2574
2756
  const names = tableNames(table);
2575
- return { select: names.selectRoot, byPk: names.byPkRoot, aggregate: names.aggregateRoot };
2757
+ return {
2758
+ select: names.selectRoot,
2759
+ byPk: names.byPkRoot,
2760
+ aggregate: names.aggregateRoot,
2761
+ aggregateGroups: names.aggregateGroupsRoot
2762
+ };
2576
2763
  }
2577
2764
  function mutationRootNames(table) {
2578
2765
  const names = tableNames(table);
@@ -2653,7 +2840,14 @@ function resolveColumnEntries(table, enums, diagnostics) {
2653
2840
  type = { enum: enumDef.name, values: [...enumDef.values] };
2654
2841
  }
2655
2842
  }
2656
- return { name: column.name, columnName: column.columnName, type, nullable: column.nullable, deprecated: column.deprecated };
2843
+ return {
2844
+ name: column.name,
2845
+ columnName: column.columnName,
2846
+ type,
2847
+ nullable: column.nullable,
2848
+ deprecated: column.deprecated,
2849
+ onUpdateFn: column.onUpdateFn !== void 0
2850
+ };
2657
2851
  });
2658
2852
  }
2659
2853
  function logicalColumn(entries, ref) {
@@ -2702,6 +2896,7 @@ function buildCompileContext(rawCanonical) {
2702
2896
  primaryKey2.push(resolved);
2703
2897
  }
2704
2898
  const active = entries.filter((c) => !c.deprecated);
2899
+ const touchOnUpdate = isView ? [] : active.filter((c) => c.onUpdateFn).map((c) => c.name);
2705
2900
  const constraints = [];
2706
2901
  if (primaryKey2.length > 0) constraints.push({ name: `${table.table}_pkey`, columns: [...primaryKey2] });
2707
2902
  for (const column of table.columns) {
@@ -2722,14 +2917,15 @@ function buildCompileContext(rawCanonical) {
2722
2917
  // Synced twins (and views reading them, resolved transitively below)
2723
2918
  // are claims-gated — see tableMetaIrSchema.claimsGated.
2724
2919
  claimsGated: relationIsClaimsGated(table.table),
2725
- columns: entries.map(({ deprecated: _deprecated, ...column }) => column),
2920
+ columns: entries.map(({ deprecated: _deprecated, onUpdateFn: _onUpdateFn, ...column }) => column),
2726
2921
  primaryKey: isView ? primaryKey2 : primaryKey2.length > 0 ? primaryKey2 : ["id"],
2727
2922
  links: [],
2728
2923
  filterable: entries.map((c) => c.name),
2729
2924
  orderable: entries.filter((c) => c.type !== "jsonb").map((c) => c.name),
2730
2925
  insertable: isView ? [] : active.map((c) => c.name),
2731
- settable: isView ? [] : active.map((c) => c.name),
2926
+ settable: isView ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
2732
2927
  appendable: isView ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
2928
+ ...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
2733
2929
  constraints: isView ? [] : constraints,
2734
2930
  dynamicFilterMaxDepth: 6,
2735
2931
  dynamicFilterMaxNodes: 64
@@ -2750,7 +2946,7 @@ function buildCompileContext(rawCanonical) {
2750
2946
  });
2751
2947
  continue;
2752
2948
  }
2753
- const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false })) : columnEntries.get(targetName);
2949
+ const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false, onUpdateFn: false })) : columnEntries.get(targetName);
2754
2950
  const onEntries = link.kind === "toMany" ? targetEntries : entries;
2755
2951
  const referencesEntries = link.kind === "toMany" ? entries : targetEntries;
2756
2952
  const on = logicalColumn(onEntries, link.on);
@@ -2847,6 +3043,9 @@ function buildCompileContext(rawCanonical) {
2847
3043
  queryRoots.set(roots.byPk, { kind: "byPk", table: table.table });
2848
3044
  }
2849
3045
  queryRoots.set(roots.aggregate, { kind: "aggregate", table: table.table });
3046
+ if (table.columns.some((column) => column.type !== "jsonb")) {
3047
+ queryRoots.set(roots.aggregateGroups, { kind: "aggregateGroups", table: table.table });
3048
+ }
2850
3049
  const hasWritePolicy = table.policies.write !== void 0 || table.policies.insert !== void 0 || table.policies.update !== void 0;
2851
3050
  if (!viewNames.has(table.table) && table.source === void 0 && hasWritePolicy) {
2852
3051
  for (const [op, field] of Object.entries(mutationRootNames(table.table))) {
@@ -2982,6 +3181,16 @@ function computeOpsHash(opHashes) {
2982
3181
  }
2983
3182
  var DEFAULT_LIMIT = 100;
2984
3183
  var MAX_LIMIT = 1e3;
3184
+ var MAX_GROUPS = 1e3;
3185
+ var DATE_BUCKET_BY_ENUM = {
3186
+ YEAR: "year",
3187
+ QUARTER: "quarter",
3188
+ MONTH: "month",
3189
+ WEEK: "week",
3190
+ DAY: "day",
3191
+ HOUR: "hour"
3192
+ };
3193
+ var BUCKETABLE_TYPES = /* @__PURE__ */ new Set(["timestamptz", "date"]);
2985
3194
  var BOOLEXP_OPS = {
2986
3195
  _eq: "eq",
2987
3196
  _neq: "neq",
@@ -3013,6 +3222,14 @@ function fail(message, code, candidates = []) {
3013
3222
  function argsByName(field) {
3014
3223
  return new Map((field.arguments ?? []).map((arg) => [arg.name.value, arg]));
3015
3224
  }
3225
+ function* aggregateBlockFields(selectionSet) {
3226
+ for (const node of selectionSet.selections) {
3227
+ if (node.kind !== Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
3228
+ if (node.name.value === "__typename") continue;
3229
+ if (node.alias) fail(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
3230
+ yield node;
3231
+ }
3232
+ }
3016
3233
  function assertKnownArgs(field, allowed) {
3017
3234
  for (const arg of field.arguments ?? []) {
3018
3235
  if (!allowed.includes(arg.name.value)) {
@@ -3313,11 +3530,8 @@ var OperationLowerer = class {
3313
3530
  lowerAggregateSelection(selectionSet, table) {
3314
3531
  const result = {};
3315
3532
  let sawAggregate = false;
3316
- for (const node of selectionSet.selections) {
3317
- if (node.kind !== Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
3533
+ for (const node of aggregateBlockFields(selectionSet)) {
3318
3534
  const name = node.name.value;
3319
- if (name === "__typename") continue;
3320
- if (node.alias) fail(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
3321
3535
  if (name === "nodes") {
3322
3536
  if (!node.selectionSet) fail("'nodes' needs a selection set", "invalid-aggregate");
3323
3537
  result.nodes = this.lowerSelection(node.selectionSet, table);
@@ -3327,48 +3541,116 @@ var OperationLowerer = class {
3327
3541
  if (sawAggregate) fail("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
3328
3542
  sawAggregate = true;
3329
3543
  if (!node.selectionSet) fail("'aggregate' needs a selection set", "invalid-aggregate");
3330
- for (const aggField of node.selectionSet.selections) {
3331
- if (aggField.kind !== Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
3332
- const aggName = aggField.name.value;
3333
- if (aggName === "__typename") continue;
3334
- if (aggField.alias) fail(`aliases are not supported inside aggregate blocks ('${aggField.alias.value}')`, "invalid-aggregate");
3335
- if (aggName === "count") {
3336
- assertKnownArgs(aggField, ["columns", "distinct"]);
3337
- const args = argsByName(aggField);
3338
- const count = {};
3339
- const columns = args.get("columns");
3340
- if (columns) count.columns = this.lowerDistinctOn(columns.value, table);
3341
- const distinct = args.get("distinct");
3342
- if (distinct) {
3343
- if (distinct.value.kind !== Kind.BOOLEAN) fail("count(distinct:) must be a boolean literal", "invalid-aggregate");
3344
- count.distinct = distinct.value.value;
3345
- }
3346
- result.count = count;
3347
- continue;
3544
+ Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
3545
+ }
3546
+ if (!sawAggregate && !result.nodes) fail("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
3547
+ return result;
3548
+ }
3549
+ /**
3550
+ * The `{ key {…} aggregate {…} }` block of a grouped aggregate root. Same
3551
+ * `aggregate` sub-block as the ungrouped shape (shared via
3552
+ * `lowerAggregateFields`), but `key` replaces `nodes` — see the IR comment on
3553
+ * `aggregateGroupsRootSchema` for why grouped roots have no `nodes`.
3554
+ */
3555
+ lowerGroupedAggregateSelection(selectionSet, table, groupBy) {
3556
+ const groupColumns = new Set(groupBy.map((entry) => entry.column));
3557
+ const result = { key: [] };
3558
+ let sawAggregate = false;
3559
+ let sawKey = false;
3560
+ for (const node of aggregateBlockFields(selectionSet)) {
3561
+ const name = node.name.value;
3562
+ if (name === "key") {
3563
+ if (sawKey) fail("one key block per grouped aggregate", "invalid-aggregate");
3564
+ sawKey = true;
3565
+ if (!node.selectionSet) fail("'key' needs a selection set", "invalid-aggregate");
3566
+ result.key = this.lowerGroupKeySelection(node.selectionSet, table, groupColumns);
3567
+ continue;
3568
+ }
3569
+ if (name === "nodes") {
3570
+ fail("'nodes' is not available on a grouped aggregate \u2014 query the rows separately", "invalid-aggregate", ["key", "aggregate"]);
3571
+ }
3572
+ if (name !== "aggregate") fail(`unknown grouped-aggregate field '${name}'`, "invalid-aggregate", ["key", "aggregate"]);
3573
+ if (sawAggregate) fail("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
3574
+ sawAggregate = true;
3575
+ if (!node.selectionSet) fail("'aggregate' needs a selection set", "invalid-aggregate");
3576
+ Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
3577
+ }
3578
+ const rendersAggregate = result.count !== void 0 || (result.sum?.length ?? 0) > 0 || (result.avg?.length ?? 0) > 0;
3579
+ if (result.key.length === 0 && !rendersAggregate) {
3580
+ fail(
3581
+ "grouped aggregate needs at least one key column or an aggregate \u2014 a block selecting only __typename produces nothing",
3582
+ "invalid-aggregate",
3583
+ ["key", "aggregate"]
3584
+ );
3585
+ }
3586
+ return result;
3587
+ }
3588
+ /**
3589
+ * `key { month officer }` → the projected group columns. Every field must be
3590
+ * one of the query's own group keys: any other column is indeterminate
3591
+ * within a group, so selecting it is a document error rather than an
3592
+ * arbitrary pick.
3593
+ */
3594
+ lowerGroupKeySelection(selectionSet, table, groupColumns) {
3595
+ const key = [];
3596
+ for (const node of selectionSet.selections) {
3597
+ if (node.kind !== Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
3598
+ const name = node.name.value;
3599
+ if (name === "__typename") continue;
3600
+ if (!this.column(table, name)) {
3601
+ fail(`unknown key column '${name}' on '${table}'`, "unknown-column", [...groupColumns]);
3602
+ }
3603
+ if (!groupColumns.has(name)) {
3604
+ fail(
3605
+ `key column '${name}' is not one of this query's groupBy keys \u2014 it has no single value within a group`,
3606
+ "invalid-group-key",
3607
+ [...groupColumns]
3608
+ );
3609
+ }
3610
+ key.push({ alias: node.alias?.value ?? name, column: name });
3611
+ }
3612
+ return key;
3613
+ }
3614
+ /** The `count` / `sum {…}` / `avg {…}` fields inside an `aggregate { … }` block. */
3615
+ lowerAggregateFields(selectionSet, table) {
3616
+ const result = {};
3617
+ for (const aggField of aggregateBlockFields(selectionSet)) {
3618
+ const aggName = aggField.name.value;
3619
+ if (aggName === "count") {
3620
+ assertKnownArgs(aggField, ["columns", "distinct"]);
3621
+ const args = argsByName(aggField);
3622
+ const count = {};
3623
+ const columns = args.get("columns");
3624
+ if (columns) count.columns = this.lowerDistinctOn(columns.value, table);
3625
+ const distinct = args.get("distinct");
3626
+ if (distinct) {
3627
+ if (distinct.value.kind !== Kind.BOOLEAN) fail("count(distinct:) must be a boolean literal", "invalid-aggregate");
3628
+ count.distinct = distinct.value.value;
3348
3629
  }
3349
- if (aggName === "sum" || aggName === "avg") {
3350
- if (!aggField.selectionSet) fail(`'${aggName}' needs a column selection`, "invalid-aggregate");
3351
- const columns = [];
3352
- for (const columnField of aggField.selectionSet.selections) {
3353
- if (columnField.kind !== Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
3354
- if (columnField.name.value === "__typename") continue;
3355
- const column = this.column(table, columnField.name.value);
3356
- if (!column || typeof column.type !== "string" || !NUMERIC_TYPES.has(column.type)) {
3357
- fail(
3358
- `'${aggName}' supports numeric columns only, got '${columnField.name.value}' on '${table}'`,
3359
- "invalid-aggregate",
3360
- this.meta(table).columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type)).map((c) => c.name)
3361
- );
3362
- }
3363
- columns.push(columnField.name.value);
3630
+ result.count = count;
3631
+ continue;
3632
+ }
3633
+ if (aggName === "sum" || aggName === "avg") {
3634
+ if (!aggField.selectionSet) fail(`'${aggName}' needs a column selection`, "invalid-aggregate");
3635
+ const columns = [];
3636
+ for (const columnField of aggField.selectionSet.selections) {
3637
+ if (columnField.kind !== Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
3638
+ if (columnField.name.value === "__typename") continue;
3639
+ const column = this.column(table, columnField.name.value);
3640
+ if (!column || typeof column.type !== "string" || !NUMERIC_TYPES.has(column.type)) {
3641
+ fail(
3642
+ `'${aggName}' supports numeric columns only, got '${columnField.name.value}' on '${table}'`,
3643
+ "invalid-aggregate",
3644
+ this.meta(table).columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type)).map((c) => c.name)
3645
+ );
3364
3646
  }
3365
- result[aggName] = columns;
3366
- continue;
3647
+ columns.push(columnField.name.value);
3367
3648
  }
3368
- fail(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
3649
+ result[aggName] = columns;
3650
+ continue;
3369
3651
  }
3652
+ fail(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
3370
3653
  }
3371
- if (!sawAggregate && !result.nodes) fail("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
3372
3654
  return result;
3373
3655
  }
3374
3656
  // -------------------------------------------------------------------------
@@ -3378,19 +3660,49 @@ var OperationLowerer = class {
3378
3660
  const roots = [];
3379
3661
  for (const node of operation.selectionSet.selections) {
3380
3662
  if (node.kind !== Kind.FIELD) fail("fragments are not supported at the operation root", "invalid-root");
3663
+ if (node.selectionSet) visit(node.selectionSet, {
3664
+ Directive(directive) {
3665
+ if (directive.name.value === "assertAffectedRows") {
3666
+ fail("@assertAffectedRows is supported on mutation roots only", "invalid-affected-row-assertion");
3667
+ }
3668
+ }
3669
+ });
3670
+ const assertion = this.parseAffectedRowAssertion(node, kind);
3381
3671
  const gate = this.parseRootDirectives(node);
3382
3672
  if (gate.omit) continue;
3383
3673
  const root = kind === "query" ? this.lowerQueryRoot(node) : this.lowerMutationRoot(node);
3384
- roots.push(gate.includeIf ? { ...root, includeIf: gate.includeIf } : root);
3674
+ const guarded = root.kind === "mutation" && assertion !== void 0 ? { ...root, assertAffectedRows: assertion } : root;
3675
+ roots.push(gate.includeIf ? { ...guarded, includeIf: gate.includeIf } : guarded);
3385
3676
  }
3386
3677
  if (roots.length === 0) fail("operation has no roots (all statically skipped?)", "invalid-root");
3387
3678
  return roots;
3388
3679
  }
3680
+ parseAffectedRowAssertion(node, kind) {
3681
+ const assertions = node.directives?.filter((directive) => directive.name.value === "assertAffectedRows") ?? [];
3682
+ if (!assertions.length) return void 0;
3683
+ if (kind !== "mutation" || assertions.length !== 1) {
3684
+ fail("@assertAffectedRows is supported once per mutation root", "invalid-affected-row-assertion");
3685
+ }
3686
+ if (node.directives?.some((directive) => ["include", "skip"].includes(directive.name.value))) {
3687
+ fail("an @assertAffectedRows root cannot use @include or @skip", "invalid-affected-row-assertion");
3688
+ }
3689
+ const args = assertions[0].arguments;
3690
+ const arg = args?.[0];
3691
+ if (args?.length !== 1 || arg?.name.value !== "exactly" || arg.value.kind !== Kind.INT) {
3692
+ fail("@assertAffectedRows(exactly:) requires a document-authored nonnegative Int literal", "invalid-affected-row-assertion");
3693
+ }
3694
+ const count = Number(arg.value.value);
3695
+ if (!Number.isInteger(count) || count < 0 || count > 2147483647) {
3696
+ fail("@assertAffectedRows(exactly:) must be between 0 and 2147483647", "invalid-affected-row-assertion");
3697
+ }
3698
+ return count;
3699
+ }
3389
3700
  parseRootDirectives(node) {
3390
3701
  let includeIf;
3391
3702
  let omit = false;
3392
3703
  for (const directive of node.directives ?? []) {
3393
3704
  const name = directive.name.value;
3705
+ if (name === "assertAffectedRows") continue;
3394
3706
  if (name !== "include" && name !== "skip") {
3395
3707
  fail(`directive @${name} is not supported on fields`, "unknown-directive", ["include", "skip"]);
3396
3708
  }
@@ -3439,6 +3751,9 @@ var OperationLowerer = class {
3439
3751
  const pk = this.lowerPkArgs(node, table);
3440
3752
  return { kind: "byPk", alias, table, selection: this.lowerSelection(node.selectionSet, table), pk, where: CONST_TRUE };
3441
3753
  }
3754
+ if (rootInfo.kind === "aggregateGroups") {
3755
+ return this.lowerAggregateGroupsRoot(node, { alias, table });
3756
+ }
3442
3757
  assertKnownArgs(node, ["where"]);
3443
3758
  if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
3444
3759
  const whereArg = args.get("where");
@@ -3451,6 +3766,108 @@ var OperationLowerer = class {
3451
3766
  ...aggregate
3452
3767
  };
3453
3768
  }
3769
+ /** `<t>AggregateGroups(where:, groupBy:, limit:, offset:)` → an aggregateGroups root. */
3770
+ lowerAggregateGroupsRoot(node, { alias, table }) {
3771
+ assertKnownArgs(node, ["where", "groupBy", "limit", "offset"]);
3772
+ if (!node.selectionSet) fail(`'${node.name.value}' needs a selection set`, "invalid-selection");
3773
+ const args = argsByName(node);
3774
+ const groupByArg = args.get("groupBy");
3775
+ if (!groupByArg) fail(`'${node.name.value}' requires a 'groupBy' argument`, "missing-group-by");
3776
+ const whereArg = args.get("where");
3777
+ const groupBy = this.lowerGroupBy(groupByArg.value, table);
3778
+ const aggregate = this.lowerGroupedAggregateSelection(node.selectionSet, table, groupBy);
3779
+ const offset = this.lowerOffset(args.get("offset")?.value);
3780
+ return {
3781
+ kind: "aggregateGroups",
3782
+ alias,
3783
+ table,
3784
+ where: whereArg ? this.lowerBoolExp(whereArg.value, table) : CONST_TRUE,
3785
+ groupBy,
3786
+ key: aggregate.key,
3787
+ ...aggregate.count ? { count: aggregate.count } : {},
3788
+ ...aggregate.sum ? { sum: aggregate.sum } : {},
3789
+ ...aggregate.avg ? { avg: aggregate.avg } : {},
3790
+ limit: this.lowerGroupLimit(args.get("limit")?.value),
3791
+ ...offset ? { offset } : {}
3792
+ };
3793
+ }
3794
+ /**
3795
+ * `groupBy: [{ column: startedAt, bucket: MONTH }, …]` → GroupByKeyIr[].
3796
+ *
3797
+ * Document-authored only: a whole-argument variable is rejected rather than
3798
+ * becoming a §4f slot. Group keys choose which expressions reach GROUP BY,
3799
+ * and dynamic-input.ts refuses aggregation shapes from client input on the
3800
+ * same reasoning — `where` stays dynamic, which is what runtime filtering
3801
+ * actually needs.
3802
+ */
3803
+ lowerGroupBy(node, table) {
3804
+ if (node.kind === Kind.VARIABLE) {
3805
+ fail(
3806
+ `groupBy on '${table}' must be written in the document \u2014 it cannot come from a variable`,
3807
+ "dynamic-group-by"
3808
+ );
3809
+ }
3810
+ const items = node.kind === Kind.LIST ? node.values : [node];
3811
+ if (items.length === 0) fail(`groupBy on '${table}' needs at least one key`, "invalid-group-by");
3812
+ const seen = /* @__PURE__ */ new Set();
3813
+ return items.map((item) => {
3814
+ if (item.kind !== Kind.OBJECT) {
3815
+ fail(`groupBy entries on '${table}' must be objects like { column: foo }`, "invalid-group-by");
3816
+ }
3817
+ const fields = new Map(item.fields.map((field) => [field.name.value, field.value]));
3818
+ for (const key of fields.keys()) {
3819
+ if (key !== "column" && key !== "bucket") {
3820
+ fail(`unknown groupBy field '${key}' on '${table}'`, "invalid-group-by", ["column", "bucket"]);
3821
+ }
3822
+ }
3823
+ const columnNode = fields.get("column");
3824
+ if (!columnNode) fail(`groupBy entries on '${table}' need a 'column'`, "invalid-group-by");
3825
+ if (columnNode.kind !== Kind.ENUM) {
3826
+ fail(`groupBy column on '${table}' must be a group-by column enum value`, "invalid-group-by");
3827
+ }
3828
+ const column = this.column(table, columnNode.value);
3829
+ if (!column) {
3830
+ fail(`unknown groupBy column '${columnNode.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
3831
+ }
3832
+ if (column.type === "jsonb") {
3833
+ fail(`groupBy column '${table}.${columnNode.value}' is jsonb and cannot be a group key`, "invalid-group-by");
3834
+ }
3835
+ const bucketNode = fields.get("bucket");
3836
+ let bucket;
3837
+ if (bucketNode) {
3838
+ if (bucketNode.kind !== Kind.ENUM) {
3839
+ fail(`groupBy bucket on '${table}.${columnNode.value}' must be a DateBucket enum value`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
3840
+ }
3841
+ bucket = DATE_BUCKET_BY_ENUM[bucketNode.value];
3842
+ if (!bucket) {
3843
+ fail(`unknown DateBucket '${bucketNode.value}'`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
3844
+ }
3845
+ if (typeof column.type !== "string" || !BUCKETABLE_TYPES.has(column.type)) {
3846
+ fail(
3847
+ `groupBy bucket is only valid on date/timestamptz columns \u2014 '${table}.${columnNode.value}' is ${typeof column.type === "string" ? column.type : "an enum"}`,
3848
+ "invalid-group-by"
3849
+ );
3850
+ }
3851
+ }
3852
+ if (seen.has(columnNode.value)) {
3853
+ fail(`duplicate groupBy column '${columnNode.value}' on '${table}'`, "invalid-group-by");
3854
+ }
3855
+ seen.add(columnNode.value);
3856
+ return bucket ? { column: columnNode.value, bucket } : { column: columnNode.value };
3857
+ });
3858
+ }
3859
+ /** Like lowerLimit, but bounded by MAX_GROUPS and defaulting to it. */
3860
+ lowerGroupLimit(node) {
3861
+ const base = { default: MAX_GROUPS, max: MAX_GROUPS };
3862
+ if (!node) return base;
3863
+ if (node.kind === Kind.VARIABLE) return { ...base, variable: node.name.value };
3864
+ if (node.kind === Kind.INT) {
3865
+ const value = Number(node.value);
3866
+ if (value < 1) fail("limit must be a positive integer", "invalid-limit");
3867
+ return { ...base, value: Math.min(value, MAX_GROUPS) };
3868
+ }
3869
+ fail("limit must be an integer literal or a variable", "invalid-limit");
3870
+ }
3454
3871
  /** byPk-style pk arguments: one argument per primary-key column, exactly. */
3455
3872
  lowerPkArgs(node, table) {
3456
3873
  const meta = this.meta(table);
@@ -3960,7 +4377,7 @@ function compileNamespace({ canonical, sdl, documents }) {
3960
4377
  const loweredRoots = lowerOperation({ restricted: { ...restricted, definition: flattened }, ctx });
3961
4378
  const { roots, tableSet } = injectPolicies({ roots: loweredRoots, ctx });
3962
4379
  operations[restricted.name] = {
3963
- irVersion: IR_VERSION,
4380
+ irVersion: roots.some((root) => root.kind === "mutation" && root.assertAffectedRows !== void 0) ? IR_VERSION : LEGACY_IR_VERSION,
3964
4381
  name: restricted.name,
3965
4382
  opHash: computeOpHash({ definition: flattened, schemaHash }),
3966
4383
  kind: restricted.kind,
@@ -3987,7 +4404,7 @@ function compileNamespace({ canonical, sdl, documents }) {
3987
4404
  }
3988
4405
  if (diagnostics.some((d) => d.severity === "error")) return { ok: false, diagnostics };
3989
4406
  const artifact = {
3990
- irVersion: IR_VERSION,
4407
+ irVersion: requiredArtifactIrVersion(Object.values(operations)),
3991
4408
  namespace: ctx.namespace,
3992
4409
  schemaHash,
3993
4410
  opsHash: computeOpsHash(Object.values(operations).map((op) => op.opHash)),
@@ -4187,12 +4604,12 @@ function castParts(cast) {
4187
4604
 
4188
4605
  // src/predicate/ast.ts
4189
4606
  var COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
4190
- function walkPredicate(predicate, visit) {
4191
- visit(predicate);
4607
+ function walkPredicate(predicate, visit2) {
4608
+ visit2(predicate);
4192
4609
  if (predicate.kind === "and" || predicate.kind === "or") {
4193
- for (const operand of predicate.operands) walkPredicate(operand, visit);
4610
+ for (const operand of predicate.operands) walkPredicate(operand, visit2);
4194
4611
  }
4195
- if (predicate.kind === "not") walkPredicate(predicate.operand, visit);
4612
+ if (predicate.kind === "not") walkPredicate(predicate.operand, visit2);
4196
4613
  }
4197
4614
  function predicateOperands(predicate) {
4198
4615
  const operands = [];
@@ -5167,7 +5584,7 @@ var FACTORIES2 = {
5167
5584
  var SCHEMA_IMPORT_KEY2 = "$schema";
5168
5585
  var enumImportKey = (name) => `$enum_${name}`;
5169
5586
  var columnTablesV2 = (definition) => definition.tables.filter((table) => !table.source);
5170
- function buildDrizzleEntitiesV2(definition) {
5587
+ function buildDrizzleEntitiesV2(definition, options = {}) {
5171
5588
  const schemaName = ormSchemaName(definition.namespace);
5172
5589
  const schema = pgSchema(schemaName);
5173
5590
  const imports = { [SCHEMA_IMPORT_KEY2]: schema };
@@ -5182,11 +5599,19 @@ function buildDrizzleEntitiesV2(definition) {
5182
5599
  }
5183
5600
  const byName = new Map(definition.tables.map((table) => [table.table, table]));
5184
5601
  for (const table of sortByEnforcedLinks2(columnTablesV2(definition))) {
5185
- const drizzleTable = buildTable2(schema, table, built, enums, byName);
5602
+ const drizzleTable = buildTable2(schema, table, built, enums, byName, options.introspection ?? false);
5186
5603
  built.set(table.table, drizzleTable);
5187
5604
  imports[table.table] = drizzleTable;
5188
5605
  tables[table.table] = drizzleTable;
5189
5606
  }
5607
+ if (options.introspection) {
5608
+ for (const table of definition.tables.filter((table2) => table2.source)) {
5609
+ const columns = Object.fromEntries(
5610
+ table.columns.map((column) => [column.columnName, buildViewColumnV2({ view: table.table, column, enums })])
5611
+ );
5612
+ imports[table.table] = schema.view(table.table, columns).existing();
5613
+ }
5614
+ }
5190
5615
  for (const view of definition.views ?? []) {
5191
5616
  const columns = Object.fromEntries(
5192
5617
  view.columns.map((column) => [column.columnName, buildViewColumnV2({ view: view.view, column, enums })])
@@ -5430,9 +5855,10 @@ function replaceQualifiedRelation(sql4, sourceSchema, sourceName, boundSchema, b
5430
5855
  function generateSnapshotV2(definition, previousId3) {
5431
5856
  const { imports, schemaName } = buildDrizzleEntitiesV2(definition);
5432
5857
  const snapshot = generateDrizzleJson(imports, previousId3, [schemaName]);
5433
- if ((definition.views?.length ?? 0) > 0) {
5858
+ if ((definition.views?.length ?? 0) > 0 || definition.tables.some((table) => table.foreignKeys?.length)) {
5434
5859
  const extended = snapshot;
5435
5860
  extended._sequenceOrm = {
5861
+ ...definition.tables.some((table) => table.foreignKeys?.length) ? { foreignKeys: Object.fromEntries(definition.tables.flatMap((table) => (table.foreignKeys ?? []).map((fk) => [`${table.table}.${fk.name}`, structuredClone(fk)]))) } : {},
5436
5862
  viewDependencies: Object.fromEntries(
5437
5863
  (definition.views ?? []).map((view) => [view.view, [...view.dependencies].sort()])
5438
5864
  )
@@ -5450,7 +5876,7 @@ function buildViewColumnV2({
5450
5876
  if (!column.nullable) builder = builder.notNull();
5451
5877
  return builder;
5452
5878
  }
5453
- function buildTable2(schema, table, built, enums, byName) {
5879
+ function buildTable2(schema, table, built, enums, byName, introspection) {
5454
5880
  const primaryKeyColumns2 = table.primaryKey ?? [];
5455
5881
  if (primaryKeyColumns2.length === 0) {
5456
5882
  throw new OrmDefinitionError(`table '${table.table}' declares no primaryKey`);
@@ -5466,13 +5892,22 @@ function buildTable2(schema, table, built, enums, byName) {
5466
5892
  const columns = Object.fromEntries(
5467
5893
  table.columns.map((column) => [
5468
5894
  column.columnName,
5469
- buildColumnV2({ table: table.table, column, singlePrimaryKey, compositeMembers, enums })
5895
+ buildColumnV2({ table: table.table, column, singlePrimaryKey, compositeMembers, enums, introspection })
5470
5896
  ])
5471
5897
  );
5472
5898
  const drizzleTable = schema.table(table.table, columns, (self) => [
5473
5899
  ...buildCompositePrimaryKey(table, primaryKeyColumns2, self),
5474
5900
  ...buildIndexes2(table, self),
5475
- ...buildForeignKeys2(table, self, built, byName)
5901
+ ...buildForeignKeys2(table, self, built, byName),
5902
+ ...(table.foreignKeys ?? []).map((fk) => {
5903
+ const target = fk.target === table.table ? self : built.get(fk.target);
5904
+ if (!target) throw new OrmDefinitionError(`foreign key '${fk.name}' has no target '${fk.target}'`);
5905
+ return foreignKey({
5906
+ name: fk.name,
5907
+ columns: fk.columns.map((column) => self[column]),
5908
+ foreignColumns: fk.references.map((column) => target[column])
5909
+ });
5910
+ })
5476
5911
  ]);
5477
5912
  return drizzleTable.enableRLS();
5478
5913
  }
@@ -5489,7 +5924,8 @@ function buildColumnV2({
5489
5924
  column,
5490
5925
  singlePrimaryKey,
5491
5926
  compositeMembers,
5492
- enums
5927
+ enums,
5928
+ introspection
5493
5929
  }) {
5494
5930
  const factory = resolveColumnFactory(table, column, enums);
5495
5931
  let builder = factory(column.columnName);
@@ -5510,7 +5946,7 @@ function buildColumnV2({
5510
5946
  }
5511
5947
  builder = builder.default(sql.raw(column.defaultFn));
5512
5948
  } else if (column.default !== void 0 && column.default !== null) {
5513
- builder = builder.default(column.default);
5949
+ builder = introspection && column.type === "jsonb" && typeof column.default === "string" ? builder.default(sql.raw(`${quoteLiteral(column.default)}::jsonb`)) : builder.default(column.default);
5514
5950
  }
5515
5951
  return builder;
5516
5952
  }
@@ -5695,7 +6131,8 @@ async function planMigrationV2({
5695
6131
  assertValidViewDependencies(canonical);
5696
6132
  const hints = applyMigrationHintsV2(previous, canonical);
5697
6133
  const snapshot = generateSnapshotV2(canonical, previousId(previous));
5698
- const baseDdl = await diffStatements(withoutViews(hints.adjusted), withoutViews(snapshot));
6134
+ const generatedBaseDdl = await diffStatements(withoutViews(hints.adjusted), withoutViews(snapshot));
6135
+ const baseDdl = orderForeignKeyAdditions(canonical, generatedBaseDdl);
5699
6136
  const previousViews = snapshotViews(hints.adjusted);
5700
6137
  const currentViews = snapshotViews(snapshot);
5701
6138
  const previousDependencies = snapshotViewDependencies(hints.adjusted);
@@ -5732,6 +6169,7 @@ async function planMigrationV2({
5732
6169
  ...dropOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(dropDdl, name, schema, "DROP") })),
5733
6170
  ...hints.statements.map((statement) => ({ kind: "hint", sql: statement })),
5734
6171
  ...baseDdl.map((statement) => ({ kind: "ddl", sql: statement.replace(/;\s*$/, "") })),
6172
+ ...foreignKeyTimingStatements(canonical, previous, baseDdl).map((sql4) => ({ kind: "ddl", sql: sql4 })),
5735
6173
  ...createOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(createDdl, name, schema, "CREATE") })),
5736
6174
  ...createOrder.map((name) => {
5737
6175
  const view = currentViewDefinitions.get(name);
@@ -5741,6 +6179,29 @@ async function planMigrationV2({
5741
6179
  ];
5742
6180
  return { namespace: canonical.namespace, schema, statements, snapshot };
5743
6181
  }
6182
+ function orderForeignKeyAdditions(canonical, statements) {
6183
+ if (!canonical.tables.some((table) => table.foreignKeys?.length)) return [...statements];
6184
+ const addsForeignKey = (statement) => /^ALTER TABLE[\s\S]* ADD CONSTRAINT[\s\S]* FOREIGN KEY\b/.test(statement);
6185
+ return [...statements.filter((statement) => !addsForeignKey(statement)), ...statements.filter(addsForeignKey)];
6186
+ }
6187
+ function foreignKeyTimingStatements(canonical, previous, baseDdl) {
6188
+ const prior = previous._sequenceOrm?.foreignKeys ?? {};
6189
+ const result = [];
6190
+ for (const table of canonical.tables) {
6191
+ for (const fk of table.foreignKeys ?? []) {
6192
+ const before = prior[`${table.table}.${fk.name}`];
6193
+ const recreated = baseDdl.some((statement) => statement.includes(`ALTER TABLE ${quoteIdentifier(ormSchemaName(canonical.namespace))}.${quoteIdentifier(table.table)} ADD CONSTRAINT ${quoteIdentifier(fk.name)}`));
6194
+ if (fk.deferrable ? !before || before.deferrable !== fk.deferrable || recreated : before?.deferrable && !recreated) {
6195
+ result.push(foreignKeyTimingSql(ormSchemaName(canonical.namespace), table.table, fk.name, fk.deferrable));
6196
+ }
6197
+ }
6198
+ }
6199
+ return result;
6200
+ }
6201
+ function foreignKeyTimingSql(schema, table, name, timing) {
6202
+ const clause = timing === void 0 ? "NOT DEFERRABLE" : timing === "initially-deferred" ? "DEFERRABLE INITIALLY DEFERRED" : "DEFERRABLE INITIALLY IMMEDIATE";
6203
+ return `ALTER TABLE ${quoteIdentifier(schema)}.${quoteIdentifier(table)} ALTER CONSTRAINT ${quoteIdentifier(name)} ${clause}`;
6204
+ }
5744
6205
  function validateViewCatalogStatement({
5745
6206
  canonical,
5746
6207
  view,
@@ -6861,8 +7322,12 @@ function generateSdl(canonical, options = {}) {
6861
7322
  '"""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'
6862
7323
  );
6863
7324
  blocks.push("directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT");
7325
+ blocks.push(
7326
+ '"""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'
7327
+ );
6864
7328
  for (const scalar2 of CUSTOM_SCALARS) blocks.push(`scalar ${scalar2}`);
6865
7329
  blocks.push(renderEnum(ORDER_BY_ENUM, [...ORDER_BY_ENUM_VALUES]));
7330
+ blocks.push(renderEnum(DATE_BUCKET_ENUM, DATE_BUCKETS.map((bucket) => bucket.toUpperCase())));
6866
7331
  for (const enumName of [...usedEnums].sort()) {
6867
7332
  const entry = enumByName.get(enumName);
6868
7333
  if (!entry) continue;
@@ -6892,9 +7357,24 @@ function generateSdl(canonical, options = {}) {
6892
7357
  blocks.push(renderQueryRoot(tables, namesByTable));
6893
7358
  const mutationRoot = renderMutationRoot(tables, namesByTable);
6894
7359
  if (mutationRoot !== null) blocks.push(mutationRoot);
7360
+ assertNoDuplicateTypeNames(blocks);
6895
7361
  return `${blocks.join("\n\n")}
6896
7362
  `;
6897
7363
  }
7364
+ var TYPE_DECLARATION_RE = /^(?:type|input|enum|scalar|interface|union)\s+([_A-Za-z][_0-9A-Za-z]*)/;
7365
+ function assertNoDuplicateTypeNames(blocks) {
7366
+ const seen = /* @__PURE__ */ new Set();
7367
+ for (const block of blocks) {
7368
+ const declared = TYPE_DECLARATION_RE.exec(block)?.[1];
7369
+ if (declared === void 0) continue;
7370
+ if (seen.has(declared)) {
7371
+ throw new SdlGenerationError(
7372
+ `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.`
7373
+ );
7374
+ }
7375
+ seen.add(declared);
7376
+ }
7377
+ }
6898
7378
  function assertEnumValue(value, context) {
6899
7379
  if (!GRAPHQL_NAME_RE2.test(value) || RESERVED_ENUM_VALUES.has(value)) {
6900
7380
  throw new SdlGenerationError(
@@ -6983,7 +7463,8 @@ function renderTableBlocks(ctx) {
6983
7463
  renderSelectColumnEnum(ctx),
6984
7464
  renderOrderByInput(ctx),
6985
7465
  renderBoolExp(ctx),
6986
- ...renderAggregateTypes(ctx)
7466
+ ...renderAggregateTypes(ctx),
7467
+ ...renderGroupByTypes(ctx)
6987
7468
  ];
6988
7469
  if (ctx.emitAggregateBoolExp) blocks.push(...renderAggregateBoolExp(ctx));
6989
7470
  const pk = primaryKeyColumns(table);
@@ -7120,6 +7601,32 @@ ${sumLines.join("\n")}
7120
7601
  }
7121
7602
  return blocks;
7122
7603
  }
7604
+ function groupableColumns(table) {
7605
+ return table.columns.filter((column) => column.type !== "jsonb");
7606
+ }
7607
+ function renderGroupByTypes(ctx) {
7608
+ const { table, names } = ctx;
7609
+ const groupable = groupableColumns(table);
7610
+ if (groupable.length === 0) return [];
7611
+ const keyLines = groupable.map((column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`);
7612
+ return [
7613
+ renderEnum(names.groupByColumnEnum, groupable.map((column) => fieldName(column.name))),
7614
+ `input ${names.groupByInput} {
7615
+ column: ${names.groupByColumnEnum}!
7616
+ bucket: ${DATE_BUCKET_ENUM}
7617
+ }`,
7618
+ `type ${names.groupKey} {
7619
+ ${keyLines.join("\n")}
7620
+ }`,
7621
+ `type ${names.aggregateGroup} {
7622
+ key: ${names.groupKey}!
7623
+ aggregate: ${names.aggregateFields}
7624
+ }`
7625
+ ];
7626
+ }
7627
+ function groupRootArgs(names) {
7628
+ return `where: ${names.boolExp}, groupBy: [${names.groupByInput}!]!, limit: Int, offset: Int`;
7629
+ }
7123
7630
  function renderAggregateBoolExp(ctx) {
7124
7631
  const { names } = ctx;
7125
7632
  return [
@@ -7145,6 +7652,9 @@ function constraintValues(table) {
7145
7652
  }
7146
7653
  return [...values].sort();
7147
7654
  }
7655
+ function clientSettableColumns(table) {
7656
+ return table.columns.filter((column) => column.onUpdateFn === void 0);
7657
+ }
7148
7658
  function renderWriteBlocks(ctx, access) {
7149
7659
  const { table, names, pinned } = ctx;
7150
7660
  const blocks = [];
@@ -7166,7 +7676,7 @@ ${insertLines.join("\n")}
7166
7676
  }`);
7167
7677
  }
7168
7678
  if (access.update) {
7169
- const setLines = table.columns.map(
7679
+ const setLines = clientSettableColumns(table).map(
7170
7680
  (column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
7171
7681
  );
7172
7682
  blocks.push(`input ${names.setInput} {
@@ -7193,7 +7703,7 @@ ${incLines.join("\n")}
7193
7703
  if (access.insert && allowsOnConflict(table) && constraints.length > 0) {
7194
7704
  for (const value of constraints) assertEnumValue(value, `constraint enum for table "${table.table}"`);
7195
7705
  blocks.push(renderEnum(names.constraintEnum, constraints));
7196
- const updateColumns = table.columns.map((column) => fieldName(column.name));
7706
+ const updateColumns = clientSettableColumns(table).map((column) => fieldName(column.name));
7197
7707
  blocks.push(renderEnum(names.updateColumnEnum, updateColumns));
7198
7708
  blocks.push(
7199
7709
  `input ${names.onConflict} {
@@ -7225,6 +7735,9 @@ function renderQueryRoot(tables, namesByTable) {
7225
7735
  lines.push(` ${names.byPkRoot}(${args}): ${names.typename}`);
7226
7736
  }
7227
7737
  lines.push(` ${names.aggregateRoot}(${rootArgs(names)}): ${names.aggregate}!`);
7738
+ if (groupableColumns(table).length > 0) {
7739
+ lines.push(` ${names.aggregateGroupsRoot}(${groupRootArgs(names)}): [${names.aggregateGroup}!]!`);
7740
+ }
7228
7741
  }
7229
7742
  lines.push("}");
7230
7743
  return lines.join("\n");
@@ -7293,6 +7806,40 @@ function renderCowMutationFields(table, names) {
7293
7806
  }
7294
7807
  return lines;
7295
7808
  }
7809
+ var ORM_MIGRATION_EXPORT_FILE = "orm.migration.json";
7810
+ var ORM_MIGRATION_EXPORT_MAX_BYTES = ORM_REGISTRATION_MAX_BYTES;
7811
+ var hash = z.string().regex(/^sha256:[0-9a-f]{64}$/);
7812
+ var ormMigrationPayloadSchema = z.object({
7813
+ canonical_definition: canonicalDefinitionSchema,
7814
+ documents: z.array(z.object({ path: z.string().min(1).max(2048), contents: z.string().max(1e6) }).strict()).max(5e3),
7815
+ client_manifest: z.object({ ops_hash: hash, operations: z.record(z.string(), hash) }).strict(),
7816
+ sdl: z.string().refine((value) => Buffer.byteLength(value, "utf8") <= MAX_GENERATED_SDL_BYTES, {
7817
+ message: `sdl exceeds ${MAX_GENERATED_SDL_BYTES} bytes`
7818
+ }),
7819
+ schema_snapshot: z.json(),
7820
+ // The existing ORM committed-migration validator and replay engine remain
7821
+ // authoritative. This envelope hashes the provenance, never authorizes SQL.
7822
+ migration: z.json().optional()
7823
+ }).strict();
7824
+ z.object({
7825
+ schema_version: z.literal(1),
7826
+ schema_hash: hash,
7827
+ payload: ormMigrationPayloadSchema,
7828
+ checksum: z.string().regex(/^[0-9a-f]{64}$/)
7829
+ }).strict();
7830
+ function checksum(value) {
7831
+ return createHash("sha256").update(stableJson(value)).digest("hex");
7832
+ }
7833
+ function createOrmMigrationExport({ schemaHash, payload }) {
7834
+ const contents = { schema_version: 1, schema_hash: hash.parse(schemaHash), payload: ormMigrationPayloadSchema.parse(payload) };
7835
+ return { ...contents, checksum: checksum(contents) };
7836
+ }
7837
+ function renderOrmMigrationExport(exported) {
7838
+ const text4 = `${JSON.stringify(exported, null, 2)}
7839
+ `;
7840
+ if (Buffer.byteLength(text4, "utf8") > ORM_MIGRATION_EXPORT_MAX_BYTES) throw new Error("ORM migration export exceeds the size limit");
7841
+ return text4;
7842
+ }
7296
7843
 
7297
7844
  // src/cli.ts
7298
7845
  var BASE_URL_ENV = "SEQUENCE_ORM_BASE_URL";
@@ -7301,6 +7848,8 @@ var ORM_MODULE_PATH_ENV = "SEQUENCE_ORM_MODULE_PATH";
7301
7848
  var USAGE = `usage:
7302
7849
  seq-studio orm init <dir> scaffold a namespace package (v2: TypeScript schema + GraphQL documents)
7303
7850
  seq-studio orm generate [dir] write schema.graphql + operations.manifest.json + typePolicies.gen.ts (+ consumer graphql-codegen when codegen.ts is present)
7851
+ seq-studio orm generate [dir] --migration-export [--check]
7852
+ export reviewed ORM inputs for a coordinated pipeline migration; --check verifies without writing
7304
7853
  seq-studio orm validate [dir] parse + validate definitions and verify committed migrations are in sync
7305
7854
  seq-studio orm plan [dir] -e <env> offline dry run: compile definitions and diff against the registry (no database, no writes)
7306
7855
  seq-studio orm apply [dir] -e <env> author the migration, register, apply, and refresh the generated outputs (the everyday command)
@@ -7315,6 +7864,8 @@ var USAGE = `usage:
7315
7864
  v2 = sequence.config.ts + schema/*.ts + graphql/**; v1 (legacy) = one <table>.yaml per table.
7316
7865
  Environments: see \`seq-studio envs list\` (discovered after you authenticate).
7317
7866
  Authenticate with: seq-studio login
7867
+ Execute endpoint rollout: SEQUENCE_ORM_ATLAS_EXECUTE_V2=1 opts into /api/data/v2;
7868
+ otherwise execute uses /_data/v2. Server rollout and authorization still apply.
7318
7869
  `;
7319
7870
  var tokenProvider = null;
7320
7871
  function setTokenProvider(provider) {
@@ -7529,6 +8080,12 @@ function printDiagnostics(diagnostics) {
7529
8080
  }
7530
8081
  async function generateCommand(rest) {
7531
8082
  const dir = positional(rest) ?? ".";
8083
+ const migrationExport = rest.includes("--migration-export");
8084
+ const check = rest.includes("--check");
8085
+ if (check && !migrationExport) {
8086
+ console.error("generate --check requires --migration-export");
8087
+ return 1;
8088
+ }
7532
8089
  if (await detectProjectFormat(dir) === "v1") {
7533
8090
  console.error(`generate needs a v2 project (${NAMESPACE_CONFIG_FILE}) \u2014 convert with: seq-studio orm migrate-from-yaml ${dir}`);
7534
8091
  return 1;
@@ -7536,6 +8093,36 @@ async function generateCommand(rest) {
7536
8093
  const compiled = await compileV2Project(dir);
7537
8094
  printDiagnostics(compiled.ok ? compiled.project.diagnostics : compiled.diagnostics);
7538
8095
  if (!compiled.ok) return 1;
8096
+ if (migrationExport) {
8097
+ if (await validateV2Command(dir) !== 0) return 1;
8098
+ const { loaded: loaded2, sdl, documents, manifest: manifest2 } = compiled.project;
8099
+ const workspace = await readMigrationWorkspace(dir, loaded2.canonical.namespace);
8100
+ const migration = await loadCommittedTipMigrationV2({ dir, loaded: loaded2, workspace });
8101
+ const exported = createOrmMigrationExport({ schemaHash: manifest2.schemaHash, payload: {
8102
+ canonical_definition: loaded2.canonical,
8103
+ documents,
8104
+ sdl,
8105
+ client_manifest: { ops_hash: manifest2.opsHash, operations: Object.fromEntries(Object.entries(manifest2.operations).map(([name, operation]) => [name, operation.opHash])) },
8106
+ schema_snapshot: workspace.tipSnapshot,
8107
+ ...migration ? { migration } : {}
8108
+ } });
8109
+ const text4 = renderOrmMigrationExport(exported);
8110
+ const path = join(dir, ORM_MIGRATION_EXPORT_FILE);
8111
+ if (check) {
8112
+ const committed = await readFile(path, "utf8").catch((error) => {
8113
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return void 0;
8114
+ throw error;
8115
+ });
8116
+ if (committed !== text4) {
8117
+ console.error(`${ORM_MIGRATION_EXPORT_FILE} is missing or stale; run seq-studio orm generate ${dir} --migration-export and commit it`);
8118
+ return 1;
8119
+ }
8120
+ console.log(`${ORM_MIGRATION_EXPORT_FILE} matches the current definitions, operations, and committed migration`);
8121
+ return 0;
8122
+ }
8123
+ await writeFile(path, text4);
8124
+ console.log(`wrote ${ORM_MIGRATION_EXPORT_FILE} (${exported.checksum.slice(0, 12)})`);
8125
+ }
7539
8126
  await writeGenerateOutputs(dir, compiled.project);
7540
8127
  const { loaded, manifest } = compiled.project;
7541
8128
  console.log(
@@ -7651,9 +8238,10 @@ async function executeV2Command(rest) {
7651
8238
  );
7652
8239
  }
7653
8240
  const variables = await readExecuteVariables(variablesFile);
8241
+ const executePrefix = process.env["SEQUENCE_ORM_ATLAS_EXECUTE_V2"] === "1" ? "/api/data/v2" : "/_data/v2";
7654
8242
  const response = await apiRequestStrict(
7655
8243
  "POST",
7656
- `/_data/v2/${loaded.canonical.namespace}`,
8244
+ `${executePrefix}/${loaded.canonical.namespace}`,
7657
8245
  { op: selected.hash, variables }
7658
8246
  );
7659
8247
  console.log(JSON.stringify(response, null, 2));
@@ -8168,6 +8756,7 @@ function columnProperty(table, column, singlePrimaryKey) {
8168
8756
  }
8169
8757
  chain += `.default(${JSON.stringify(column.default)})`;
8170
8758
  }
8759
+ if (column.onUpdateFn !== void 0) chain += `.onUpdate(${quote(column.onUpdateFn)})`;
8171
8760
  if (column.description !== void 0) chain += `.description(${quote(column.description)})`;
8172
8761
  if (column.renamedFrom !== void 0) chain += `.renamedFrom(${quote(column.renamedFrom)})`;
8173
8762
  if (column.cast !== void 0) chain += `.cast(${quote(column.cast)})`;