@sequenceholdings/orm 0.1.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +49 -24
  2. package/dist/authoring.cjs +1072 -60
  3. package/dist/authoring.cjs.map +1 -1
  4. package/dist/authoring.d.cts +291 -26
  5. package/dist/authoring.d.ts +291 -26
  6. package/dist/authoring.js +1062 -61
  7. package/dist/authoring.js.map +1 -1
  8. package/dist/canonical.cjs +415 -9
  9. package/dist/canonical.cjs.map +1 -1
  10. package/dist/canonical.d.cts +235 -1
  11. package/dist/canonical.d.ts +235 -1
  12. package/dist/canonical.js +411 -10
  13. package/dist/canonical.js.map +1 -1
  14. package/dist/cli.cjs +1860 -228
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.js +1861 -229
  17. package/dist/cli.js.map +1 -1
  18. package/dist/client.cjs.map +1 -1
  19. package/dist/client.d.cts +1 -1
  20. package/dist/client.d.ts +1 -1
  21. package/dist/client.js.map +1 -1
  22. package/dist/compiler.cjs +512 -54
  23. package/dist/compiler.cjs.map +1 -1
  24. package/dist/compiler.d.cts +39 -231
  25. package/dist/compiler.d.ts +39 -231
  26. package/dist/compiler.js +510 -55
  27. package/dist/compiler.js.map +1 -1
  28. package/dist/drizzle.cjs +14 -0
  29. package/dist/drizzle.cjs.map +1 -0
  30. package/dist/drizzle.d.cts +1 -0
  31. package/dist/drizzle.d.ts +1 -0
  32. package/dist/drizzle.js +3 -0
  33. package/dist/drizzle.js.map +1 -0
  34. package/dist/dynamic-input.cjs +391 -0
  35. package/dist/dynamic-input.cjs.map +1 -0
  36. package/dist/dynamic-input.d.cts +111 -0
  37. package/dist/dynamic-input.d.ts +111 -0
  38. package/dist/dynamic-input.js +380 -0
  39. package/dist/dynamic-input.js.map +1 -0
  40. package/dist/index.cjs +2229 -220
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/index.d.cts +125 -9
  43. package/dist/index.d.ts +125 -9
  44. package/dist/index.js +2205 -221
  45. package/dist/index.js.map +1 -1
  46. package/dist/ir.cjs +249 -14
  47. package/dist/ir.cjs.map +1 -1
  48. package/dist/ir.d.cts +20 -80
  49. package/dist/ir.d.ts +20 -80
  50. package/dist/ir.js +249 -14
  51. package/dist/ir.js.map +1 -1
  52. package/dist/predicate-v2.cjs +1099 -0
  53. package/dist/predicate-v2.cjs.map +1 -0
  54. package/dist/predicate-v2.d.cts +142 -0
  55. package/dist/predicate-v2.d.ts +142 -0
  56. package/dist/predicate-v2.js +1089 -0
  57. package/dist/predicate-v2.js.map +1 -0
  58. package/dist/sdl.cjs +344 -55
  59. package/dist/sdl.cjs.map +1 -1
  60. package/dist/sdl.d.cts +1 -3
  61. package/dist/sdl.d.ts +1 -3
  62. package/dist/sdl.js +344 -55
  63. package/dist/sdl.js.map +1 -1
  64. package/package.json +20 -3
  65. package/src/codegen-gql/preset.cjs +64 -0
  66. package/src/codegen-gql/server-fns.cjs +11 -6
  67. package/templates/orm-namespace/CLAUDE.md +6 -5
  68. package/templates/orm-namespace/codegen.preset.cjs +17 -0
  69. package/templates/orm-namespace/codegen.ts +1 -1
  70. package/templates/orm-namespace/package.json +3 -0
  71. package/templates/orm-namespace/schema/tables.ts +1 -1
  72. package/templates/orm-namespace/sequence.config.ts +1 -1
package/dist/cli.cjs CHANGED
@@ -7,9 +7,9 @@ var url = require('url');
7
7
  var jiti = require('jiti');
8
8
  var crypto = require('crypto');
9
9
  var zod = require('zod');
10
- var graphql = require('graphql');
11
10
  var drizzleOrm = require('drizzle-orm');
12
11
  var pgCore = require('drizzle-orm/pg-core');
12
+ var graphql = require('graphql');
13
13
  var api = require('drizzle-kit/api');
14
14
  var jsYaml = require('js-yaml');
15
15
  var customRolesManifest = require('@sequenceholdings/custom-roles-manifest');
@@ -111,8 +111,190 @@ var policyExpressionSchema = zod.z.union([
111
111
  ]);
112
112
  var canonicalPoliciesSchema = zod.z.object({
113
113
  read: policyExpressionSchema.optional(),
114
- write: policyExpressionSchema.optional()
114
+ write: policyExpressionSchema.optional(),
115
+ insert: policyExpressionSchema.optional(),
116
+ update: policyExpressionSchema.optional()
115
117
  });
118
+ function isSingleViewQuerySql(value) {
119
+ const query = value.trim();
120
+ if (!/^\(*(?:select|with)\b/i.test(query) || query.includes("\0")) return false;
121
+ let state = "normal";
122
+ for (let index3 = 0; index3 < query.length; index3 += 1) {
123
+ const char = query[index3];
124
+ const next = query[index3 + 1];
125
+ if (state === "single") {
126
+ if (char === "'" && next === "'") index3 += 1;
127
+ else if (char === "'") state = "normal";
128
+ continue;
129
+ }
130
+ if (state === "double") {
131
+ if (char === '"' && next === '"') index3 += 1;
132
+ else if (char === '"') state = "normal";
133
+ continue;
134
+ }
135
+ if (char === "'") state = "single";
136
+ else if (char === '"') state = "double";
137
+ else if (char === ";" || char === "-" && next === "-" || char === "/" && next === "*") return false;
138
+ }
139
+ return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
140
+ }
141
+ var VIEW_QUERY_FUNCTION_ALLOWLIST = [
142
+ "abs",
143
+ "age",
144
+ "array_agg",
145
+ "array_append",
146
+ "array_cat",
147
+ "array_dims",
148
+ "array_length",
149
+ "array_lower",
150
+ "array_ndims",
151
+ "array_position",
152
+ "array_positions",
153
+ "array_prepend",
154
+ "array_remove",
155
+ "array_replace",
156
+ "array_to_string",
157
+ "array_trim",
158
+ "array_upper",
159
+ "ascii",
160
+ "avg",
161
+ "bit_and",
162
+ "bit_or",
163
+ "bit_xor",
164
+ "bool_and",
165
+ "bool_or",
166
+ "btrim",
167
+ "cardinality",
168
+ "cbrt",
169
+ "ceil",
170
+ "ceiling",
171
+ "char_length",
172
+ "character_length",
173
+ "chr",
174
+ "concat",
175
+ "concat_ws",
176
+ "count",
177
+ "cume_dist",
178
+ "date_bin",
179
+ "date_part",
180
+ "date_trunc",
181
+ "decode",
182
+ "degrees",
183
+ "dense_rank",
184
+ "encode",
185
+ "enum_first",
186
+ "enum_last",
187
+ "enum_range",
188
+ "every",
189
+ "exp",
190
+ "first_value",
191
+ "floor",
192
+ "format",
193
+ "grouping",
194
+ "initcap",
195
+ "json_agg",
196
+ "json_array_length",
197
+ "json_build_array",
198
+ "json_build_object",
199
+ "json_extract_path",
200
+ "json_extract_path_text",
201
+ "json_object_agg",
202
+ "json_object_keys",
203
+ "json_strip_nulls",
204
+ "json_typeof",
205
+ "jsonb_agg",
206
+ "jsonb_array_length",
207
+ "jsonb_build_array",
208
+ "jsonb_build_object",
209
+ "jsonb_extract_path",
210
+ "jsonb_extract_path_text",
211
+ "jsonb_insert",
212
+ "jsonb_object_agg",
213
+ "jsonb_object_keys",
214
+ "jsonb_path_exists",
215
+ "jsonb_path_match",
216
+ "jsonb_path_query",
217
+ "jsonb_path_query_array",
218
+ "jsonb_path_query_first",
219
+ "jsonb_pretty",
220
+ "jsonb_set",
221
+ "jsonb_set_lax",
222
+ "jsonb_strip_nulls",
223
+ "jsonb_typeof",
224
+ "justify_days",
225
+ "justify_hours",
226
+ "justify_interval",
227
+ "lag",
228
+ "last_value",
229
+ "lead",
230
+ "left",
231
+ "length",
232
+ "ln",
233
+ "log",
234
+ "lower",
235
+ "lpad",
236
+ "ltrim",
237
+ "make_date",
238
+ "make_interval",
239
+ "make_time",
240
+ "make_timestamp",
241
+ "make_timestamptz",
242
+ "max",
243
+ "md5",
244
+ "min",
245
+ "mod",
246
+ "nth_value",
247
+ "ntile",
248
+ "octet_length",
249
+ "percent_rank",
250
+ "pi",
251
+ "power",
252
+ "quote_ident",
253
+ "quote_literal",
254
+ "quote_nullable",
255
+ "radians",
256
+ "rank",
257
+ "regexp_count",
258
+ "regexp_instr",
259
+ "regexp_like",
260
+ "regexp_match",
261
+ "regexp_matches",
262
+ "regexp_replace",
263
+ "regexp_split_to_array",
264
+ "regexp_split_to_table",
265
+ "repeat",
266
+ "replace",
267
+ "reverse",
268
+ "right",
269
+ "round",
270
+ "row_number",
271
+ "rpad",
272
+ "rtrim",
273
+ "sign",
274
+ "split_part",
275
+ "sqrt",
276
+ "starts_with",
277
+ "string_agg",
278
+ "string_to_array",
279
+ "strpos",
280
+ "substr",
281
+ "substring",
282
+ "sum",
283
+ "timezone",
284
+ "to_ascii",
285
+ "to_char",
286
+ "to_date",
287
+ "to_hex",
288
+ "to_json",
289
+ "to_jsonb",
290
+ "to_number",
291
+ "to_timestamp",
292
+ "translate",
293
+ "trunc",
294
+ "unnest",
295
+ "upper",
296
+ "width_bucket"
297
+ ];
116
298
  var canonicalActionSchema = zod.z.object({
117
299
  name: identifier,
118
300
  requires: zod.z.string().max(1e3).optional(),
@@ -124,6 +306,37 @@ var canonicalActionSchema = zod.z.object({
124
306
  returns: zod.z.enum(["rows", "rowsAffected", "scalar"]).default("rowsAffected"),
125
307
  audit: zod.z.object({ action: zod.z.string(), resource: zod.z.string() }).optional()
126
308
  });
309
+ var canonicalCowMutationSchema = zod.z.object({
310
+ kind: zod.z.enum(["override", "revert"]),
311
+ name: identifier,
312
+ /** Synced table (same namespace) the overlay copies from / falls back to. */
313
+ synced: identifier,
314
+ /** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
315
+ pk: zod.z.string().min(1).max(63),
316
+ /** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
317
+ copy: zod.z.array(zod.z.string().min(1).max(63)).default([]),
318
+ /** Override: overlay properties written by the UPDATE (never the pk). Empty for revert. */
319
+ set: zod.z.array(zod.z.string().min(1).max(63)).default([])
320
+ }).superRefine((mutation, ctx) => {
321
+ if (mutation.kind === "override") {
322
+ if (mutation.set.length === 0) {
323
+ ctx.addIssue({ code: "custom", path: ["set"], message: "override mutations must set at least one column" });
324
+ }
325
+ if (!mutation.copy.includes(mutation.pk)) {
326
+ ctx.addIssue({ code: "custom", path: ["copy"], message: "override copy list must include the pk" });
327
+ }
328
+ } else if (mutation.set.length > 0 || mutation.copy.length > 0) {
329
+ ctx.addIssue({ code: "custom", path: ["set"], message: "revert mutations take no copy/set columns" });
330
+ }
331
+ if (mutation.set.includes(mutation.pk)) {
332
+ ctx.addIssue({ code: "custom", path: ["set"], message: "set must not include the pk \u2014 identity columns are pinned" });
333
+ }
334
+ for (const [field, values] of [["copy", mutation.copy], ["set", mutation.set]]) {
335
+ if (new Set(values).size !== values.length) {
336
+ ctx.addIssue({ code: "custom", path: [field], message: `${field} must not contain duplicate columns` });
337
+ }
338
+ }
339
+ });
127
340
  var canonicalTableSchema = zod.z.object({
128
341
  table: identifier,
129
342
  description: zod.z.string().max(2e3).optional(),
@@ -136,7 +349,45 @@ var canonicalTableSchema = zod.z.object({
136
349
  links: zod.z.array(canonicalLinkSchema).default([]),
137
350
  indexes: zod.z.array(canonicalIndexSchema).default([]),
138
351
  policies: canonicalPoliciesSchema.default({}),
139
- actions: zod.z.array(canonicalActionSchema).default([])
352
+ /**
353
+ * v1-conversion carry-over only. v2 has no action surface — the runtime
354
+ * neither compiles nor executes these; writes are mutations. Kept parseable
355
+ * so stored definitions and their content hashes stay stable.
356
+ */
357
+ actions: zod.z.array(canonicalActionSchema).default([]),
358
+ /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
359
+ cowMutations: zod.z.array(canonicalCowMutationSchema).optional()
360
+ });
361
+ var canonicalViewSchema = zod.z.object({
362
+ view: identifier,
363
+ description: zod.z.string().max(2e3).optional(),
364
+ columns: zod.z.array(canonicalColumnSchema).min(1),
365
+ /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
366
+ primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
367
+ links: zod.z.array(canonicalLinkSchema).default([]),
368
+ policies: zod.z.object({ read: policyExpressionSchema }).strict(),
369
+ query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
370
+ message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
371
+ }),
372
+ /** Namespace-local logical relation names, sorted for deterministic hashing. */
373
+ dependencies: zod.z.array(identifier).max(500)
374
+ }).superRefine((view, ctx) => {
375
+ for (const [index3, column] of view.columns.entries()) {
376
+ if (column.default !== void 0 || column.defaultFn !== void 0) {
377
+ ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare a default` });
378
+ }
379
+ if (column.unique) {
380
+ ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare unique` });
381
+ }
382
+ if (column.renamedFrom !== void 0 || column.cast !== void 0) {
383
+ ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare migration hints` });
384
+ }
385
+ }
386
+ for (const [index3, link] of view.links.entries()) {
387
+ if (link.enforce) {
388
+ ctx.addIssue({ code: "custom", path: ["links", index3, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
389
+ }
390
+ }
140
391
  });
141
392
  var canonicalDefinitionSchema = zod.z.object({
142
393
  definition_format: zod.z.literal(CANONICAL_FORMAT),
@@ -144,24 +395,174 @@ var canonicalDefinitionSchema = zod.z.object({
144
395
  description: zod.z.string().max(2e3).optional(),
145
396
  enums: zod.z.array(canonicalEnumSchema).default([]),
146
397
  tables: zod.z.array(canonicalTableSchema).min(0).max(500),
398
+ /** Optional to preserve byte-identical hashes for pre-view canonical v2 documents. */
399
+ views: zod.z.array(canonicalViewSchema).max(500).optional(),
147
400
  /** Verbatim custom-roles grammar (@sequenceholdings/custom-roles-manifest). */
148
401
  capabilities: zod.z.object({
149
402
  roles: zod.z.unknown().optional(),
150
403
  uses: zod.z.array(zod.z.string()).default([])
151
404
  }).default({ uses: [] })
405
+ }).superRefine((definition, ctx) => {
406
+ const seen = /* @__PURE__ */ new Set();
407
+ for (const [index3, table] of definition.tables.entries()) {
408
+ if (seen.has(table.table)) {
409
+ ctx.addIssue({ code: "custom", path: ["tables", index3, "table"], message: `duplicate relation '${table.table}'` });
410
+ }
411
+ seen.add(table.table);
412
+ }
413
+ const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
414
+ for (const [tableIndex, table] of definition.tables.entries()) {
415
+ const cowMutations = table.cowMutations ?? [];
416
+ if (cowMutations.length === 0) continue;
417
+ const path = (index3, ...rest) => ["tables", tableIndex, "cowMutations", index3, ...rest];
418
+ if (table.source !== void 0) {
419
+ ctx.addIssue({ code: "custom", path: path(0), message: `synced table '${table.table}' cannot declare cow mutations \u2014 it is read-only` });
420
+ continue;
421
+ }
422
+ for (const [index3, mutation] of cowMutations.entries()) {
423
+ const synced = tableByName.get(mutation.synced);
424
+ if (!synced || synced.source === void 0) {
425
+ ctx.addIssue({
426
+ code: "custom",
427
+ path: path(index3, "synced"),
428
+ message: `cow mutation '${mutation.name}' references '${mutation.synced}', which is not a synced table in this namespace`
429
+ });
430
+ continue;
431
+ }
432
+ const overlayColumnsByName = new Map(table.columns.map((column) => [column.name, column]));
433
+ const syncedColumnsByName = new Map(synced.columns.map((column) => [column.name, column]));
434
+ let columnsExist = true;
435
+ for (const property of [mutation.pk, ...mutation.copy, ...mutation.set]) {
436
+ if (!overlayColumnsByName.has(property)) {
437
+ columnsExist = false;
438
+ ctx.addIssue({ code: "custom", path: path(index3), message: `cow mutation '${mutation.name}': column '${property}' does not exist on overlay '${table.table}'` });
439
+ }
440
+ }
441
+ for (const property of [mutation.pk, ...mutation.copy]) {
442
+ if (!syncedColumnsByName.has(property)) {
443
+ columnsExist = false;
444
+ ctx.addIssue({ code: "custom", path: path(index3), message: `cow mutation '${mutation.name}': column '${property}' does not exist on synced '${mutation.synced}'` });
445
+ }
446
+ }
447
+ if (!columnsExist) continue;
448
+ const overlayPk = table.primaryKey ?? [];
449
+ const syncedPk = synced.primaryKey ?? [];
450
+ if (overlayPk.length !== 1 || overlayPk[0] !== mutation.pk || syncedPk.length !== 1 || syncedPk[0] !== mutation.pk) {
451
+ ctx.addIssue({
452
+ code: "custom",
453
+ path: path(index3, "pk"),
454
+ message: `cow mutation '${mutation.name}': pk '${mutation.pk}' must be the single-column primary key on both '${table.table}' and '${mutation.synced}'`
455
+ });
456
+ }
457
+ for (const property of [mutation.pk, ...mutation.copy]) {
458
+ const overlayColumn = overlayColumnsByName.get(property);
459
+ const syncedColumn = syncedColumnsByName.get(property);
460
+ if (!overlayColumn || !syncedColumn) continue;
461
+ if (JSON.stringify(overlayColumn.type) !== JSON.stringify(syncedColumn.type)) {
462
+ ctx.addIssue({
463
+ code: "custom",
464
+ path: path(index3),
465
+ message: `cow mutation '${mutation.name}': column '${property}' must have the same type on overlay and synced`
466
+ });
467
+ }
468
+ if (overlayColumn.columnName !== syncedColumn.columnName) {
469
+ ctx.addIssue({
470
+ code: "custom",
471
+ path: path(index3),
472
+ message: `cow mutation '${mutation.name}': column '${property}' maps to different Postgres names on overlay ('${overlayColumn.columnName}') and synced ('${syncedColumn.columnName}')`
473
+ });
474
+ }
475
+ if (syncedColumn.nullable !== false && overlayColumn.nullable === false) {
476
+ ctx.addIssue({
477
+ code: "custom",
478
+ path: path(index3),
479
+ message: `cow mutation '${mutation.name}': column '${property}' is nullable on synced but NOT NULL on overlay \u2014 a NULL synced value would fail the copy`
480
+ });
481
+ }
482
+ }
483
+ }
484
+ }
485
+ for (const [index3, view] of (definition.views ?? []).entries()) {
486
+ if (seen.has(view.view)) {
487
+ ctx.addIssue({ code: "custom", path: ["views", index3, "view"], message: `duplicate relation '${view.view}'` });
488
+ }
489
+ seen.add(view.view);
490
+ }
491
+ const views = definition.views ?? [];
492
+ const viewByName = new Map(views.map((view) => [view.view, view]));
493
+ for (const [viewIndex, view] of views.entries()) {
494
+ for (const [dependencyIndex, dependency] of view.dependencies.entries()) {
495
+ if (!seen.has(dependency)) {
496
+ ctx.addIssue({
497
+ code: "custom",
498
+ path: ["views", viewIndex, "dependencies", dependencyIndex],
499
+ message: `view '${view.view}' depends on unknown relation '${dependency}'`
500
+ });
501
+ }
502
+ }
503
+ }
504
+ const visiting = /* @__PURE__ */ new Set();
505
+ const visited = /* @__PURE__ */ new Set();
506
+ let cycleReported = false;
507
+ const visit = (name, path) => {
508
+ if (visited.has(name) || cycleReported) return;
509
+ if (visiting.has(name)) {
510
+ cycleReported = true;
511
+ ctx.addIssue({
512
+ code: "custom",
513
+ path: ["views"],
514
+ message: `view dependencies form a cycle: ${[...path, name].join(" -> ")}`
515
+ });
516
+ return;
517
+ }
518
+ visiting.add(name);
519
+ const view = viewByName.get(name);
520
+ for (const dependency of view?.dependencies ?? []) {
521
+ if (viewByName.has(dependency)) visit(dependency, [...path, name]);
522
+ }
523
+ visiting.delete(name);
524
+ visited.add(name);
525
+ };
526
+ for (const view of views) visit(view.view, []);
152
527
  });
528
+ function canonicalRelations(definition) {
529
+ const views = (definition.views ?? []).map((view) => ({
530
+ table: view.view,
531
+ ...view.description !== void 0 ? { description: view.description } : {},
532
+ columns: view.columns,
533
+ ...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
534
+ links: view.links,
535
+ indexes: [],
536
+ policies: { read: view.policies.read },
537
+ actions: []
538
+ }));
539
+ return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
540
+ }
153
541
  function normalizeCanonical(definition) {
154
542
  const sortByName = (items) => [...items].sort((a, b) => a.name.localeCompare(b.name));
155
- return {
543
+ const normalized = {
156
544
  ...definition,
157
545
  enums: sortByName(definition.enums),
158
- tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => ({
159
- ...t2,
160
- links: sortByName(t2.links),
161
- indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
162
- actions: sortByName(t2.actions)
163
- }))
546
+ tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
547
+ const { cowMutations, ...rest } = t2;
548
+ return {
549
+ ...rest,
550
+ links: sortByName(t2.links),
551
+ indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
552
+ actions: sortByName(t2.actions),
553
+ // Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
554
+ ...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
555
+ };
556
+ })
164
557
  };
558
+ if (definition.views !== void 0) {
559
+ normalized.views = [...definition.views].sort((a, b) => a.view.localeCompare(b.view)).map((view) => ({
560
+ ...view,
561
+ links: sortByName(view.links),
562
+ dependencies: [...view.dependencies].sort()
563
+ }));
564
+ }
565
+ return normalized;
165
566
  }
166
567
 
167
568
  // src/constants.ts
@@ -202,9 +603,13 @@ function canonicalTable(table) {
202
603
  source: table.source ?? null,
203
604
  links: emptyToNull(table.links),
204
605
  indexes: emptyToNull(table.indexes),
606
+ // insert/update are omitted rather than nulled when absent, so definitions
607
+ // predating split policies keep the hash they were registered under.
205
608
  policies: {
206
609
  read: table.policies?.read?.source ?? null,
207
- write: table.policies?.write?.source ?? null
610
+ write: table.policies?.write?.source ?? null,
611
+ ...table.policies?.insert !== void 0 ? { insert: table.policies.insert.source } : {},
612
+ ...table.policies?.update !== void 0 ? { update: table.policies.update.source } : {}
208
613
  },
209
614
  actions: emptyToNull(
210
615
  Object.fromEntries(
@@ -276,7 +681,7 @@ function contextStack() {
276
681
  return created;
277
682
  }
278
683
  function beginAuthoringContext() {
279
- const context = { currentFile: void 0, tables: [], enums: [], configs: [] };
684
+ const context = { currentFile: void 0, tables: [], views: [], enums: [], configs: [] };
280
685
  contextStack().push(context);
281
686
  return context;
282
687
  }
@@ -294,6 +699,155 @@ var TABLE_BRAND = "seq-table";
294
699
  function isSeqTable(value) {
295
700
  return typeof value === "object" && value !== null && value.kind === TABLE_BRAND;
296
701
  }
702
+ var VIEW_BRAND = "seq-view";
703
+ function isSeqView(value) {
704
+ return typeof value === "object" && value !== null && value.kind === VIEW_BRAND;
705
+ }
706
+ var QUERY_COLUMN_FACTORIES = {
707
+ uuid: (name) => pgCore.uuid(name),
708
+ text: (name) => pgCore.text(name),
709
+ integer: (name) => pgCore.integer(name),
710
+ bigint: (name) => pgCore.bigint(name, { mode: "bigint" }),
711
+ numeric: (name) => pgCore.numeric(name),
712
+ double: (name) => pgCore.doublePrecision(name),
713
+ boolean: (name) => pgCore.boolean(name),
714
+ timestamptz: (name) => pgCore.timestamp(name, { withTimezone: true }),
715
+ date: (name) => pgCore.date(name),
716
+ jsonb: (name) => pgCore.jsonb(name)
717
+ };
718
+ function compileViewQueries(input) {
719
+ const schemaName = `orm_${input.namespace}`;
720
+ const schema = pgCore.pgSchema(schemaName);
721
+ const enumFactories = /* @__PURE__ */ new Map();
722
+ for (const entry of input.enums) {
723
+ const values = entry.values;
724
+ enumFactories.set(entry.name, schema.enum(entry.name, values));
725
+ }
726
+ const handles = /* @__PURE__ */ new Map();
727
+ const physicalToLogical = /* @__PURE__ */ new Map();
728
+ const tableByName = new Map(input.tables.map((table) => [table.table, table]));
729
+ for (const authored of input.authoredTables) {
730
+ const canonical = tableByName.get(authored.tableName);
731
+ if (!canonical) continue;
732
+ const physicalSchema = schemaName;
733
+ const physicalName = canonical.table;
734
+ const relationSchema = pgCore.pgSchema(physicalSchema);
735
+ const columns = buildQueryColumns(canonical.columns, enumFactories);
736
+ const handle = relationSchema.table(physicalName, columns);
737
+ handles.set(authored, handle);
738
+ physicalToLogical.set(`${physicalSchema}.${physicalName}`, canonical.table);
739
+ }
740
+ const viewByName = new Map(input.views.map((view) => [view.view, view]));
741
+ for (const authored of input.authoredViews) {
742
+ const canonical = viewByName.get(authored.viewName);
743
+ if (!canonical) continue;
744
+ const columns = buildQueryColumns(canonical.columns, enumFactories);
745
+ const handle = schema.view(canonical.view, columns).existing();
746
+ handles.set(authored, handle);
747
+ physicalToLogical.set(`${schemaName}.${canonical.view}`, canonical.view);
748
+ }
749
+ const dialect = new pgCore.PgDialect();
750
+ const compiled = /* @__PURE__ */ new Map();
751
+ for (const authored of input.authoredViews) {
752
+ const expectedFields = Object.keys(authored.columns);
753
+ const declaredDependencies = /* @__PURE__ */ new Set();
754
+ const relation = (ref) => {
755
+ let resolved;
756
+ try {
757
+ resolved = typeof ref === "function" ? ref() : ref;
758
+ } catch (error) {
759
+ throw new OrmDefinitionError(
760
+ `view "${authored.viewName}" relation thunk threw: ${error instanceof Error ? error.message : String(error)}`,
761
+ authored.file
762
+ );
763
+ }
764
+ if (!isSeqTable(resolved) && !isSeqView(resolved)) {
765
+ throw new OrmDefinitionError(
766
+ `view "${authored.viewName}" relation() expects a table or view defined by this ORM`,
767
+ authored.file
768
+ );
769
+ }
770
+ const handle = handles.get(resolved);
771
+ if (!handle) {
772
+ const name = isSeqTable(resolved) ? resolved.tableName : resolved.viewName;
773
+ throw new OrmDefinitionError(
774
+ `view "${authored.viewName}" references relation "${name}" which is not part of this namespace`,
775
+ authored.file
776
+ );
777
+ }
778
+ declaredDependencies.add(isSeqTable(resolved) ? resolved.tableName : resolved.viewName);
779
+ return handle;
780
+ };
781
+ let queryBuilder;
782
+ try {
783
+ queryBuilder = authored.config.query({ qb: new pgCore.QueryBuilder(), relation });
784
+ } catch (error) {
785
+ if (error instanceof OrmDefinitionError) throw error;
786
+ throw new OrmDefinitionError(
787
+ `view "${authored.viewName}" query callback failed: ${error instanceof Error ? error.message : String(error)}`,
788
+ authored.file
789
+ );
790
+ }
791
+ let config;
792
+ try {
793
+ config = pgCore.getViewConfig(schema.view(authored.viewName).as(queryBuilder));
794
+ } catch (error) {
795
+ throw new OrmDefinitionError(
796
+ `view "${authored.viewName}" query must return a Drizzle select builder: ${error instanceof Error ? error.message : String(error)}`,
797
+ authored.file
798
+ );
799
+ }
800
+ const selectedFields = Object.keys(config.selectedFields);
801
+ if (selectedFields.length !== expectedFields.length || selectedFields.some((field, index3) => field !== expectedFields[index3])) {
802
+ throw new OrmDefinitionError(
803
+ `view "${authored.viewName}" query selects [${selectedFields.join(", ")}], but declared columns are [${expectedFields.join(", ")}]`,
804
+ authored.file
805
+ );
806
+ }
807
+ if (!config.query) throw new OrmDefinitionError(`view "${authored.viewName}" produced no query`, authored.file);
808
+ const tableDependencies = pgCore.extractUsedTable(config.query).map((physical) => {
809
+ const logical = physicalToLogical.get(physical);
810
+ if (!logical) {
811
+ throw new OrmDefinitionError(
812
+ `view "${authored.viewName}" query references unmanaged relation "${physical}" \u2014 use relation(...) with a namespace table or view`,
813
+ authored.file
814
+ );
815
+ }
816
+ return logical;
817
+ });
818
+ const renderedQuery = dialect.sqlToQuery(config.query.inlineParams());
819
+ if (renderedQuery.params.length > 0) {
820
+ throw new OrmDefinitionError(
821
+ `view "${authored.viewName}" query produced bind parameters which cannot be persisted in view DDL`,
822
+ authored.file
823
+ );
824
+ }
825
+ compiled.set(authored.viewName, {
826
+ query: renderedQuery.sql,
827
+ dependencies: [.../* @__PURE__ */ new Set([...declaredDependencies, ...tableDependencies])].sort()
828
+ });
829
+ }
830
+ return compiled;
831
+ }
832
+ function buildQueryColumns(columns, enumFactories) {
833
+ return Object.fromEntries(columns.map((column) => [column.name, buildQueryColumn(column, enumFactories)]));
834
+ }
835
+ function buildQueryColumn(column, enumFactories) {
836
+ const factory = typeof column.type === "string" ? QUERY_COLUMN_FACTORIES[column.type] : enumFactories.get(column.type.enum);
837
+ if (!factory) {
838
+ const type = typeof column.type === "string" ? column.type : column.type.enum;
839
+ throw new OrmDefinitionError(`view query column '${column.columnName}' references unknown type '${type}'`);
840
+ }
841
+ let builder = factory(column.columnName);
842
+ if (!column.nullable) builder = builder.notNull();
843
+ return builder;
844
+ }
845
+ function relationName(relation) {
846
+ return isSeqTable(relation) ? relation.tableName : relation.viewName;
847
+ }
848
+ function relationKind(relation) {
849
+ return isSeqTable(relation) ? "table" : "view";
850
+ }
297
851
 
298
852
  // src/authoring/namespace.ts
299
853
  var NAMESPACE_CONFIG_BRAND = "seq-namespace-config";
@@ -302,6 +856,7 @@ function isSeqNamespaceConfig(value) {
302
856
  }
303
857
  function serializeNamespace(input) {
304
858
  const { config, tables } = input;
859
+ const views = input.views ?? [];
305
860
  const enums = input.enums ?? [];
306
861
  const tablesByName = /* @__PURE__ */ new Map();
307
862
  for (const table of tables) {
@@ -314,6 +869,29 @@ function serializeNamespace(input) {
314
869
  }
315
870
  tablesByName.set(table.tableName, table);
316
871
  }
872
+ const viewsByName = /* @__PURE__ */ new Map();
873
+ for (const view of views) {
874
+ const existing = viewsByName.get(view.viewName);
875
+ if (existing) {
876
+ throw new OrmDefinitionError(
877
+ `duplicate view "${view.viewName}" (defined in ${describeFile(existing)} and ${describeFile(view)})`,
878
+ view.file
879
+ );
880
+ }
881
+ viewsByName.set(view.viewName, view);
882
+ }
883
+ const relationsByName = /* @__PURE__ */ new Map();
884
+ for (const relation of [...tables, ...views]) {
885
+ const name = relationName(relation);
886
+ const existing = relationsByName.get(name);
887
+ if (existing) {
888
+ throw new OrmDefinitionError(
889
+ `duplicate relation "${name}" (defined as ${relationKind(existing)} in ${describeFile(existing)} and ${relationKind(relation)} in ${describeFile(relation)})`,
890
+ relation.file
891
+ );
892
+ }
893
+ relationsByName.set(name, relation);
894
+ }
317
895
  const enumsByName = /* @__PURE__ */ new Map();
318
896
  for (const ref of enums) {
319
897
  if (enumsByName.has(ref.name)) {
@@ -321,11 +899,26 @@ function serializeNamespace(input) {
321
899
  }
322
900
  enumsByName.set(ref.name, ref);
323
901
  }
324
- const canonicalTables = tables.map((table) => buildCanonicalTable({ table, tablesByName, enumsByName }));
902
+ const canonicalTables = tables.map((table) => buildCanonicalTable({ table, relationsByName, enumsByName }));
325
903
  const canonicalEnums = [...enumsByName.values()].map((ref) => ({
326
904
  name: ref.name,
327
905
  values: [...ref.values]
328
906
  }));
907
+ const viewDrafts = views.map((view) => buildCanonicalView({ view, relationsByName, enumsByName }));
908
+ const compiledViews = compileViewQueries({
909
+ namespace: config.namespace,
910
+ tables: canonicalTables,
911
+ views: viewDrafts,
912
+ enums: canonicalEnums,
913
+ authoredTables: tables,
914
+ authoredViews: views
915
+ });
916
+ const canonicalViews = viewDrafts.map((draft) => {
917
+ const compiled = compiledViews.get(draft.view);
918
+ if (!compiled) throw new OrmDefinitionError(`view "${draft.view}" did not compile`, viewsByName.get(draft.view)?.file);
919
+ return canonicalViewSchema.parse({ ...draft, ...compiled });
920
+ });
921
+ assertAcyclicViewDependencies(canonicalViews);
329
922
  const candidate = {
330
923
  definition_format: CANONICAL_FORMAT,
331
924
  namespace: config.namespace,
@@ -337,6 +930,7 @@ function serializeNamespace(input) {
337
930
  }
338
931
  };
339
932
  if (config.description !== void 0) candidate.description = config.description;
933
+ if (canonicalViews.length > 0) candidate.views = canonicalViews;
340
934
  let parsed;
341
935
  try {
342
936
  parsed = canonicalDefinitionSchema.parse(candidate);
@@ -351,7 +945,7 @@ function serializeNamespace(input) {
351
945
  function computeCanonicalHash(definition) {
352
946
  return crypto.createHash("sha256").update(HASH_DOMAIN).update(stableJson(definition)).digest("hex");
353
947
  }
354
- function buildCanonicalTable({ table, tablesByName, enumsByName }) {
948
+ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
355
949
  const candidate = {
356
950
  table: table.tableName,
357
951
  columns: [],
@@ -363,6 +957,31 @@ function buildCanonicalTable({ table, tablesByName, enumsByName }) {
363
957
  if (table.config.description !== void 0) candidate.description = table.config.description;
364
958
  if (table.config.renamedFrom !== void 0) candidate.renamedFrom = table.config.renamedFrom;
365
959
  if (table.source !== void 0) candidate.source = table.source;
960
+ if (table.source !== void 0) {
961
+ if (table.config.indexes !== void 0 && table.config.indexes.length > 0) {
962
+ throw new OrmDefinitionError(
963
+ `synced table "${table.tableName}" is system-owned; indexes are not allowed`,
964
+ table.file
965
+ );
966
+ }
967
+ if (table.config.cowMutations !== void 0 && table.config.cowMutations.length > 0) {
968
+ throw new OrmDefinitionError(
969
+ `synced table "${table.tableName}" is read-only; cow mutations belong on the overlay table`,
970
+ table.file
971
+ );
972
+ }
973
+ if (table.config.policies?.write !== void 0 || table.config.policies?.insert !== void 0 || table.config.policies?.update !== void 0) {
974
+ throw new OrmDefinitionError(`synced table "${table.tableName}" is read-only`, table.file);
975
+ }
976
+ for (const [linkName, link] of Object.entries(table.config.links ?? {})) {
977
+ if (link.enforce) {
978
+ throw new OrmDefinitionError(
979
+ `synced table "${table.tableName}": link "${linkName}" cannot enforce foreign keys \u2014 its DDL is owned by the sync`,
980
+ table.file
981
+ );
982
+ }
983
+ }
984
+ }
366
985
  const columnLevelPk = [];
367
986
  for (const [propertyName, builder] of Object.entries(table.columns)) {
368
987
  const column = builder.toCanonicalColumn(propertyName);
@@ -384,40 +1003,38 @@ function buildCanonicalTable({ table, tablesByName, enumsByName }) {
384
1003
  );
385
1004
  }
386
1005
  candidate.primaryKey = table.config.primaryKey.map(
387
- (ref) => resolveColumnRef({ table, ref, role: "primaryKey" })
1006
+ (ref) => resolveColumnRef({ relation: table, ref, role: "primaryKey" })
388
1007
  );
389
1008
  } else if (columnLevelPk.length > 0) {
390
1009
  candidate.primaryKey = columnLevelPk;
391
1010
  }
392
1011
  for (const index3 of table.config.indexes ?? []) {
393
1012
  const entry = {
394
- columns: index3.columns.map((ref) => resolveColumnRef({ table, ref, role: "index" }))
1013
+ columns: index3.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
395
1014
  };
396
1015
  if (index3.unique !== void 0) entry.unique = index3.unique;
397
1016
  if (index3.name !== void 0) entry.name = index3.name;
398
1017
  candidate.indexes?.push(entry);
399
1018
  }
400
1019
  for (const [linkName, link] of Object.entries(table.config.links ?? {})) {
401
- candidate.links?.push(buildCanonicalLink({ table, linkName, link, tablesByName }));
1020
+ candidate.links?.push(buildCanonicalLink({ relation: table, linkName, link, relationsByName }));
402
1021
  }
403
1022
  const policies = {};
404
1023
  if (table.config.policies?.read !== void 0) policies.read = toCanonicalPolicy(table.config.policies.read);
405
1024
  if (table.config.policies?.write !== void 0) policies.write = toCanonicalPolicy(table.config.policies.write);
1025
+ if (table.config.policies?.insert !== void 0) policies.insert = toCanonicalPolicy(table.config.policies.insert);
1026
+ if (table.config.policies?.update !== void 0) policies.update = toCanonicalPolicy(table.config.policies.update);
406
1027
  candidate.policies = policies;
407
- for (const action of table.config.actions ?? []) {
408
- candidate.actions?.push({
409
- name: action.name,
410
- ...action.requires !== void 0 ? { requires: action.requires } : {},
411
- params: Object.fromEntries(
412
- Object.entries(action.params ?? {}).map(([name, param]) => [
413
- name,
414
- { type: param.type, nullable: param.nullable ?? false }
415
- ])
416
- ),
417
- executes: action.executes,
418
- returns: action.returns ?? "rowsAffected",
419
- ...action.audit !== void 0 ? { audit: action.audit } : {}
420
- });
1028
+ const cowMutations = table.config.cowMutations ?? [];
1029
+ if (cowMutations.length > 0) {
1030
+ candidate.cowMutations = cowMutations.map((mutation) => ({
1031
+ kind: mutation.kind,
1032
+ name: mutation.name,
1033
+ synced: mutation.synced,
1034
+ pk: mutation.pk,
1035
+ copy: [...mutation.copy],
1036
+ set: [...mutation.set]
1037
+ }));
421
1038
  }
422
1039
  try {
423
1040
  return canonicalTableSchema.parse(candidate);
@@ -428,14 +1045,71 @@ function buildCanonicalTable({ table, tablesByName, enumsByName }) {
428
1045
  throw error;
429
1046
  }
430
1047
  }
1048
+ function buildCanonicalView({
1049
+ view,
1050
+ relationsByName,
1051
+ enumsByName
1052
+ }) {
1053
+ if (view.config.policies?.read === void 0) {
1054
+ throw new OrmDefinitionError(`view "${view.viewName}" must declare policies.read`, view.file);
1055
+ }
1056
+ const columns = [];
1057
+ const columnLevelPk = [];
1058
+ for (const [propertyName, builder] of Object.entries(view.columns)) {
1059
+ const column = builder.toCanonicalColumn(propertyName);
1060
+ if (column.default !== void 0 || column.defaultFn !== void 0) {
1061
+ throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare a default`, view.file);
1062
+ }
1063
+ if (column.unique) {
1064
+ throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare unique`, view.file);
1065
+ }
1066
+ if (column.renamedFrom !== void 0 || column.cast !== void 0) {
1067
+ throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare migration hints`, view.file);
1068
+ }
1069
+ const enumName = builder.enumName;
1070
+ if (enumName !== void 0 && !enumsByName.has(enumName)) {
1071
+ throw new OrmDefinitionError(
1072
+ `view "${view.viewName}": column "${propertyName}" references enum "${enumName}" which is not part of this namespace`,
1073
+ view.file
1074
+ );
1075
+ }
1076
+ if (builder.isPrimaryKey) columnLevelPk.push(column.columnName);
1077
+ columns.push(column);
1078
+ }
1079
+ let primaryKey2;
1080
+ if (view.config.primaryKey !== void 0) {
1081
+ if (columnLevelPk.length > 0) {
1082
+ throw new OrmDefinitionError(
1083
+ `view "${view.viewName}": declare the logical primary key either on columns or with config.primaryKey \u2014 not both`,
1084
+ view.file
1085
+ );
1086
+ }
1087
+ primaryKey2 = view.config.primaryKey.map((ref) => resolveColumnRef({ relation: view, ref, role: "primaryKey" }));
1088
+ } else if (columnLevelPk.length > 0) {
1089
+ primaryKey2 = columnLevelPk;
1090
+ }
1091
+ const links = Object.entries(view.config.links ?? {}).map(
1092
+ ([linkName, link]) => buildCanonicalLink({ relation: view, linkName, link, relationsByName })
1093
+ );
1094
+ return {
1095
+ view: view.viewName,
1096
+ ...view.config.description !== void 0 ? { description: view.config.description } : {},
1097
+ columns,
1098
+ ...primaryKey2 !== void 0 ? { primaryKey: primaryKey2 } : {},
1099
+ links,
1100
+ policies: { read: toCanonicalPolicy(view.config.policies.read) }
1101
+ };
1102
+ }
431
1103
  function buildCanonicalLink({
432
- table,
1104
+ relation,
433
1105
  linkName,
434
1106
  link,
435
- tablesByName
1107
+ relationsByName
436
1108
  }) {
1109
+ const ownerKind = relationKind(relation);
1110
+ const ownerName = relationName(relation);
437
1111
  const fail2 = (message) => {
438
- throw new OrmDefinitionError(`table "${table.tableName}": link "${linkName}" ${message}`, table.file);
1112
+ throw new OrmDefinitionError(`${ownerKind} "${ownerName}": link "${linkName}" ${message}`, relation.file);
439
1113
  };
440
1114
  if (link.enforce && link.linkKind !== "toOne") {
441
1115
  return fail2("sets enforce: true, which is only supported on toOne links");
@@ -445,29 +1119,34 @@ function buildCanonicalLink({
445
1119
  return {
446
1120
  name: linkName,
447
1121
  kind: "toPlatformUser",
448
- on: resolveColumnRef({ table, ref: link.on, role: `link "${linkName}" on` }),
1122
+ on: resolveColumnRef({ relation, ref: link.on, role: `link "${linkName}" on` }),
449
1123
  ...link.references !== void 0 ? { references: deriveColumnName(link.references) } : {},
450
1124
  enforce: false,
451
1125
  ...link.description !== void 0 ? { description: link.description } : {}
452
1126
  };
453
1127
  }
454
- const target = resolveTableRef({ ref: link.target, fail: fail2 });
455
- if (!tablesByName.has(target.tableName)) {
456
- return fail2(`targets table "${target.tableName}" which is not part of this namespace`);
1128
+ const target = resolveRelationRef({ ref: link.target, fail: fail2 });
1129
+ const targetName = relationName(target);
1130
+ if (!relationsByName.has(targetName)) {
1131
+ return fail2(`targets ${relationKind(target)} "${targetName}" which is not part of this namespace`);
1132
+ }
1133
+ if (link.enforce && (!isSeqTable(relation) || !isSeqTable(target))) {
1134
+ return fail2("cannot enforce a PostgreSQL foreign key involving a view");
457
1135
  }
458
- const owningSide = link.linkKind === "toOne" ? table : target;
1136
+ const owningSide = link.linkKind === "toOne" ? relation : target;
1137
+ const referencedSide = link.linkKind === "toOne" ? target : relation;
459
1138
  return {
460
1139
  name: linkName,
461
1140
  kind: link.linkKind,
462
- target: target.tableName,
463
- on: resolveColumnRef({ table: owningSide, ref: link.on, role: `link "${linkName}" on` }),
464
- ...link.references !== void 0 ? { references: resolveColumnRef({ table: target, ref: link.references, role: `link "${linkName}" references` }) } : {},
1141
+ target: targetName,
1142
+ on: resolveColumnRef({ relation: owningSide, ref: link.on, role: `link "${linkName}" on` }),
1143
+ ...link.references !== void 0 ? { references: resolveColumnRef({ relation: referencedSide, ref: link.references, role: `link "${linkName}" references` }) } : {},
465
1144
  enforce: link.enforce,
466
1145
  ...link.description !== void 0 ? { description: link.description } : {}
467
1146
  };
468
1147
  }
469
- function resolveTableRef({ ref, fail: fail2 }) {
470
- if (ref === void 0) return fail2("declares no target table");
1148
+ function resolveRelationRef({ ref, fail: fail2 }) {
1149
+ if (ref === void 0) return fail2("declares no target relation");
471
1150
  if (typeof ref !== "function") return ref;
472
1151
  let resolved;
473
1152
  try {
@@ -477,21 +1156,36 @@ function resolveTableRef({ ref, fail: fail2 }) {
477
1156
  `target thunk threw (${error instanceof Error ? error.message : String(error)}) \u2014 lazy refs resolve at serialize time; make sure the target module ran`
478
1157
  );
479
1158
  }
480
- if (!isSeqTable(resolved)) return fail2("target thunk did not return a table defined with defineTable");
1159
+ if (!isSeqTable(resolved) && !isSeqView(resolved)) {
1160
+ return fail2("target thunk did not return a relation defined with defineTable/defineView");
1161
+ }
481
1162
  return resolved;
482
1163
  }
483
- function resolveColumnRef({ table, ref, role }) {
484
- const direct = table.columns[ref];
1164
+ function resolveColumnRef({ relation, ref, role }) {
1165
+ const direct = relation.columns[ref];
485
1166
  if (direct) return direct.columnNameFor(ref);
486
- for (const [propertyName, builder] of Object.entries(table.columns)) {
1167
+ for (const [propertyName, builder] of Object.entries(relation.columns)) {
487
1168
  if (builder.columnNameFor(propertyName) === ref) return ref;
488
1169
  }
489
- const known = Object.keys(table.columns).join(", ");
1170
+ const known = Object.keys(relation.columns).join(", ");
490
1171
  throw new OrmDefinitionError(
491
- `table "${table.tableName}": ${role} references unknown column "${ref}" (known columns: ${known || "none"})`,
492
- table.file
1172
+ `${relationKind(relation)} "${relationName(relation)}": ${role} references unknown column "${ref}" (known columns: ${known || "none"})`,
1173
+ relation.file
493
1174
  );
494
1175
  }
1176
+ function assertAcyclicViewDependencies(views) {
1177
+ const viewNames = new Set(views.map((view) => view.view));
1178
+ const remaining = new Map(
1179
+ views.map((view) => [view.view, new Set(view.dependencies.filter((dependency) => viewNames.has(dependency)))])
1180
+ );
1181
+ while (remaining.size > 0) {
1182
+ const ready = [...remaining.entries()].filter(([, dependencies]) => [...dependencies].every((dependency) => !remaining.has(dependency))).map(([name]) => name);
1183
+ if (ready.length === 0) {
1184
+ throw new OrmDefinitionError(`view dependencies form a cycle among: ${[...remaining.keys()].sort().join(", ")}`);
1185
+ }
1186
+ for (const name of ready) remaining.delete(name);
1187
+ }
1188
+ }
495
1189
  function toCanonicalPolicy(policy) {
496
1190
  if (typeof policy === "string") return policy;
497
1191
  return {
@@ -503,8 +1197,8 @@ function toCanonicalPolicy(policy) {
503
1197
  }))
504
1198
  };
505
1199
  }
506
- function describeFile(table) {
507
- return table.file ?? "<unknown file>";
1200
+ function describeFile(relation) {
1201
+ return relation.file ?? "<unknown file>";
508
1202
  }
509
1203
  function formatZodIssues(error) {
510
1204
  return error.issues.map((issue) => issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message).join("; ");
@@ -528,7 +1222,12 @@ async function loadNamespaceFromDir(dir, options = {}) {
528
1222
  const jiti$1 = jiti.createJiti((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href)), {
529
1223
  interopDefault: true,
530
1224
  fsCache: false,
531
- ...options.ormModulePath !== void 0 ? { alias: { "@sequenceholdings/orm": options.ormModulePath } } : {}
1225
+ ...options.ormModulePath !== void 0 ? {
1226
+ alias: {
1227
+ "@sequenceholdings/orm": options.ormModulePath,
1228
+ "@sequenceholdings/orm/authoring": options.ormModulePath
1229
+ }
1230
+ } : {}
532
1231
  });
533
1232
  const context = beginAuthoringContext();
534
1233
  try {
@@ -569,16 +1268,20 @@ async function loadNamespaceFromDir(dir, options = {}) {
569
1268
  for (const entry of context.tables) {
570
1269
  if (isSeqTable(entry.value)) tables.push(entry.value);
571
1270
  }
572
- if (tables.length === 0) {
1271
+ const views = [];
1272
+ for (const entry of context.views) {
1273
+ if (isSeqView(entry.value)) views.push(entry.value);
1274
+ }
1275
+ if (tables.length === 0 && views.length === 0) {
573
1276
  throw new OrmDefinitionError(
574
- `no tables registered \u2014 ${SCHEMA_DIR}/*.ts modules must call defineTable(...) at import time`
1277
+ `no relations registered \u2014 ${SCHEMA_DIR}/*.ts modules must call defineTable(...) or defineView(...) at import time`
575
1278
  );
576
1279
  }
577
1280
  const enums = [];
578
1281
  for (const entry of context.enums) {
579
1282
  if (isEnumRef(entry.value)) enums.push(entry.value);
580
1283
  }
581
- const canonical = serializeNamespace({ config: configEntry.value, tables, enums });
1284
+ const canonical = serializeNamespace({ config: configEntry.value, tables, views, enums });
582
1285
  return { canonical, hash: computeCanonicalHash(canonical), files };
583
1286
  }
584
1287
  async function listSchemaFiles(root) {
@@ -612,10 +1315,10 @@ var manifestRequiresSchema = zod.z.object({
612
1315
  }).nullable();
613
1316
  var manifestOperationSchema = zod.z.object({
614
1317
  hash: zod.z.string().regex(OP_HASH_RE),
615
- kind: zod.z.enum(["query", "mutation", "action"]),
1318
+ kind: zod.z.enum(["query", "mutation"]),
616
1319
  requires: manifestRequiresSchema
617
1320
  });
618
- zod.z.object({
1321
+ var operationsManifestSchema = zod.z.object({
619
1322
  manifestVersion: zod.z.literal(MANIFEST_VERSION),
620
1323
  schemaHash: zod.z.string().min(1),
621
1324
  opsHash: zod.z.string().regex(OP_HASH_RE),
@@ -775,14 +1478,14 @@ function generateTypePolicies(canonical) {
775
1478
  const def = normalizeCanonical(canonicalDefinitionSchema.parse(canonical));
776
1479
  const entries = [];
777
1480
  let hasPlatformUser = false;
778
- for (const table of def.tables) {
1481
+ for (const table of canonicalRelations(def)) {
779
1482
  if (table.source !== void 0) continue;
780
1483
  if (table.links.some((link) => link.kind === "toPlatformUser")) hasPlatformUser = true;
781
1484
  if (!table.primaryKey || table.primaryKey.length === 0) continue;
782
1485
  const keyFields = table.primaryKey.map((identifier2) => {
783
1486
  const column = table.columns.find((c) => c.columnName === identifier2 || c.name === identifier2);
784
1487
  if (!column) {
785
- throw new CodegenGqlError(`Primary key column "${identifier2}" not found on table "${table.table}".`);
1488
+ throw new CodegenGqlError(`Primary key column "${identifier2}" not found on relation "${table.table}".`);
786
1489
  }
787
1490
  return fieldName(column.name);
788
1491
  });
@@ -936,9 +1639,23 @@ var onConflictIrSchema = zod.z.union([
936
1639
  ]);
937
1640
  var mutationRootSchema = zod.z.object({
938
1641
  kind: zod.z.literal("mutation"),
939
- op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk"]),
1642
+ /**
1643
+ * CRUD verbs plus the structured copy-on-write ops. `cowOverride` copies the
1644
+ * synced row into the overlay (INSERT…SELECT, ON CONFLICT DO NOTHING) then
1645
+ * applies `set` — both statements generated by the executor from this
1646
+ * metadata, never authored SQL. `cowRevert` deletes the override so the
1647
+ * coalesce view falls back to synced. Both are gated on a matching synced
1648
+ * row and carry the write policy in `where` like update/delete.
1649
+ */
1650
+ op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
940
1651
  alias: zod.z.string(),
941
1652
  table: zod.z.string(),
1653
+ /** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
1654
+ cowSynced: zod.z.string().optional(),
1655
+ /** cowOverride: logical properties copied INSERT…SELECT from synced (pk included). */
1656
+ cowCopyColumns: zod.z.array(zod.z.string()).optional(),
1657
+ /** cowOverride: insert-policy WITH CHECK evaluated over the copied rows. */
1658
+ cowCopyCheck: predIrSchema.optional(),
942
1659
  /** insert/insertOne: rows come from a slot ($object/$objects) or literals. */
943
1660
  objects: zod.z.union([
944
1661
  zod.z.object({ slot: zod.z.string() }),
@@ -959,19 +1676,11 @@ var mutationRootSchema = zod.z.object({
959
1676
  affectedRows: zod.z.boolean().default(false),
960
1677
  includeIf: includeIfSchema.optional()
961
1678
  });
962
- var actionRootSchema = zod.z.object({
963
- kind: zod.z.literal("action"),
964
- alias: zod.z.string(),
965
- name: zod.z.string(),
966
- params: zod.z.array(zod.z.object({ name: zod.z.string(), value: valueIrSchema })),
967
- returns: zod.z.enum(["rows", "rowsAffected", "scalar"])
968
- });
969
1679
  var rootIrSchema = zod.z.discriminatedUnion("kind", [
970
1680
  selectRootSchema,
971
1681
  byPkRootSchema,
972
1682
  aggregateRootSchema,
973
- mutationRootSchema,
974
- actionRootSchema
1683
+ mutationRootSchema
975
1684
  ]);
976
1685
  var INPUT_REF_KINDS = [
977
1686
  "boolExp",
@@ -1003,7 +1712,7 @@ var compiledOperationSchema = zod.z.object({
1003
1712
  irVersion: zod.z.literal(IR_VERSION),
1004
1713
  name: zod.z.string(),
1005
1714
  opHash: zod.z.string().regex(/^sha256:[0-9a-f]{64}$/),
1006
- kind: zod.z.enum(["query", "mutation", "action"]),
1715
+ kind: zod.z.enum(["query", "mutation"]),
1007
1716
  requires: zod.z.object({
1008
1717
  capability: zod.z.string().optional(),
1009
1718
  role: zod.z.string().optional()
@@ -1017,13 +1726,24 @@ var tableMetaIrSchema = zod.z.object({
1017
1726
  table: zod.z.string(),
1018
1727
  schema: zod.z.string(),
1019
1728
  typename: zod.z.string(),
1729
+ /**
1730
+ * True when statements over this relation evaluate a claims-gated synced
1731
+ * view: synced twins (`source: gold.*`) materialize as views whose baked
1732
+ * policy reads `current_setting('request.jwt.claims')`, and namespace views
1733
+ * inherit the gate transitively through their declared dependencies. The
1734
+ * executor must stamp that GUC (transaction-scoped) before any statement
1735
+ * touching such a relation, or the synced arm yields zero rows regardless
1736
+ * of the compiled predicates.
1737
+ */
1738
+ claimsGated: zod.z.boolean().default(false),
1020
1739
  columns: zod.z.array(zod.z.object({
1021
1740
  name: zod.z.string(),
1022
1741
  columnName: zod.z.string(),
1023
1742
  type: zod.z.union([zod.z.enum(COLUMN_TYPES), zod.z.object({ enum: zod.z.string(), values: zod.z.array(zod.z.string()) })]),
1024
1743
  nullable: zod.z.boolean()
1025
1744
  })),
1026
- primaryKey: zod.z.array(zod.z.string()).min(1),
1745
+ /** Empty for keyless read-only views; keyed tables and views list logical columns. */
1746
+ primaryKey: zod.z.array(zod.z.string()),
1027
1747
  links: zod.z.array(zod.z.object({
1028
1748
  name: zod.z.string(),
1029
1749
  kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
@@ -1209,6 +1929,10 @@ var Parser = class {
1209
1929
  this.expect("rparen", "')'");
1210
1930
  return inner;
1211
1931
  }
1932
+ if (this.peekKeyword("true") || this.peekKeyword("false")) {
1933
+ const token = this.advance();
1934
+ return { kind: "literal", value: token.value === "true" };
1935
+ }
1212
1936
  if (this.peekKeyword("capability") || this.peekKeyword("role")) {
1213
1937
  const fn = this.peek().value;
1214
1938
  const node = this.parseBooleanCall(fn);
@@ -1392,6 +2116,8 @@ var PolicyCompiler = class {
1392
2116
  }
1393
2117
  compile(node, table, depth) {
1394
2118
  switch (node.kind) {
2119
+ case "literal":
2120
+ return { kind: "const", value: node.value };
1395
2121
  case "and":
1396
2122
  case "or":
1397
2123
  return { kind: node.kind, operands: node.operands.map((operand) => this.compile(operand, table, depth)) };
@@ -1562,6 +2288,7 @@ var NamespaceCompileError = class extends Error {
1562
2288
 
1563
2289
  // src/compiler-gql/pred-utils.ts
1564
2290
  var CONST_TRUE = { kind: "const", value: true };
2291
+ var CONST_FALSE = { kind: "const", value: false };
1565
2292
  function andPred(...preds) {
1566
2293
  const operands = preds.filter(
1567
2294
  (pred) => pred !== void 0 && !(pred.kind === "const" && pred.value)
@@ -1647,6 +2374,12 @@ var PolicyInjector = class {
1647
2374
  write(table) {
1648
2375
  return this.ctx.writePolicies.get(table) ?? { kind: "const", value: true };
1649
2376
  }
2377
+ insert(table) {
2378
+ return this.ctx.insertPolicies.get(table) ?? this.write(table);
2379
+ }
2380
+ update(table) {
2381
+ return this.ctx.updatePolicies.get(table) ?? this.write(table);
2382
+ }
1650
2383
  linkTarget(table, link) {
1651
2384
  const meta = this.ctx.tables[table];
1652
2385
  const resolved = meta?.links.find((l) => l.name === link);
@@ -1721,31 +2454,50 @@ var PolicyInjector = class {
1721
2454
  };
1722
2455
  case "mutation":
1723
2456
  return this.injectMutation(root);
1724
- case "action":
1725
- return root;
1726
2457
  }
1727
2458
  }
1728
2459
  injectMutation(root) {
1729
2460
  const table = root.table;
1730
- const writePolicy = this.write(table);
1731
2461
  const returning = root.returning ? this.injectSelection(root.returning, table) : root.returning;
1732
2462
  if (root.op === "insert" || root.op === "insertOne") {
1733
- if (predContainsExists(writePolicy)) {
2463
+ const insertPolicy = this.insert(table);
2464
+ if (predContainsExists(insertPolicy)) {
1734
2465
  throw new DocumentCompileError(
1735
- `table '${table}' cannot be inserted into: v2.0 insert checks are claims + inserted-values only, but its write policy traverses a link (exists)`,
2466
+ `table '${table}' cannot be inserted into: v2.0 insert checks are claims + inserted-values only, but its insert policy traverses a link (exists)`,
1736
2467
  "insert-check-unrepresentable"
1737
2468
  );
1738
2469
  }
1739
2470
  const pins = (this.ctx.pins.get(table) ?? []).map(
1740
2471
  (pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
1741
2472
  );
1742
- return { ...root, pinnedColumns: pins, insertCheck: writePolicy, returning };
2473
+ if (root.onConflict && this.ctx.splitWritePolicyTables.has(table)) {
2474
+ throw new DocumentCompileError(
2475
+ `table '${table}' cannot use onConflict while insert and update policies are split \u2014 use separate insert and update mutations`,
2476
+ "on-conflict-split-policies"
2477
+ );
2478
+ }
2479
+ return { ...root, pinnedColumns: pins, insertCheck: insertPolicy, returning };
1743
2480
  }
1744
- const where = andPred(writePolicy, root.where ? this.injectDocPred(root.where, table) : void 0);
2481
+ const updatePolicy = this.update(table);
2482
+ const where = andPred(updatePolicy, root.where ? this.injectDocPred(root.where, table) : void 0);
1745
2483
  if (root.op === "update" || root.op === "updateByPk") {
1746
- const check = predContainsExists(writePolicy) ? {} : { insertCheck: writePolicy };
2484
+ const check = predContainsExists(updatePolicy) ? {} : { insertCheck: updatePolicy };
1747
2485
  return { ...root, where, ...check, returning };
1748
2486
  }
2487
+ if (root.op === "cowOverride") {
2488
+ const insertPolicy = this.insert(table);
2489
+ if (predContainsExists(insertPolicy)) {
2490
+ throw new DocumentCompileError(
2491
+ `table '${table}' cannot be copy-on-write overridden: v2.0 insert checks are claims + inserted-values only, but its insert policy traverses a link (exists)`,
2492
+ "insert-check-unrepresentable"
2493
+ );
2494
+ }
2495
+ const pins = (this.ctx.pins.get(table) ?? []).map(
2496
+ (pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
2497
+ );
2498
+ const check = predContainsExists(updatePolicy) ? {} : { insertCheck: updatePolicy };
2499
+ return { ...root, pinnedColumns: pins, cowCopyCheck: insertPolicy, where, ...check, returning };
2500
+ }
1749
2501
  return { ...root, where, returning };
1750
2502
  }
1751
2503
  };
@@ -1791,7 +2543,6 @@ function collectTableSet(roots, ctx) {
1791
2543
  }
1792
2544
  };
1793
2545
  for (const root of roots) {
1794
- if (root.kind === "action") continue;
1795
2546
  tables.add(root.table);
1796
2547
  if (root.kind === "select" || root.kind === "byPk") {
1797
2548
  visitPred(root.where, root.table);
@@ -1802,8 +2553,10 @@ function collectTableSet(roots, ctx) {
1802
2553
  if (root.nodes) visitSelection(root.nodes, root.table);
1803
2554
  }
1804
2555
  if (root.kind === "mutation") {
2556
+ if (root.cowSynced !== void 0) tables.add(root.cowSynced);
1805
2557
  if (root.where) visitPred(root.where, root.table);
1806
2558
  if (root.insertCheck) visitPred(root.insertCheck, root.table);
2559
+ if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
1807
2560
  if (root.returning) visitSelection(root.returning, root.table);
1808
2561
  }
1809
2562
  }
@@ -1870,6 +2623,7 @@ function platformUserMeta() {
1870
2623
  table: "users_directory",
1871
2624
  schema: "platform",
1872
2625
  typename: PLATFORM_USER_TYPENAME,
2626
+ claimsGated: false,
1873
2627
  columns: columns.map((c) => ({ ...c })),
1874
2628
  primaryKey: ["sub"],
1875
2629
  links: [],
@@ -1912,15 +2666,30 @@ function buildCompileContext(rawCanonical) {
1912
2666
  const canonical = normalizeCanonical(canonicalDefinitionSchema.parse(rawCanonical));
1913
2667
  const diagnostics = [];
1914
2668
  const enums = new Map(canonical.enums.map((e) => [e.name, e]));
2669
+ const relations = canonicalRelations(canonical);
2670
+ const viewNames = new Set((canonical.views ?? []).map((view) => view.view));
1915
2671
  const columnEntries = /* @__PURE__ */ new Map();
1916
2672
  const tables = {};
1917
- const usesPlatformUsers = canonical.tables.some((t2) => t2.links.some((l) => l.kind === "toPlatformUser"));
2673
+ const usesPlatformUsers = relations.some((t2) => t2.links.some((l) => l.kind === "toPlatformUser"));
1918
2674
  const platformMeta = usesPlatformUsers ? platformUserMeta() : void 0;
1919
2675
  if (platformMeta) tables[PLATFORM_USER_TABLE] = platformMeta;
1920
- for (const table of canonical.tables) {
2676
+ const syncedTableNames = new Set(canonical.tables.filter((t2) => t2.source !== void 0).map((t2) => t2.table));
2677
+ const viewDependencies = new Map((canonical.views ?? []).map((view) => [view.view, view.dependencies]));
2678
+ const claimsGatedMemo = /* @__PURE__ */ new Map();
2679
+ const relationIsClaimsGated = (name) => {
2680
+ if (syncedTableNames.has(name)) return true;
2681
+ const memo = claimsGatedMemo.get(name);
2682
+ if (memo !== void 0) return memo;
2683
+ claimsGatedMemo.set(name, false);
2684
+ const gated = (viewDependencies.get(name) ?? []).some(relationIsClaimsGated);
2685
+ claimsGatedMemo.set(name, gated);
2686
+ return gated;
2687
+ };
2688
+ for (const table of relations) {
2689
+ const isView = viewNames.has(table.table);
1921
2690
  const entries = resolveColumnEntries(table, enums, diagnostics);
1922
2691
  columnEntries.set(table.table, entries);
1923
- const pkRefs = table.primaryKey ?? ["id"];
2692
+ const pkRefs = table.primaryKey ?? (isView ? [] : ["id"]);
1924
2693
  const primaryKey2 = [];
1925
2694
  for (const ref of pkRefs) {
1926
2695
  const resolved = logicalColumn(entries, ref);
@@ -1941,7 +2710,7 @@ function buildCompileContext(rawCanonical) {
1941
2710
  for (const column of table.columns) {
1942
2711
  if (column.unique) {
1943
2712
  const logical = logicalColumn(entries, column.columnName);
1944
- constraints.push({ name: `${table.table}_${column.columnName}_key`, columns: [logical] });
2713
+ constraints.push({ name: `${table.table}_${column.columnName}_unique`, columns: [logical] });
1945
2714
  }
1946
2715
  }
1947
2716
  for (const index3 of table.indexes) {
@@ -1953,20 +2722,23 @@ function buildCompileContext(rawCanonical) {
1953
2722
  table: table.table,
1954
2723
  schema: ormSchemaName(canonical.namespace),
1955
2724
  typename: typeNameForTable(table.table),
2725
+ // Synced twins (and views reading them, resolved transitively below)
2726
+ // are claims-gated — see tableMetaIrSchema.claimsGated.
2727
+ claimsGated: relationIsClaimsGated(table.table),
1956
2728
  columns: entries.map(({ deprecated: _deprecated, ...column }) => column),
1957
- primaryKey: primaryKey2.length > 0 ? primaryKey2 : ["id"],
2729
+ primaryKey: isView ? primaryKey2 : primaryKey2.length > 0 ? primaryKey2 : ["id"],
1958
2730
  links: [],
1959
2731
  filterable: entries.map((c) => c.name),
1960
2732
  orderable: entries.filter((c) => c.type !== "jsonb").map((c) => c.name),
1961
- insertable: active.map((c) => c.name),
1962
- settable: active.map((c) => c.name),
1963
- appendable: active.filter((c) => c.type === "jsonb").map((c) => c.name),
1964
- constraints,
2733
+ insertable: isView ? [] : active.map((c) => c.name),
2734
+ settable: isView ? [] : active.map((c) => c.name),
2735
+ appendable: isView ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
2736
+ constraints: isView ? [] : constraints,
1965
2737
  dynamicFilterMaxDepth: 6,
1966
2738
  dynamicFilterMaxNodes: 64
1967
2739
  };
1968
2740
  }
1969
- for (const table of canonical.tables) {
2741
+ for (const table of relations) {
1970
2742
  const meta = tables[table.table];
1971
2743
  const entries = columnEntries.get(table.table);
1972
2744
  for (const link of table.links) {
@@ -2001,17 +2773,17 @@ function buildCompileContext(rawCanonical) {
2001
2773
  }
2002
2774
  const readPolicies = /* @__PURE__ */ new Map();
2003
2775
  const writePolicies = /* @__PURE__ */ new Map();
2776
+ const insertPolicies = /* @__PURE__ */ new Map();
2777
+ const updatePolicies = /* @__PURE__ */ new Map();
2004
2778
  const pins = /* @__PURE__ */ new Map();
2779
+ const splitWritePolicyTables = /* @__PURE__ */ new Set();
2005
2780
  const resolver = { tables };
2006
- for (const table of canonical.tables) {
2007
- for (const [access, store] of [
2008
- ["read", readPolicies],
2009
- ["write", writePolicies]
2010
- ]) {
2781
+ for (const table of relations) {
2782
+ const isView = viewNames.has(table.table);
2783
+ const compilePolicyFor = (access, store) => {
2011
2784
  const expression = table.policies[access];
2012
2785
  if (expression === void 0) {
2013
- store.set(table.table, CONST_TRUE);
2014
- continue;
2786
+ return;
2015
2787
  }
2016
2788
  try {
2017
2789
  store.set(table.table, compilePolicy({ expression, table: table.table, resolver }));
@@ -2024,37 +2796,117 @@ function buildCompileContext(rawCanonical) {
2024
2796
  table: table.table
2025
2797
  });
2026
2798
  store.set(table.table, CONST_TRUE);
2027
- continue;
2799
+ return;
2028
2800
  }
2029
2801
  throw error;
2030
2802
  }
2803
+ };
2804
+ compilePolicyFor("read", readPolicies);
2805
+ compilePolicyFor("write", writePolicies);
2806
+ compilePolicyFor("insert", insertPolicies);
2807
+ compilePolicyFor("update", updatePolicies);
2808
+ if (isView) {
2809
+ writePolicies.set(table.table, CONST_FALSE);
2810
+ insertPolicies.set(table.table, CONST_FALSE);
2811
+ updatePolicies.set(table.table, CONST_FALSE);
2812
+ pins.set(table.table, []);
2813
+ continue;
2814
+ }
2815
+ if (table.policies.insert !== void 0 || table.policies.update !== void 0) {
2816
+ splitWritePolicyTables.add(table.table);
2817
+ }
2818
+ const fallbackWrite = writePolicies.get(table.table);
2819
+ const hasSplitPolicy = table.policies.insert !== void 0 || table.policies.update !== void 0;
2820
+ if (!insertPolicies.has(table.table)) {
2821
+ if (fallbackWrite !== void 0) insertPolicies.set(table.table, fallbackWrite);
2822
+ else if (hasSplitPolicy) insertPolicies.set(table.table, CONST_FALSE);
2823
+ else insertPolicies.set(table.table, CONST_TRUE);
2031
2824
  }
2032
- const tablePins = collectPinnedColumns(writePolicies.get(table.table));
2825
+ if (!updatePolicies.has(table.table)) {
2826
+ if (fallbackWrite !== void 0) updatePolicies.set(table.table, fallbackWrite);
2827
+ else if (hasSplitPolicy) updatePolicies.set(table.table, CONST_FALSE);
2828
+ else updatePolicies.set(table.table, CONST_TRUE);
2829
+ }
2830
+ const tablePins = collectPinnedColumns(insertPolicies.get(table.table));
2033
2831
  pins.set(table.table, tablePins);
2034
2832
  const pinned = new Set(tablePins.map((pin) => pin.column));
2035
2833
  const meta = tables[table.table];
2036
2834
  meta.insertable = meta.insertable.filter((column) => !pinned.has(column));
2037
- meta.settable = meta.settable.filter((column) => !pinned.has(column));
2038
2835
  }
2039
2836
  if (platformMeta) {
2040
2837
  readPolicies.set(PLATFORM_USER_TABLE, CONST_TRUE);
2041
2838
  writePolicies.set(PLATFORM_USER_TABLE, { kind: "const", value: false });
2839
+ insertPolicies.set(PLATFORM_USER_TABLE, { kind: "const", value: false });
2840
+ updatePolicies.set(PLATFORM_USER_TABLE, { kind: "const", value: false });
2042
2841
  pins.set(PLATFORM_USER_TABLE, []);
2043
2842
  }
2044
2843
  const tableByTypename = new Map(Object.entries(tables).map(([key, meta]) => [meta.typename, key]));
2045
2844
  const queryRoots = /* @__PURE__ */ new Map();
2046
2845
  const mutationRoots = /* @__PURE__ */ new Map();
2047
- for (const table of canonical.tables) {
2846
+ for (const table of relations) {
2048
2847
  const roots = queryRootNames(table.table);
2049
2848
  queryRoots.set(roots.select, { kind: "select", table: table.table });
2050
- queryRoots.set(roots.byPk, { kind: "byPk", table: table.table });
2849
+ if ((tables[table.table]?.primaryKey.length ?? 0) > 0) {
2850
+ queryRoots.set(roots.byPk, { kind: "byPk", table: table.table });
2851
+ }
2051
2852
  queryRoots.set(roots.aggregate, { kind: "aggregate", table: table.table });
2052
- if (table.source === void 0) {
2853
+ const hasWritePolicy = table.policies.write !== void 0 || table.policies.insert !== void 0 || table.policies.update !== void 0;
2854
+ if (!viewNames.has(table.table) && table.source === void 0 && hasWritePolicy) {
2053
2855
  for (const [op, field] of Object.entries(mutationRootNames(table.table))) {
2054
2856
  mutationRoots.set(field, { op, table: table.table });
2055
2857
  }
2056
2858
  }
2057
2859
  }
2860
+ for (const table of relations) {
2861
+ for (const mutation of table.cowMutations ?? []) {
2862
+ const hasWritePolicy = table.policies.write !== void 0 || table.policies.insert !== void 0 || table.policies.update !== void 0;
2863
+ if (!hasWritePolicy) {
2864
+ diagnostics.push({
2865
+ severity: "error",
2866
+ code: "cow-mutation-unpoliced",
2867
+ message: `cow mutation '${mutation.name}' on '${table.table}' requires a write (or insert/update) policy on the overlay table`,
2868
+ table: table.table
2869
+ });
2870
+ continue;
2871
+ }
2872
+ const field = camelCase(mutation.name);
2873
+ if (mutationRoots.has(field) || queryRoots.has(field)) {
2874
+ diagnostics.push({
2875
+ severity: "error",
2876
+ code: "cow-mutation-name-collision",
2877
+ message: `cow mutation '${mutation.name}' on '${table.table}' collides with the generated root '${field}'`,
2878
+ table: table.table
2879
+ });
2880
+ continue;
2881
+ }
2882
+ if (mutation.kind === "override") {
2883
+ const pinned = new Set((pins.get(table.table) ?? []).map((pin) => pin.column));
2884
+ const covered = /* @__PURE__ */ new Set([mutation.pk, ...mutation.copy, ...pinned]);
2885
+ const gaps = table.columns.filter((column) => {
2886
+ if (covered.has(column.name)) return false;
2887
+ const hasDefault = column.default !== void 0 || column.defaultFn !== void 0;
2888
+ return column.nullable === false && !hasDefault;
2889
+ });
2890
+ if (gaps.length > 0) {
2891
+ diagnostics.push({
2892
+ severity: "error",
2893
+ code: "cow-copy-uncovered-column",
2894
+ message: `cow mutation '${mutation.name}' on '${table.table}': overlay-only column(s) ${gaps.map((column) => `'${column.name}'`).join(", ")} are NOT NULL without a default, a policy pin, or a synced contract column \u2014 the copy insert cannot populate them`,
2895
+ table: table.table
2896
+ });
2897
+ continue;
2898
+ }
2899
+ }
2900
+ mutationRoots.set(field, {
2901
+ op: mutation.kind === "override" ? "cowOverride" : "cowRevert",
2902
+ table: table.table,
2903
+ synced: mutation.synced,
2904
+ pk: mutation.pk,
2905
+ set: mutation.set,
2906
+ copyColumns: mutation.copy
2907
+ });
2908
+ }
2909
+ }
2058
2910
  const enumsByTypeName = new Map(canonical.enums.map((e) => [enumTypeName(e.name), e]));
2059
2911
  return {
2060
2912
  ctx: {
@@ -2066,7 +2918,10 @@ function buildCompileContext(rawCanonical) {
2066
2918
  enumsByTypeName,
2067
2919
  readPolicies,
2068
2920
  writePolicies,
2069
- pins
2921
+ insertPolicies,
2922
+ updatePolicies,
2923
+ pins,
2924
+ splitWritePolicyTables
2070
2925
  },
2071
2926
  diagnostics
2072
2927
  };
@@ -2618,6 +3473,9 @@ var OperationLowerer = class {
2618
3473
  const alias = node.alias?.value ?? name;
2619
3474
  const rootInfo = this.ctx.mutationRoots.get(name);
2620
3475
  if (!rootInfo) fail(`unknown mutation root '${name}'`, "unknown-root", [...this.ctx.mutationRoots.keys()]);
3476
+ if (rootInfo.op === "cowOverride" || rootInfo.op === "cowRevert") {
3477
+ return this.lowerCowRoot(node, rootInfo);
3478
+ }
2621
3479
  const { op, table } = rootInfo;
2622
3480
  const args = argsByName(node);
2623
3481
  if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
@@ -2687,6 +3545,55 @@ var OperationLowerer = class {
2687
3545
  }
2688
3546
  }
2689
3547
  }
3548
+ /**
3549
+ * Copy-on-write roots: flat args — the pk plus (override only) every
3550
+ * declared set column. Overrides are full snapshots of the set columns, so
3551
+ * every set argument must be supplied explicitly (pass null to clear a
3552
+ * nullable column); the executor generates the copy/update/delete SQL from
3553
+ * the structured fields, never from authored statements.
3554
+ */
3555
+ lowerCowRoot(node, rootInfo) {
3556
+ const name = node.name.value;
3557
+ const alias = node.alias?.value ?? name;
3558
+ const { table } = rootInfo;
3559
+ if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
3560
+ const meta = this.meta(table);
3561
+ const args = argsByName(node);
3562
+ const pkArg = args.get(rootInfo.pk);
3563
+ if (!pkArg) fail(`'${name}' is missing primary-key argument '${rootInfo.pk}'`, "missing-pk-argument", [rootInfo.pk]);
3564
+ const pk = [{ column: rootInfo.pk, value: this.lowerValue(pkArg.value) }];
3565
+ const base = {
3566
+ kind: "mutation",
3567
+ op: rootInfo.op,
3568
+ alias,
3569
+ table,
3570
+ pinnedColumns: [],
3571
+ affectedRows: false,
3572
+ pk,
3573
+ cowSynced: rootInfo.synced,
3574
+ returning: this.lowerSelection(node.selectionSet, table)
3575
+ };
3576
+ if (rootInfo.op === "cowRevert") {
3577
+ assertKnownArgs(node, [rootInfo.pk]);
3578
+ return base;
3579
+ }
3580
+ assertKnownArgs(node, [rootInfo.pk, ...rootInfo.set]);
3581
+ const entries = rootInfo.set.map((column) => {
3582
+ const arg = args.get(column);
3583
+ if (!arg) {
3584
+ fail(
3585
+ `'${name}' is missing set argument '${column}' \u2014 a cow override writes every declared set column (pass null to clear a nullable one)`,
3586
+ "missing-argument",
3587
+ [...rootInfo.set]
3588
+ );
3589
+ }
3590
+ if (!meta.settable.includes(column)) {
3591
+ fail(`column '${table}.${column}' is not updatable (pinned by the write policy or deprecated)`, "column-not-settable", meta.settable);
3592
+ }
3593
+ return { column, value: this.lowerValue(arg.value) };
3594
+ });
3595
+ return { ...base, set: { entries }, cowCopyColumns: [...rootInfo.copyColumns] };
3596
+ }
2690
3597
  lowerMutationResponse(selectionSet, table) {
2691
3598
  let affectedRows = false;
2692
3599
  let returning = null;
@@ -2780,8 +3687,8 @@ var OperationLowerer = class {
2780
3687
  if (inc.value.kind === graphql.Kind.VARIABLE) {
2781
3688
  fail("_inc has no dynamic input type \u2014 inline the columns with variable leaf values", "invalid-inc");
2782
3689
  }
2783
- const numeric3 = meta.columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type) && meta.settable.includes(c.name)).map((c) => c.name);
2784
- result.inc = { entries: this.lowerColumnEntries(inc.value, table, numeric3, "_inc") };
3690
+ const numeric4 = meta.columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type) && meta.settable.includes(c.name)).map((c) => c.name);
3691
+ result.inc = { entries: this.lowerColumnEntries(inc.value, table, numeric4, "_inc") };
2785
3692
  }
2786
3693
  if (!result.set && !result.append && !result.inc) {
2787
3694
  fail(`an update on '${table}' needs at least one of _set/_append/_inc`, "missing-argument");
@@ -3132,13 +4039,17 @@ function generateNamespaceTypes(definition) {
3132
4039
  `export type ${typesInterfaceName(definition.namespace)} = {`
3133
4040
  ];
3134
4041
  for (const table of definition.tables) {
3135
- if (table.source) {
4042
+ const columns = Object.entries(table.columns ?? {});
4043
+ if (table.source && columns.length === 0) {
3136
4044
  lines.push(` /** Synced from ${table.source} \u2014 columns reflect the gold source. */`);
3137
4045
  lines.push(` ${table.table}: Record<string, unknown>`);
3138
4046
  continue;
3139
4047
  }
4048
+ if (table.source) {
4049
+ lines.push(` /** Synced from ${table.source} \u2014 read-only typed contract. */`);
4050
+ }
3140
4051
  lines.push(` ${table.table}: {`);
3141
- for (const [name, column] of Object.entries(table.columns ?? {})) {
4052
+ for (const [name, column] of columns) {
3142
4053
  if (column.deprecated) continue;
3143
4054
  lines.push(` ${name}: ${columnWireType(column)}`);
3144
4055
  }
@@ -3604,8 +4515,8 @@ var actionSchema = zod.z.strictObject({
3604
4515
  }
3605
4516
  }
3606
4517
  });
3607
- function extractNamedParams(sql3) {
3608
- const withoutStrings = sql3.replace(QUOTED_STRING_PATTERN, "''");
4518
+ function extractNamedParams(sql4) {
4519
+ const withoutStrings = sql4.replace(QUOTED_STRING_PATTERN, "''");
3609
4520
  const names = [...withoutStrings.matchAll(NAMED_PARAM_PATTERN)].map((match) => match[1]);
3610
4521
  return [...new Set(names)];
3611
4522
  }
@@ -3659,7 +4570,9 @@ var indexSchema = zod.z.union([
3659
4570
  ]).transform((index3) => Array.isArray(index3) ? { columns: index3 } : index3);
3660
4571
  var policiesSchema = zod.z.strictObject({
3661
4572
  read: parsedPredicateSchema.optional(),
3662
- write: parsedPredicateSchema.optional()
4573
+ write: parsedPredicateSchema.optional(),
4574
+ insert: parsedPredicateSchema.optional(),
4575
+ update: parsedPredicateSchema.optional()
3663
4576
  });
3664
4577
  var tableFileSchema = zod.z.strictObject({
3665
4578
  namespace: namespaceSchema,
@@ -3695,7 +4608,6 @@ var policyName = (table, suffix) => `${table}_${suffix}`;
3695
4608
  var foreignKeyName = (table, link) => `fk_${table}_${link}`;
3696
4609
  function validateSyncedTable(table, ctx) {
3697
4610
  const forbidden = [
3698
- ["columns", "synced tables reflect their source; columns are not declared"],
3699
4611
  ["actions", "synced tables are read-only; actions are not allowed"],
3700
4612
  ["indexes", "synced tables are system-owned; indexes are not allowed"]
3701
4613
  ];
@@ -3705,6 +4617,39 @@ function validateSyncedTable(table, ctx) {
3705
4617
  if (table.policies?.write) {
3706
4618
  ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "write"] });
3707
4619
  }
4620
+ if (table.policies?.insert) {
4621
+ ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "insert"] });
4622
+ }
4623
+ if (table.policies?.update) {
4624
+ ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "update"] });
4625
+ }
4626
+ const columns = table.columns ?? {};
4627
+ const names = Object.keys(columns);
4628
+ if (names.length === 0) return;
4629
+ const primaryKeys = names.filter((name) => columns[name]?.primary_key);
4630
+ if (primaryKeys.length !== 1) {
4631
+ ctx.addIssue({ code: "custom", message: "exactly one column must be primary_key", path: ["columns"] });
4632
+ }
4633
+ validateRenames(columns, names, ctx);
4634
+ for (const name of names) {
4635
+ const column = columns[name];
4636
+ if (!column) continue;
4637
+ const cast = column.cast ? castParts(column.cast) : null;
4638
+ if (column.cast && cast?.column !== name) {
4639
+ ctx.addIssue({
4640
+ code: "custom",
4641
+ message: "cast must reference the column it is declared on",
4642
+ path: ["columns", name, "cast"]
4643
+ });
4644
+ }
4645
+ if (cast && cast.type !== column.type) {
4646
+ ctx.addIssue({
4647
+ code: "custom",
4648
+ message: `cast target '${cast.type}' must match the declared type '${column.type}'`,
4649
+ path: ["columns", name, "cast"]
4650
+ });
4651
+ }
4652
+ }
3708
4653
  }
3709
4654
  function validateRenames(columns, names, ctx) {
3710
4655
  const renameSources = /* @__PURE__ */ new Map();
@@ -3782,7 +4727,7 @@ function validateColumnTable(table, ctx) {
3782
4727
  function validateLinks(table, ctx) {
3783
4728
  const columnNames = Object.keys(table.columns ?? {});
3784
4729
  for (const [name, link] of Object.entries(table.links ?? {})) {
3785
- if (!table.source && !columnNames.includes(link.on)) {
4730
+ if (columnNames.length > 0 && !columnNames.includes(link.on)) {
3786
4731
  ctx.addIssue({ code: "custom", message: `link references unknown column '${link.on}'`, path: ["links", name, "on"] });
3787
4732
  }
3788
4733
  if (link.enforce && link.to.includes(".")) {
@@ -3855,10 +4800,10 @@ var TYPE_GROUPS = {
3855
4800
  };
3856
4801
  var columnTypeGroup = (type) => TYPE_GROUPS[type];
3857
4802
  function validatePolicyColumns(table, ctx) {
3858
- if (table.source) return;
3859
4803
  const columns = table.columns ?? {};
3860
4804
  const columnNames = Object.keys(columns);
3861
- for (const kind of ["read", "write"]) {
4805
+ if (columnNames.length === 0) return;
4806
+ for (const kind of ["read", "write", "insert", "update"]) {
3862
4807
  const predicate = table.policies?.[kind];
3863
4808
  if (!predicate) continue;
3864
4809
  for (const column of referencedColumns(predicate.ast)) {
@@ -4013,10 +4958,15 @@ function policyStatements(schema, table) {
4013
4958
  if (read) {
4014
4959
  statements.push(`CREATE POLICY ${policyName2("read")} ON ${qualified} FOR SELECT USING (${compilePredicate(read.ast, columnTypes)})`);
4015
4960
  }
4016
- const write = table.policies?.write;
4017
- if (write) {
4018
- const predicate = compilePredicate(write.ast, columnTypes);
4019
- statements.push(`CREATE POLICY ${policyName2("insert")} ON ${qualified} FOR INSERT WITH CHECK (${predicate})`);
4961
+ const insertPolicy = table.policies?.insert ?? table.policies?.write;
4962
+ const updatePolicy = table.policies?.update ?? table.policies?.write;
4963
+ if (insertPolicy) {
4964
+ statements.push(
4965
+ `CREATE POLICY ${policyName2("insert")} ON ${qualified} FOR INSERT WITH CHECK (${compilePredicate(insertPolicy.ast, columnTypes)})`
4966
+ );
4967
+ }
4968
+ if (updatePolicy) {
4969
+ const predicate = compilePredicate(updatePolicy.ast, columnTypes);
4020
4970
  statements.push(
4021
4971
  `CREATE POLICY ${policyName2("update")} ON ${qualified} FOR UPDATE USING (${predicate}) WITH CHECK (${predicate})`
4022
4972
  );
@@ -4240,11 +5190,268 @@ function buildDrizzleEntitiesV2(definition) {
4240
5190
  imports[table.table] = drizzleTable;
4241
5191
  tables[table.table] = drizzleTable;
4242
5192
  }
5193
+ for (const view of definition.views ?? []) {
5194
+ const columns = Object.fromEntries(
5195
+ view.columns.map((column) => [column.columnName, buildViewColumnV2({ view: view.view, column, enums })])
5196
+ );
5197
+ imports[view.view] = schema.view(view.view, columns).as(
5198
+ drizzleOrm.sql.raw(
5199
+ rewriteSyncedSourceRelationsInViewQuery({
5200
+ query: view.query,
5201
+ canonical: definition,
5202
+ schemaName,
5203
+ dependencies: view.dependencies
5204
+ })
5205
+ )
5206
+ );
5207
+ }
4243
5208
  return { imports, tables, schemaName };
4244
5209
  }
5210
+ function rewriteSyncedSourceRelationsInViewQuery({
5211
+ query,
5212
+ canonical,
5213
+ schemaName,
5214
+ dependencies
5215
+ }) {
5216
+ const tablesBySource = /* @__PURE__ */ new Map();
5217
+ for (const table of canonical.tables) {
5218
+ if (!table.source) continue;
5219
+ tablesBySource.set(table.source, [...tablesBySource.get(table.source) ?? [], table.table]);
5220
+ }
5221
+ const declared = new Set(dependencies ?? []);
5222
+ const boundTableBySource = /* @__PURE__ */ new Map();
5223
+ for (const [source, tables] of tablesBySource) {
5224
+ if (tables.length === 1) {
5225
+ boundTableBySource.set(source, tables[0]);
5226
+ continue;
5227
+ }
5228
+ const [sourceSchema, sourceName] = source.split(".");
5229
+ if (!sourceSchema || !sourceName) continue;
5230
+ const referenced = sqlReferencesQualifiedRelation(query, sourceSchema, sourceName);
5231
+ if (!referenced) continue;
5232
+ const declaredMatches = tables.filter((table) => declared.has(table));
5233
+ if (declaredMatches.length === 1) {
5234
+ boundTableBySource.set(source, declaredMatches[0]);
5235
+ continue;
5236
+ }
5237
+ throw new OrmDefinitionError(
5238
+ `synced source '${source}' is declared by multiple synced tables (${[...tables].sort().join(", ")}) \u2014 this view references it but its declared dependencies do not name exactly one of them, so the rewrite is ambiguous`
5239
+ );
5240
+ }
5241
+ return transformSqlOutsideLiterals(query, (segment) => {
5242
+ let rewritten = segment;
5243
+ for (const table of canonical.tables) {
5244
+ if (!table.source) continue;
5245
+ const boundTable = boundTableBySource.get(table.source);
5246
+ if (boundTable === void 0 || boundTable !== table.table) continue;
5247
+ const [sourceSchema, sourceName] = table.source.split(".");
5248
+ if (!sourceSchema || !sourceName) continue;
5249
+ rewritten = replaceQualifiedRelation(
5250
+ rewritten,
5251
+ sourceSchema,
5252
+ sourceName,
5253
+ schemaName,
5254
+ table.table
5255
+ );
5256
+ }
5257
+ return rewritten;
5258
+ });
5259
+ }
5260
+ function transformSqlOutsideLiterals(sql4, transform) {
5261
+ const out = [];
5262
+ let state = { kind: "normal" };
5263
+ let i = 0;
5264
+ let codeStart = 0;
5265
+ const emitCode = (end) => {
5266
+ if (end > codeStart) out.push(transform(sql4.slice(codeStart, end)));
5267
+ codeStart = end;
5268
+ };
5269
+ while (i < sql4.length) {
5270
+ const c = sql4[i];
5271
+ const next = i + 1 < sql4.length ? sql4[i + 1] : "";
5272
+ switch (state.kind) {
5273
+ case "normal": {
5274
+ if (c === "-" && next === "-") {
5275
+ emitCode(i);
5276
+ state = { kind: "line-comment" };
5277
+ out.push("--");
5278
+ i += 2;
5279
+ codeStart = i;
5280
+ break;
5281
+ }
5282
+ if (c === "/" && next === "*") {
5283
+ emitCode(i);
5284
+ state = { kind: "block-comment", depth: 1 };
5285
+ out.push("/*");
5286
+ i += 2;
5287
+ codeStart = i;
5288
+ break;
5289
+ }
5290
+ if ((c === "E" || c === "e") && next === "'") {
5291
+ const prev = i > 0 ? sql4[i - 1] : "";
5292
+ if (!/[A-Za-z0-9_]/.test(prev)) {
5293
+ emitCode(i);
5294
+ state = { kind: "e-single" };
5295
+ out.push("E'");
5296
+ i += 2;
5297
+ codeStart = i;
5298
+ break;
5299
+ }
5300
+ }
5301
+ if (c === "'") {
5302
+ emitCode(i);
5303
+ state = { kind: "single" };
5304
+ out.push("'");
5305
+ i += 1;
5306
+ codeStart = i;
5307
+ break;
5308
+ }
5309
+ if (c === "$") {
5310
+ const prev = i > 0 ? sql4[i - 1] : "";
5311
+ if (!/[A-Za-z0-9_$]/.test(prev)) {
5312
+ const tagMatch = /^\$([A-Za-z_][A-Za-z0-9_]*)?\$/.exec(sql4.slice(i));
5313
+ if (tagMatch) {
5314
+ emitCode(i);
5315
+ state = { kind: "dollar", tag: tagMatch[1] ?? "" };
5316
+ out.push(tagMatch[0]);
5317
+ i += tagMatch[0].length;
5318
+ codeStart = i;
5319
+ break;
5320
+ }
5321
+ }
5322
+ }
5323
+ i += 1;
5324
+ break;
5325
+ }
5326
+ case "single": {
5327
+ if (c === "'" && next === "'") {
5328
+ out.push("''");
5329
+ i += 2;
5330
+ break;
5331
+ }
5332
+ if (c === "'") {
5333
+ out.push("'");
5334
+ state = { kind: "normal" };
5335
+ i += 1;
5336
+ codeStart = i;
5337
+ break;
5338
+ }
5339
+ out.push(c);
5340
+ i += 1;
5341
+ break;
5342
+ }
5343
+ case "e-single": {
5344
+ if (c === "\\" && i + 1 < sql4.length) {
5345
+ out.push(c, sql4[i + 1]);
5346
+ i += 2;
5347
+ break;
5348
+ }
5349
+ if (c === "'" && next === "'") {
5350
+ out.push("''");
5351
+ i += 2;
5352
+ break;
5353
+ }
5354
+ if (c === "'") {
5355
+ out.push("'");
5356
+ state = { kind: "normal" };
5357
+ i += 1;
5358
+ codeStart = i;
5359
+ break;
5360
+ }
5361
+ out.push(c);
5362
+ i += 1;
5363
+ break;
5364
+ }
5365
+ case "dollar": {
5366
+ const closer = `$${state.tag}$`;
5367
+ if (sql4.startsWith(closer, i)) {
5368
+ out.push(closer);
5369
+ state = { kind: "normal" };
5370
+ i += closer.length;
5371
+ codeStart = i;
5372
+ break;
5373
+ }
5374
+ out.push(c);
5375
+ i += 1;
5376
+ break;
5377
+ }
5378
+ case "line-comment": {
5379
+ if (c === "\n") {
5380
+ out.push("\n");
5381
+ state = { kind: "normal" };
5382
+ i += 1;
5383
+ codeStart = i;
5384
+ break;
5385
+ }
5386
+ out.push(c);
5387
+ i += 1;
5388
+ break;
5389
+ }
5390
+ case "block-comment": {
5391
+ if (c === "/" && next === "*") {
5392
+ out.push("/*");
5393
+ state = { kind: "block-comment", depth: state.depth + 1 };
5394
+ i += 2;
5395
+ break;
5396
+ }
5397
+ if (c === "*" && next === "/") {
5398
+ out.push("*/");
5399
+ const newDepth = state.depth - 1;
5400
+ state = newDepth === 0 ? { kind: "normal" } : { kind: "block-comment", depth: newDepth };
5401
+ i += 2;
5402
+ if (state.kind === "normal") codeStart = i;
5403
+ break;
5404
+ }
5405
+ out.push(c);
5406
+ i += 1;
5407
+ break;
5408
+ }
5409
+ }
5410
+ }
5411
+ emitCode(sql4.length);
5412
+ return out.join("");
5413
+ }
5414
+ function escapeRegExp(value) {
5415
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5416
+ }
5417
+ function sqlReferencesQualifiedRelation(sql4, sourceSchema, sourceName) {
5418
+ let referenced = false;
5419
+ transformSqlOutsideLiterals(sql4, (segment) => {
5420
+ if (qualifiedRelationPattern(sourceSchema, sourceName).test(segment)) referenced = true;
5421
+ return segment;
5422
+ });
5423
+ return referenced;
5424
+ }
5425
+ function qualifiedRelationPattern(sourceSchema, sourceName) {
5426
+ const schemaPattern = `(?:"${escapeRegExp(sourceSchema)}"|${escapeRegExp(sourceSchema)})`;
5427
+ const namePattern = `(?:"${escapeRegExp(sourceName)}"|${escapeRegExp(sourceName)})`;
5428
+ return new RegExp(`(?<![\\w."])${schemaPattern}\\.${namePattern}(?![\\w"])`, "g");
5429
+ }
5430
+ function replaceQualifiedRelation(sql4, sourceSchema, sourceName, boundSchema, boundTable) {
5431
+ return sql4.replace(qualifiedRelationPattern(sourceSchema, sourceName), `"${boundSchema}"."${boundTable}"`);
5432
+ }
4245
5433
  function generateSnapshotV2(definition, previousId3) {
4246
5434
  const { imports, schemaName } = buildDrizzleEntitiesV2(definition);
4247
- return api.generateDrizzleJson(imports, previousId3, [schemaName]);
5435
+ const snapshot = api.generateDrizzleJson(imports, previousId3, [schemaName]);
5436
+ if ((definition.views?.length ?? 0) > 0) {
5437
+ const extended = snapshot;
5438
+ extended._sequenceOrm = {
5439
+ viewDependencies: Object.fromEntries(
5440
+ (definition.views ?? []).map((view) => [view.view, [...view.dependencies].sort()])
5441
+ )
5442
+ };
5443
+ }
5444
+ return snapshot;
5445
+ }
5446
+ function buildViewColumnV2({
5447
+ view,
5448
+ column,
5449
+ enums
5450
+ }) {
5451
+ const factory = resolveColumnFactory(view, column, enums);
5452
+ let builder = factory(column.columnName);
5453
+ if (!column.nullable) builder = builder.notNull();
5454
+ return builder;
4248
5455
  }
4249
5456
  function buildTable2(schema, table, built, enums, byName) {
4250
5457
  const primaryKeyColumns2 = table.primaryKey ?? [];
@@ -4488,16 +5695,270 @@ async function planMigrationV2({
4488
5695
  }) {
4489
5696
  const schema = ormSchemaName(canonical.namespace);
4490
5697
  const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
5698
+ assertValidViewDependencies(canonical);
4491
5699
  const hints = applyMigrationHintsV2(previous, canonical);
4492
5700
  const snapshot = generateSnapshotV2(canonical, previousId(previous));
4493
- const ddl = await diffStatements(hints.adjusted, snapshot);
5701
+ const baseDdl = await diffStatements(withoutViews(hints.adjusted), withoutViews(snapshot));
5702
+ const previousViews = snapshotViews(hints.adjusted);
5703
+ const currentViews = snapshotViews(snapshot);
5704
+ const previousDependencies = snapshotViewDependencies(hints.adjusted);
5705
+ const currentDependencies = snapshotViewDependencies(snapshot);
5706
+ const affected = affectedViewNames({
5707
+ previousViews,
5708
+ currentViews,
5709
+ previousDependencies,
5710
+ currentDependencies,
5711
+ baseChanged: hints.statements.length > 0 || baseDdl.length > 0
5712
+ });
5713
+ const dropOrder = topologicalViewNames(
5714
+ new Set([...affected].filter((name) => previousViews.has(name))),
5715
+ previousDependencies
5716
+ ).reverse();
5717
+ const createOrder = topologicalViewNames(
5718
+ new Set([...affected].filter((name) => currentViews.has(name))),
5719
+ currentDependencies
5720
+ );
5721
+ const dropDdl = await generatedViewStatements({
5722
+ snapshot: hints.adjusted,
5723
+ names: new Set(dropOrder),
5724
+ mode: "drop",
5725
+ schema
5726
+ });
5727
+ const createDdl = await generatedViewStatements({
5728
+ snapshot,
5729
+ names: new Set(createOrder),
5730
+ mode: "create",
5731
+ schema
5732
+ });
5733
+ const currentViewDefinitions = new Map((canonical.views ?? []).map((view) => [view.view, view]));
4494
5734
  const statements = [
5735
+ ...dropOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(dropDdl, name, schema, "DROP") })),
4495
5736
  ...hints.statements.map((statement) => ({ kind: "hint", sql: statement })),
4496
- ...ddl.map((statement) => ({ kind: "ddl", sql: statement.replace(/;\s*$/, "") }))
5737
+ ...baseDdl.map((statement) => ({ kind: "ddl", sql: statement.replace(/;\s*$/, "") })),
5738
+ ...createOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(createDdl, name, schema, "CREATE") })),
5739
+ ...createOrder.map((name) => {
5740
+ const view = currentViewDefinitions.get(name);
5741
+ if (!view) throw new OrmDefinitionError(`missing canonical definition for view '${name}'`);
5742
+ return { kind: "ddl", sql: validateViewCatalogStatement({ canonical, view, schema }) };
5743
+ })
4497
5744
  ];
4498
5745
  return { namespace: canonical.namespace, schema, statements, snapshot };
4499
5746
  }
5747
+ function validateViewCatalogStatement({
5748
+ canonical,
5749
+ view,
5750
+ schema
5751
+ }) {
5752
+ const qualified = `${schema}.${view.view}`;
5753
+ const expectedColumns = sqlTextArray(view.columns.map((column) => column.columnName));
5754
+ const expectedTypes = `ARRAY[${view.columns.map((column) => `${viewTypeRegtype(column, schema)}::oid`).join(", ")}]::oid[]`;
5755
+ const relationIdentity = /* @__PURE__ */ new Map();
5756
+ for (const table of canonical.tables) {
5757
+ relationIdentity.set(table.table, `${schema}.${table.table}`);
5758
+ }
5759
+ for (const candidate of canonical.views ?? []) {
5760
+ relationIdentity.set(candidate.view, `${schema}.${candidate.view}`);
5761
+ }
5762
+ const expectedDependencies = sqlTextArray(
5763
+ view.dependencies.map((dependency) => {
5764
+ const physical = relationIdentity.get(dependency);
5765
+ if (!physical) throw new OrmDefinitionError(`view '${view.view}' depends on unknown relation '${dependency}'`);
5766
+ return physical;
5767
+ }).sort()
5768
+ );
5769
+ const allowedFunctions = sqlTextArray(VIEW_QUERY_FUNCTION_ALLOWLIST);
5770
+ return `DO $sequence_orm_validate_view$
5771
+ DECLARE
5772
+ actual_columns text[];
5773
+ actual_types oid[];
5774
+ actual_dependencies text[];
5775
+ BEGIN
5776
+ SELECT
5777
+ array_agg(attribute.attname ORDER BY attribute.attnum),
5778
+ array_agg(attribute.atttypid ORDER BY attribute.attnum)
5779
+ INTO actual_columns, actual_types
5780
+ FROM pg_catalog.pg_attribute AS attribute
5781
+ WHERE attribute.attrelid = pg_catalog.to_regclass(${quoteLiteral(qualified)})
5782
+ AND attribute.attnum > 0
5783
+ AND NOT attribute.attisdropped;
5784
+
5785
+ IF actual_columns IS DISTINCT FROM ${expectedColumns} THEN
5786
+ RAISE EXCEPTION 'managed view ${qualified} columns do not match its declaration: expected %, got %', ${expectedColumns}, actual_columns
5787
+ USING ERRCODE = '42804';
5788
+ END IF;
5789
+ IF actual_types IS DISTINCT FROM ${expectedTypes} THEN
5790
+ RAISE EXCEPTION 'managed view ${qualified} types do not match its declaration: expected %, got %', ${expectedTypes}, actual_types
5791
+ USING ERRCODE = '42804';
5792
+ END IF;
5793
+
5794
+ SELECT coalesce(array_agg(found.name ORDER BY found.name), ARRAY[]::text[])
5795
+ INTO actual_dependencies
5796
+ FROM (
5797
+ SELECT DISTINCT dependency_namespace.nspname || '.' || dependency_relation.relname AS name
5798
+ FROM pg_catalog.pg_rewrite AS rewrite
5799
+ JOIN pg_catalog.pg_depend AS dependency
5800
+ ON dependency.classid = 'pg_catalog.pg_rewrite'::regclass
5801
+ AND dependency.objid = rewrite.oid
5802
+ AND dependency.refclassid = 'pg_catalog.pg_class'::regclass
5803
+ JOIN pg_catalog.pg_class AS dependency_relation ON dependency_relation.oid = dependency.refobjid
5804
+ JOIN pg_catalog.pg_namespace AS dependency_namespace ON dependency_namespace.oid = dependency_relation.relnamespace
5805
+ WHERE rewrite.ev_class = pg_catalog.to_regclass(${quoteLiteral(qualified)})
5806
+ AND dependency.refobjid <> rewrite.ev_class
5807
+ AND dependency_relation.relkind IN ('r', 'p', 'v', 'm', 'f')
5808
+ ) AS found;
5809
+
5810
+ IF actual_dependencies IS DISTINCT FROM ${expectedDependencies} THEN
5811
+ RAISE EXCEPTION 'managed view ${qualified} dependencies do not match its declaration: expected %, got %', ${expectedDependencies}, actual_dependencies
5812
+ USING ERRCODE = '2BP01';
5813
+ END IF;
5814
+
5815
+ IF EXISTS (
5816
+ SELECT 1
5817
+ FROM pg_catalog.pg_rewrite AS rewrite
5818
+ JOIN pg_catalog.pg_depend AS dependency
5819
+ ON dependency.classid = 'pg_catalog.pg_rewrite'::regclass
5820
+ AND dependency.objid = rewrite.oid
5821
+ AND dependency.refclassid = 'pg_catalog.pg_proc'::regclass
5822
+ JOIN pg_catalog.pg_proc AS dependency_function ON dependency_function.oid = dependency.refobjid
5823
+ JOIN pg_catalog.pg_namespace AS function_namespace ON function_namespace.oid = dependency_function.pronamespace
5824
+ WHERE rewrite.ev_class = pg_catalog.to_regclass(${quoteLiteral(qualified)})
5825
+ AND (
5826
+ function_namespace.nspname <> 'pg_catalog'
5827
+ OR dependency_function.proname <> ALL (${allowedFunctions})
5828
+ )
5829
+ ) THEN
5830
+ RAISE EXCEPTION 'managed view ${qualified} may only call explicitly allowlisted side-effect-free pg_catalog functions'
5831
+ USING ERRCODE = '42501';
5832
+ END IF;
5833
+
5834
+ IF EXISTS (
5835
+ SELECT 1
5836
+ FROM pg_catalog.pg_rewrite AS rewrite
5837
+ JOIN pg_catalog.pg_depend AS dependency
5838
+ ON dependency.classid = 'pg_catalog.pg_rewrite'::regclass
5839
+ AND dependency.objid = rewrite.oid
5840
+ AND dependency.refclassid = 'pg_catalog.pg_operator'::regclass
5841
+ JOIN pg_catalog.pg_operator AS dependency_operator ON dependency_operator.oid = dependency.refobjid
5842
+ JOIN pg_catalog.pg_namespace AS operator_namespace ON operator_namespace.oid = dependency_operator.oprnamespace
5843
+ WHERE rewrite.ev_class = pg_catalog.to_regclass(${quoteLiteral(qualified)})
5844
+ AND operator_namespace.nspname <> 'pg_catalog'
5845
+ ) THEN
5846
+ RAISE EXCEPTION 'managed view ${qualified} may only use pg_catalog operators'
5847
+ USING ERRCODE = '42501';
5848
+ END IF;
5849
+ END
5850
+ $sequence_orm_validate_view$`;
5851
+ }
5852
+ function sqlTextArray(values) {
5853
+ return values.length === 0 ? "ARRAY[]::text[]" : `ARRAY[${values.map((value) => quoteLiteral(value)).join(", ")}]::text[]`;
5854
+ }
5855
+ function viewTypeRegtype(column, schema) {
5856
+ if (typeof column.type === "string") return `${quoteLiteral(sqlTypeName(column.type))}::regtype`;
5857
+ return `${quoteLiteral(`${quoteIdentifier(schema)}.${quoteIdentifier(column.type.enum)}`)}::regtype`;
5858
+ }
4500
5859
  var previousId = (snapshot) => snapshot.id;
5860
+ function snapshotViews(snapshot) {
5861
+ const views = snapshot.views ?? {};
5862
+ return new Map(Object.values(views).map((view) => [view.name, view]));
5863
+ }
5864
+ function snapshotViewDependencies(snapshot) {
5865
+ const metadata = snapshot._sequenceOrm;
5866
+ return metadata?.viewDependencies ?? {};
5867
+ }
5868
+ function withoutViews(snapshot) {
5869
+ const clone = structuredClone(snapshot);
5870
+ clone.views = {};
5871
+ delete clone._sequenceOrm;
5872
+ return clone;
5873
+ }
5874
+ function affectedViewNames({
5875
+ previousViews,
5876
+ currentViews,
5877
+ previousDependencies,
5878
+ currentDependencies,
5879
+ baseChanged
5880
+ }) {
5881
+ const allNames = /* @__PURE__ */ new Set([...previousViews.keys(), ...currentViews.keys()]);
5882
+ if (baseChanged) return allNames;
5883
+ const affected = new Set(
5884
+ [...allNames].filter((name) => {
5885
+ const before = previousViews.get(name);
5886
+ const after = currentViews.get(name);
5887
+ return before === void 0 || after === void 0 || stableJson(before) !== stableJson(after);
5888
+ })
5889
+ );
5890
+ let grew = true;
5891
+ while (grew) {
5892
+ grew = false;
5893
+ for (const name of allNames) {
5894
+ if (affected.has(name)) continue;
5895
+ const dependencies = /* @__PURE__ */ new Set([...previousDependencies[name] ?? [], ...currentDependencies[name] ?? []]);
5896
+ if ([...dependencies].some((dependency) => affected.has(dependency))) {
5897
+ affected.add(name);
5898
+ grew = true;
5899
+ }
5900
+ }
5901
+ }
5902
+ return affected;
5903
+ }
5904
+ function topologicalViewNames(names, dependencies) {
5905
+ const remaining = new Set(names);
5906
+ const ordered = [];
5907
+ while (remaining.size > 0) {
5908
+ const ready = [...remaining].filter((name) => (dependencies[name] ?? []).every((dependency) => !remaining.has(dependency))).sort();
5909
+ if (ready.length === 0) {
5910
+ throw new OrmDefinitionError(`view dependencies form a cycle among: ${[...remaining].sort().join(", ")}`);
5911
+ }
5912
+ for (const name of ready) {
5913
+ ordered.push(name);
5914
+ remaining.delete(name);
5915
+ }
5916
+ }
5917
+ return ordered;
5918
+ }
5919
+ async function generatedViewStatements({
5920
+ snapshot,
5921
+ names,
5922
+ mode,
5923
+ schema
5924
+ }) {
5925
+ if (names.size === 0) return /* @__PURE__ */ new Map();
5926
+ const without = structuredClone(snapshot);
5927
+ const holder = without;
5928
+ const views = holder.views ?? {};
5929
+ holder.views = Object.fromEntries(Object.entries(views).filter(([, view]) => !names.has(view.name)));
5930
+ const generated = mode === "drop" ? await diffStatements(snapshot, without) : await diffStatements(without, snapshot);
5931
+ const statements = /* @__PURE__ */ new Map();
5932
+ for (const name of names) {
5933
+ const qualified = `${quoteIdentifier(schema)}.${quoteIdentifier(name)}`;
5934
+ const prefix = mode === "drop" ? "DROP VIEW" : "CREATE VIEW";
5935
+ const statement = generated.find((candidate) => candidate.trimStart().startsWith(`${prefix} ${qualified}`));
5936
+ if (statement) statements.set(name, statement.replace(/;\s*$/, ""));
5937
+ }
5938
+ return statements;
5939
+ }
5940
+ function requiredViewStatement(statements, name, schema, operation) {
5941
+ const statement = statements.get(name);
5942
+ if (!statement) {
5943
+ throw new OrmDefinitionError(`drizzle-kit did not generate ${operation} VIEW for '${schema}.${name}'`);
5944
+ }
5945
+ return statement;
5946
+ }
5947
+ function assertValidViewDependencies(canonical) {
5948
+ const views = canonical.views ?? [];
5949
+ const known = /* @__PURE__ */ new Set([...canonical.tables.map((table) => table.table), ...views.map((view) => view.view)]);
5950
+ for (const view of views) {
5951
+ for (const dependency of view.dependencies) {
5952
+ if (!known.has(dependency)) {
5953
+ throw new OrmDefinitionError(`view '${view.view}' depends on unknown relation '${dependency}'`);
5954
+ }
5955
+ }
5956
+ }
5957
+ topologicalViewNames(
5958
+ new Set(views.map((view) => view.view)),
5959
+ Object.fromEntries(views.map((view) => [view.view, view.dependencies]))
5960
+ );
5961
+ }
4501
5962
  var QUOTED_STRING_PATTERN2 = /'(?:[^']|'')*'/g;
4502
5963
  var TOKEN_PATTERN = /\bclaim\(\s*'((?:[^']|'')*)'\s*\)|\bcapability\(\s*'((?:[^']|'')*)'\s*\)|'(?:[^']|'')*'|(?<!:):([a-z][a-z0-9_]*)/g;
4503
5964
  var BARE_CALL_PATTERN = /\b(?:claim|capability)\s*\(/;
@@ -4518,8 +5979,8 @@ var actionParamArg = (name) => `p_${name}`;
4518
5979
  function compileActionStatements(action, renderParam = actionParamArg) {
4519
5980
  return actionStatementSources(action.executes).map((statement) => compileStatement(statement, renderParam));
4520
5981
  }
4521
- function compileStatement(sql3, renderParam) {
4522
- const compiled = sql3.replace(TOKEN_PATTERN, (token, claimKey, capabilityKey, paramName) => {
5982
+ function compileStatement(sql4, renderParam) {
5983
+ const compiled = sql4.replace(TOKEN_PATTERN, (token, claimKey, capabilityKey, paramName) => {
4523
5984
  if (claimKey !== void 0) {
4524
5985
  if (!CLAIM_KEY_PATTERN.test(claimKey)) throw new OrmDefinitionError(`invalid claim key in executes: ${token}`);
4525
5986
  return claimExpression(claimKey);
@@ -4572,8 +6033,8 @@ function actionPrepareChecks(definition) {
4572
6033
  const order = actionParamOrder(action);
4573
6034
  const signature = order.length > 0 ? ` (${order.map((param) => sqlTypeName(action.params[param].type)).join(", ")})` : "";
4574
6035
  const compiled = compileActionStatements(action, (param) => `$${order.indexOf(param) + 1}`);
4575
- compiled.forEach((sql3, index3) => {
4576
- const checked = action.returns && index3 === compiled.length - 1 ? returnProjection(action.returns, sql3) : sql3;
6036
+ compiled.forEach((sql4, index3) => {
6037
+ const checked = action.returns && index3 === compiled.length - 1 ? returnProjection(action.returns, sql4) : sql4;
4577
6038
  statements.push(`PREPARE ${quoteIdentifier(`orm_check_${name}_${index3}`)}${signature} AS ${checked}`);
4578
6039
  });
4579
6040
  }
@@ -4689,15 +6150,15 @@ async function planMigration({
4689
6150
  const ddl = await diffStatements(hints.adjusted, snapshot);
4690
6151
  const policyDrops = policyDropStatements(schema, snapshotTableNames(previous, schema));
4691
6152
  const statements = [
4692
- ...policyDrops.map((sql3) => ({ kind: "security", sql: sql3 })),
4693
- ...hints.statements.map((sql3) => ({ kind: "hint", sql: sql3 })),
4694
- ...ddl.map((sql3) => ({ kind: "ddl", sql: sql3.replace(/;\s*$/, "") })),
4695
- ...securityStatements(definition).map((sql3) => ({ kind: "security", sql: sql3 })),
6153
+ ...policyDrops.map((sql4) => ({ kind: "security", sql: sql4 })),
6154
+ ...hints.statements.map((sql4) => ({ kind: "hint", sql: sql4 })),
6155
+ ...ddl.map((sql4) => ({ kind: "ddl", sql: sql4.replace(/;\s*$/, "") })),
6156
+ ...securityStatements(definition).map((sql4) => ({ kind: "security", sql: sql4 })),
4696
6157
  // PREPARE each action's executes first (plpgsql bodies don't check columns
4697
6158
  // at CREATE), then create the functions. Both reference the tables the DDL
4698
6159
  // above creates, so they close the plan.
4699
- ...actionPrepareChecks(definition).map((sql3) => ({ kind: "function", sql: sql3 })),
4700
- ...actionFunctionStatements(definition).map((sql3) => ({ kind: "function", sql: sql3 }))
6160
+ ...actionPrepareChecks(definition).map((sql4) => ({ kind: "function", sql: sql4 })),
6161
+ ...actionFunctionStatements(definition).map((sql4) => ({ kind: "function", sql: sql4 }))
4701
6162
  ];
4702
6163
  return { namespace: definition.namespace, schema, statements, snapshot };
4703
6164
  }
@@ -4826,6 +6287,8 @@ function validateCapabilityRefs(files, declared) {
4826
6287
  const referenced = [
4827
6288
  ...table.policies?.read ? referencedCapabilities(table.policies.read.ast) : [],
4828
6289
  ...table.policies?.write ? referencedCapabilities(table.policies.write.ast) : [],
6290
+ ...table.policies?.insert ? referencedCapabilities(table.policies.insert.ast) : [],
6291
+ ...table.policies?.update ? referencedCapabilities(table.policies.update.ast) : [],
4829
6292
  ...Object.values(table.actions ?? {}).flatMap((action) => [
4830
6293
  ...action.requires ? referencedCapabilities(action.requires.ast) : [],
4831
6294
  ...referencedExecuteCapabilities(action)
@@ -4983,7 +6446,7 @@ async function readMigrationSql(dir, entry) {
4983
6446
  throw new OrmDefinitionError("missing migration file", path$1);
4984
6447
  });
4985
6448
  }
4986
- var checksumOf = (text3) => crypto.createHash("sha256").update(text3).digest("hex");
6449
+ var checksumOf = (text4) => crypto.createHash("sha256").update(text4).digest("hex");
4987
6450
  var migrationName = (ordinal, contentHash) => `${pad(ordinal)}_${contentHash.slice(0, 12)}`;
4988
6451
  function renderMigrationSql({
4989
6452
  name,
@@ -5009,12 +6472,12 @@ async function writeMigration({
5009
6472
  const tip = workspace.journal.entries.at(-1);
5010
6473
  const ordinal = (tip?.ordinal ?? 0) + 1;
5011
6474
  const name = migrationName(ordinal, contentHash);
5012
- const sql3 = renderMigrationSql({ name, namespace: workspace.journal.namespace, contentHash, plan });
6475
+ const sql4 = renderMigrationSql({ name, namespace: workspace.journal.namespace, contentHash, plan });
5013
6476
  const metaDir = path.join(dir, MIGRATIONS_DIRNAME, META_DIRNAME);
5014
6477
  await promises.mkdir(metaDir, { recursive: true });
5015
6478
  const snapshotText = `${JSON.stringify(plan.snapshot, null, 2)}
5016
6479
  `;
5017
- await promises.writeFile(path.join(dir, MIGRATIONS_DIRNAME, `${name}.sql`), sql3);
6480
+ await promises.writeFile(path.join(dir, MIGRATIONS_DIRNAME, `${name}.sql`), sql4);
5018
6481
  await promises.writeFile(path.join(metaDir, `${pad(ordinal)}.snapshot.json`), snapshotText);
5019
6482
  const journal = {
5020
6483
  ...workspace.journal,
@@ -5025,7 +6488,7 @@ async function writeMigration({
5025
6488
  name,
5026
6489
  content_hash: contentHash,
5027
6490
  previous_content_hash: tip?.content_hash ?? null,
5028
- checksum: checksumOf(sql3),
6491
+ checksum: checksumOf(sql4),
5029
6492
  snapshot_checksum: checksumOf(snapshotText),
5030
6493
  destructive
5031
6494
  }
@@ -5140,8 +6603,8 @@ async function checkWorkspace({
5140
6603
  let previousId3;
5141
6604
  let previousHash = null;
5142
6605
  for (const entry of workspace.journal.entries) {
5143
- const sql3 = await readMigrationSql(dir, entry);
5144
- if (checksumOf(sql3) !== entry.checksum) {
6606
+ const sql4 = await readMigrationSql(dir, entry);
6607
+ if (checksumOf(sql4) !== entry.checksum) {
5145
6608
  log(`${MIGRATIONS_DIRNAME}/${entry.name}.sql does not match its journal checksum \u2014 committed migrations must not be edited`);
5146
6609
  return 1;
5147
6610
  }
@@ -5181,7 +6644,7 @@ async function checkWorkspace({
5181
6644
  log(`migrations in sync \u2014 ${workspace.journal.entries.length} committed, tip ${tip.name}`);
5182
6645
  return 0;
5183
6646
  }
5184
- var firstLine = (sql3) => sql3.split("\n")[0] ?? sql3;
6647
+ var firstLine = (sql4) => sql4.split("\n")[0] ?? sql4;
5185
6648
 
5186
6649
  // src/migrations/from-yaml.ts
5187
6650
  function convertV1DefinitionToCanonical(definition) {
@@ -5213,7 +6676,7 @@ function convertTable(table) {
5213
6676
  description: table.description,
5214
6677
  source: table.source,
5215
6678
  columns: columns.map(([name, column]) => convertColumn(name, column)),
5216
- // Synced tables reflect their source — no columns, no primary key.
6679
+ // Synced tables may declare a typed column/PK contract; untyped remain empty.
5217
6680
  primaryKey: primaryKey2.length > 0 ? primaryKey2 : void 0,
5218
6681
  links: Object.entries(table.links ?? {}).map(([name, link]) => convertLink(table.table, name, link)),
5219
6682
  indexes: (table.indexes ?? []).map((index3) => ({
@@ -5226,9 +6689,20 @@ function convertTable(table) {
5226
6689
  read: table.policies?.read?.source,
5227
6690
  write: table.policies?.write?.source
5228
6691
  },
5229
- actions: Object.entries(table.actions ?? {}).map(([name, action]) => convertAction(table.table, name, action))
6692
+ // v2 has no action surface (writes are mutations); the transition sweep
6693
+ // drops the plpgsql action_* functions, so carrying the metadata would be
6694
+ // dead weight. droppedV1Actions() surfaces them for the emitter's TODO.
6695
+ actions: []
5230
6696
  };
5231
6697
  }
6698
+ function droppedV1Actions(definition) {
6699
+ const dropped = /* @__PURE__ */ new Map();
6700
+ for (const table of definition.tables) {
6701
+ const names = Object.keys(table.actions ?? {});
6702
+ if (names.length > 0) dropped.set(table.table, names);
6703
+ }
6704
+ return dropped;
6705
+ }
5232
6706
  function convertColumn(name, column) {
5233
6707
  const isDefaultFn = column.default !== void 0 && isFunctionDefault(column.default, column.type);
5234
6708
  return {
@@ -5262,25 +6736,6 @@ function convertLink(table, name, link) {
5262
6736
  description: link.description
5263
6737
  };
5264
6738
  }
5265
- function convertAction(table, name, action) {
5266
- return {
5267
- name,
5268
- requires: action.requires?.source,
5269
- params: Object.fromEntries(
5270
- Object.entries(action.params ?? {}).map(([param, definition]) => [
5271
- param,
5272
- { type: definition.type, nullable: definition.optional === true }
5273
- ])
5274
- ),
5275
- // The v2 contract carries one SQL string; statement boundaries survive as
5276
- // `;\n` (v1 validation guarantees no bare `;` inside a single statement).
5277
- executes: actionStatementSources(action.executes).join(";\n"),
5278
- // v1 declared the RETURNING columns inline; v2 types them at compile time,
5279
- // so the shape collapses to rows-vs-rowsAffected.
5280
- returns: action.returns ? "rows" : "rowsAffected",
5281
- audit: action.audit ? { action: action.audit.action, resource: action.audit.resource ?? table } : void 0
5282
- };
5283
- }
5284
6739
 
5285
6740
  // src/migrations/v2.ts
5286
6741
  var SHA256_HEX = /^[0-9a-f]{64}$/;
@@ -5303,7 +6758,7 @@ function buildRebaseJournalEntry({ namespace, previousHash, newHash, ordinal, sn
5303
6758
  throw new OrmDefinitionError(`rebase ordinal must be an integer \u2265 2 (a rebase sits on an existing journal tip); got ${ordinal}`);
5304
6759
  }
5305
6760
  const name = migrationName(ordinal, newHash);
5306
- const sql3 = renderRebaseMigrationSql({ name, namespace, previousHash, newHash });
6761
+ const sql4 = renderRebaseMigrationSql({ name, namespace, previousHash, newHash });
5307
6762
  const snapshotText = `${JSON.stringify(snapshot, null, 2)}
5308
6763
  `;
5309
6764
  const entry = {
@@ -5311,11 +6766,11 @@ function buildRebaseJournalEntry({ namespace, previousHash, newHash, ordinal, sn
5311
6766
  name,
5312
6767
  content_hash: newHash,
5313
6768
  previous_content_hash: previousHash,
5314
- checksum: checksumOf(sql3),
6769
+ checksum: checksumOf(sql4),
5315
6770
  snapshot_checksum: checksumOf(snapshotText),
5316
6771
  destructive: false
5317
6772
  };
5318
- return { name, entry, sql: sql3, snapshotText };
6773
+ return { name, entry, sql: sql4, snapshotText };
5319
6774
  }
5320
6775
  function renderRebaseMigrationSql({
5321
6776
  name,
@@ -5353,15 +6808,15 @@ var PLATFORM_USER_FIELDS = [
5353
6808
  ];
5354
6809
  function generateSdl(canonical, options = {}) {
5355
6810
  const def = normalizeCanonical(canonicalDefinitionSchema.parse(canonical));
5356
- const tables = def.tables.filter((table) => table.source === void 0);
6811
+ const tables = canonicalRelations(def).filter((table) => table.source === void 0);
5357
6812
  if (tables.length === 0) {
5358
6813
  throw new SdlGenerationError(
5359
- `Namespace "${def.namespace}" exposes no tables (synced tables are excluded from SDL in v2.0) \u2014 nothing to generate.`
6814
+ `Namespace "${def.namespace}" exposes no relations (synced tables are excluded from SDL in v2.0) \u2014 nothing to generate.`
5360
6815
  );
5361
6816
  }
5362
6817
  for (const table of tables) {
5363
6818
  if (table.columns.length === 0) {
5364
- throw new SdlGenerationError(`Table "${table.table}" has no columns \u2014 cannot generate an object type.`);
6819
+ throw new SdlGenerationError(`Relation "${table.table}" has no columns \u2014 cannot generate an object type.`);
5365
6820
  }
5366
6821
  }
5367
6822
  const namesByTable = new Map(tables.map((table) => [table.table, tableNames(table.table)]));
@@ -5455,8 +6910,8 @@ function renderEnum(name, values) {
5455
6910
  ${values.map((value) => ` ${value}`).join("\n")}
5456
6911
  }`;
5457
6912
  }
5458
- function descriptionLines(text3, indent) {
5459
- const escaped = text3.replace(/"""/g, '\\"""');
6913
+ function descriptionLines(text4, indent) {
6914
+ const escaped = text4.replace(/"""/g, '\\"""');
5460
6915
  return [`${indent}"""`, ...escaped.split("\n").map((line) => `${indent}${line}`), `${indent}"""`];
5461
6916
  }
5462
6917
  function renderComparisonExp(gqlTypeName) {
@@ -5541,10 +6996,20 @@ function renderTableBlocks(ctx) {
5541
6996
  ${fields}
5542
6997
  }`);
5543
6998
  }
5544
- if (isWritable(table)) blocks.push(...renderWriteBlocks(ctx));
6999
+ if (allowsInsert(table) || allowsUpdate(table)) {
7000
+ blocks.push(...renderWriteBlocks(ctx, { insert: allowsInsert(table), update: allowsUpdate(table) }));
7001
+ }
5545
7002
  return blocks;
5546
7003
  }
5547
- function isWritable(table) {
7004
+ function allowsInsert(table) {
7005
+ return table.policies.write !== void 0 || table.policies.insert !== void 0;
7006
+ }
7007
+ function allowsUpdate(table) {
7008
+ return table.policies.write !== void 0 || table.policies.update !== void 0;
7009
+ }
7010
+ function allowsOnConflict(table) {
7011
+ if (!allowsInsert(table) || !allowsUpdate(table)) return false;
7012
+ if (table.policies.insert !== void 0 || table.policies.update !== void 0) return false;
5548
7013
  return table.policies.write !== void 0;
5549
7014
  }
5550
7015
  function renderObjectType(ctx) {
@@ -5676,46 +7141,50 @@ function constraintValues(table) {
5676
7141
  const values = /* @__PURE__ */ new Set();
5677
7142
  if (table.primaryKey && table.primaryKey.length > 0) values.add(`${table.table}_pkey`);
5678
7143
  for (const column of table.columns) {
5679
- if (column.unique) values.add(`${table.table}_${column.columnName}_key`);
7144
+ if (column.unique) values.add(`${table.table}_${column.columnName}_unique`);
5680
7145
  }
5681
7146
  for (const index3 of table.indexes) {
5682
7147
  if (index3.unique) values.add(index3.name ?? `${table.table}_${index3.columns.join("_")}_key`);
5683
7148
  }
5684
7149
  return [...values].sort();
5685
7150
  }
5686
- function renderWriteBlocks(ctx) {
7151
+ function renderWriteBlocks(ctx, access) {
5687
7152
  const { table, names, pinned } = ctx;
5688
7153
  const blocks = [];
5689
- const pinnedSet = new Set(pinned);
5690
- const insertable = table.columns.filter(
5691
- (column) => !pinnedSet.has(column.name) && !pinnedSet.has(column.columnName)
5692
- );
5693
- if (insertable.length === 0) {
5694
- throw new SdlGenerationError(
5695
- `Table "${table.table}" has no insertable columns after excluding pinned columns [${pinned.join(", ")}].`
7154
+ if (access.insert) {
7155
+ const pinnedSet = new Set(pinned);
7156
+ const insertable = table.columns.filter(
7157
+ (column) => !pinnedSet.has(column.name) && !pinnedSet.has(column.columnName)
5696
7158
  );
5697
- }
5698
- const insertLines = insertable.map(
5699
- (column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
5700
- );
5701
- blocks.push(`input ${names.insertInput} {
7159
+ if (insertable.length === 0) {
7160
+ throw new SdlGenerationError(
7161
+ `Table "${table.table}" has no insertable columns after excluding pinned columns [${pinned.join(", ")}].`
7162
+ );
7163
+ }
7164
+ const insertLines = insertable.map(
7165
+ (column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
7166
+ );
7167
+ blocks.push(`input ${names.insertInput} {
5702
7168
  ${insertLines.join("\n")}
5703
7169
  }`);
5704
- const setLines = table.columns.map(
5705
- (column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
5706
- );
5707
- blocks.push(`input ${names.setInput} {
7170
+ }
7171
+ if (access.update) {
7172
+ const setLines = table.columns.map(
7173
+ (column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
7174
+ );
7175
+ blocks.push(`input ${names.setInput} {
5708
7176
  ${setLines.join("\n")}
5709
7177
  }`);
5710
- const jsonb3 = jsonbColumns(table);
5711
- if (jsonb3.length > 0) {
5712
- const appendLines = jsonb3.map((column) => ` ${fieldName(column.name)}: jsonb`);
7178
+ }
7179
+ const jsonb4 = jsonbColumns(table);
7180
+ if (access.update && jsonb4.length > 0) {
7181
+ const appendLines = jsonb4.map((column) => ` ${fieldName(column.name)}: jsonb`);
5713
7182
  blocks.push(`input ${names.appendInput} {
5714
7183
  ${appendLines.join("\n")}
5715
7184
  }`);
5716
7185
  }
5717
7186
  const numericish = numericishColumns(table);
5718
- if (numericish.length > 0) {
7187
+ if (access.update && numericish.length > 0) {
5719
7188
  const incLines = numericish.map(
5720
7189
  (column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
5721
7190
  );
@@ -5724,7 +7193,7 @@ ${incLines.join("\n")}
5724
7193
  }`);
5725
7194
  }
5726
7195
  const constraints = constraintValues(table);
5727
- if (constraints.length > 0) {
7196
+ if (access.insert && allowsOnConflict(table) && constraints.length > 0) {
5728
7197
  for (const value of constraints) assertEnumValue(value, `constraint enum for table "${table.table}"`);
5729
7198
  blocks.push(renderEnum(names.constraintEnum, constraints));
5730
7199
  const updateColumns = table.columns.map((column) => fieldName(column.name));
@@ -5737,12 +7206,14 @@ ${incLines.join("\n")}
5737
7206
  }`
5738
7207
  );
5739
7208
  }
5740
- blocks.push(
5741
- `type ${names.mutationResponse} {
7209
+ if (access.insert || access.update) {
7210
+ blocks.push(
7211
+ `type ${names.mutationResponse} {
5742
7212
  affectedRows: Int!
5743
7213
  returning: [${names.typename}!]!
5744
7214
  }`
5745
- );
7215
+ );
7216
+ }
5746
7217
  return blocks;
5747
7218
  }
5748
7219
  function renderQueryRoot(tables, namesByTable) {
@@ -5767,35 +7238,64 @@ function columnGqlTypeOf(table, column) {
5767
7238
  function renderMutationRoot(tables, namesByTable) {
5768
7239
  const lines = [];
5769
7240
  for (const table of tables) {
5770
- if (!isWritable(table)) continue;
7241
+ const canInsert = allowsInsert(table);
7242
+ const canUpdate = allowsUpdate(table);
7243
+ if (!canInsert && !canUpdate) continue;
5771
7244
  const names = namesByTable.get(table.table);
5772
7245
  if (!names) continue;
5773
7246
  const constraints = constraintValues(table);
5774
- const onConflictArg = constraints.length > 0 ? `, onConflict: ${names.onConflict}` : "";
7247
+ const onConflictArg = canInsert && allowsOnConflict(table) && constraints.length > 0 ? `, onConflict: ${names.onConflict}` : "";
5775
7248
  const updateArgs = [`_set: ${names.setInput}`];
5776
7249
  if (jsonbColumns(table).length > 0) updateArgs.push(`_append: ${names.appendInput}`);
5777
7250
  if (numericishColumns(table).length > 0) updateArgs.push(`_inc: ${names.incInput}`);
5778
7251
  const updateArgList = updateArgs.join(", ");
5779
- lines.push(` ${names.insertRoot}(objects: [${names.insertInput}!]!${onConflictArg}): ${names.mutationResponse}`);
5780
- lines.push(` ${names.insertOneRoot}(object: ${names.insertInput}!${onConflictArg}): ${names.typename}`);
5781
- lines.push(` ${names.updateRoot}(where: ${names.boolExp}!, ${updateArgList}): ${names.mutationResponse}`);
5782
- const pk = primaryKeyColumns(table);
5783
- if (pk.length > 0) {
5784
- lines.push(
5785
- ` ${names.updateByPkRoot}(pkColumns: ${names.pkColumnsInput}!, ${updateArgList}): ${names.typename}`
5786
- );
5787
- }
5788
- lines.push(` ${names.deleteRoot}(where: ${names.boolExp}!): ${names.mutationResponse}`);
5789
- if (pk.length > 0) {
5790
- const args = pk.map((column) => `${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}!`).join(", ");
5791
- lines.push(` ${names.deleteByPkRoot}(${args}): ${names.typename}`);
7252
+ if (canInsert) {
7253
+ lines.push(` ${names.insertRoot}(objects: [${names.insertInput}!]!${onConflictArg}): ${names.mutationResponse}`);
7254
+ lines.push(` ${names.insertOneRoot}(object: ${names.insertInput}!${onConflictArg}): ${names.typename}`);
7255
+ }
7256
+ if (canUpdate) {
7257
+ lines.push(` ${names.updateRoot}(where: ${names.boolExp}!, ${updateArgList}): ${names.mutationResponse}`);
7258
+ const pk = primaryKeyColumns(table);
7259
+ if (pk.length > 0) {
7260
+ lines.push(
7261
+ ` ${names.updateByPkRoot}(pkColumns: ${names.pkColumnsInput}!, ${updateArgList}): ${names.typename}`
7262
+ );
7263
+ }
7264
+ lines.push(` ${names.deleteRoot}(where: ${names.boolExp}!): ${names.mutationResponse}`);
7265
+ if (pk.length > 0) {
7266
+ const args = pk.map((column) => `${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}!`).join(", ");
7267
+ lines.push(` ${names.deleteByPkRoot}(${args}): ${names.typename}`);
7268
+ }
5792
7269
  }
7270
+ lines.push(...renderCowMutationFields(table, names));
5793
7271
  }
5794
7272
  if (lines.length === 0) return null;
5795
7273
  return `type Mutation {
5796
7274
  ${lines.join("\n")}
5797
7275
  }`;
5798
7276
  }
7277
+ function renderCowMutationFields(table, names) {
7278
+ const lines = [];
7279
+ const columnByName = new Map(table.columns.map((column) => [column.name, column]));
7280
+ for (const mutation of table.cowMutations ?? []) {
7281
+ const pkColumn = columnByName.get(mutation.pk);
7282
+ if (!pkColumn) continue;
7283
+ const pkArg = `${fieldName(mutation.pk)}: ${columnGqlTypeOf(table, pkColumn)}!`;
7284
+ if (mutation.kind === "revert") {
7285
+ lines.push(` ${fieldName(mutation.name)}(${pkArg}): ${names.typename}`);
7286
+ continue;
7287
+ }
7288
+ const setArgs = mutation.set.map((property) => {
7289
+ const column = columnByName.get(property);
7290
+ if (!column) return null;
7291
+ const bang = column.nullable === false ? "!" : "";
7292
+ return `${fieldName(property)}: ${columnGqlTypeOf(table, column)}${bang}`;
7293
+ });
7294
+ if (setArgs.some((arg) => arg === null)) continue;
7295
+ lines.push(` ${fieldName(mutation.name)}(${pkArg}, ${setArgs.join(", ")}): ${names.typename}`);
7296
+ }
7297
+ return lines;
7298
+ }
5799
7299
 
5800
7300
  // src/cli.ts
5801
7301
  var BASE_URL_ENV = "SEQUENCE_ORM_BASE_URL";
@@ -5809,6 +7309,8 @@ var USAGE = `usage:
5809
7309
  seq-studio orm apply [dir] -e <env> author the migration, register, apply, and refresh the generated outputs (the everyday command)
5810
7310
  seq-studio orm apply --dry-run -e <env> rehearse the migration on a copy of the environment's real data, then discard it (no real apply)
5811
7311
  seq-studio orm apply --no-create -e <env> apply only if the namespace is already registered (CI guard; never provisions a new namespace)
7312
+ seq-studio orm execute <operation> [dir] --variables-file <file> -e <env>
7313
+ execute one named persisted operation as the signed-in caller
5812
7314
  seq-studio orm diff [dir] write/verify the committed migration standalone (--check is the offline CI gate; --allow-destructive consents; --rebase records a v1 -> v2 conversion as a verified no-op)
5813
7315
  seq-studio orm migrate-from-yaml <dir> convert a v1 YAML namespace to the v2 TypeScript format (migrations/ preserved)
5814
7316
 
@@ -5832,6 +7334,10 @@ async function runCli(argv) {
5832
7334
  console.log(USAGE);
5833
7335
  return command ? 0 : 1;
5834
7336
  }
7337
+ if (rest.includes("--help") || rest.includes("-h")) {
7338
+ console.log(USAGE);
7339
+ return 0;
7340
+ }
5835
7341
  try {
5836
7342
  switch (command) {
5837
7343
  case "init":
@@ -5869,6 +7375,8 @@ async function runCli(argv) {
5869
7375
  const dir = positional(rest) ?? ".";
5870
7376
  return await detectProjectFormat(dir) === "v2" ? await applyV2Command(rest, dir) : await applyCommand(rest);
5871
7377
  }
7378
+ case "execute":
7379
+ return await executeV2Command(rest);
5872
7380
  default:
5873
7381
  console.error(`unknown command: ${command}`);
5874
7382
  console.log(USAGE);
@@ -5879,13 +7387,14 @@ async function runCli(argv) {
5879
7387
  return 1;
5880
7388
  }
5881
7389
  }
7390
+ var NON_DEFINITION_YAML_FILES = /* @__PURE__ */ new Set(["pnpm-lock.yaml", "pnpm-workspace.yaml"]);
5882
7391
  async function detectProjectFormat(dir) {
5883
7392
  const entries = await promises.readdir(dir, { withFileTypes: true }).catch(() => {
5884
7393
  throw new OrmDefinitionError(`namespace directory not found: ${dir}`);
5885
7394
  });
5886
7395
  const hasConfig = entries.some((entry) => entry.isFile() && entry.name === NAMESPACE_CONFIG_FILE);
5887
7396
  const hasYaml = entries.some(
5888
- (entry) => entry.isFile() && DEFINITION_FILE_EXTENSIONS.some((extension) => entry.name.endsWith(extension))
7397
+ (entry) => entry.isFile() && !NON_DEFINITION_YAML_FILES.has(entry.name) && DEFINITION_FILE_EXTENSIONS.some((extension) => entry.name.endsWith(extension))
5889
7398
  );
5890
7399
  if (hasConfig && hasYaml) {
5891
7400
  throw new OrmDefinitionError(
@@ -6033,7 +7542,7 @@ async function generateCommand(rest) {
6033
7542
  await writeGenerateOutputs(dir, compiled.project);
6034
7543
  const { loaded, manifest } = compiled.project;
6035
7544
  console.log(
6036
- `wrote ${SCHEMA_GRAPHQL_FILE}, ${OPERATIONS_MANIFEST_FILE}, ${TYPE_POLICIES_FILE} (${loaded.canonical.tables.length} table(s), ${Object.keys(manifest.operations).length} operation(s))`
7545
+ `wrote ${SCHEMA_GRAPHQL_FILE}, ${OPERATIONS_MANIFEST_FILE}, ${TYPE_POLICIES_FILE} (${loaded.canonical.tables.length} table(s), ${loaded.canonical.views?.length ?? 0} view(s), ${Object.keys(manifest.operations).length} operation(s))`
6037
7546
  );
6038
7547
  return runConsumerCodegen(dir);
6039
7548
  }
@@ -6080,13 +7589,86 @@ async function runConsumerCodegen(dir) {
6080
7589
  console.log(`ran consumer graphql-codegen (${CODEGEN_CONFIG_FILE})`);
6081
7590
  return 0;
6082
7591
  }
7592
+ function isVariablesObject(value) {
7593
+ return value !== null && typeof value === "object" && !Array.isArray(value);
7594
+ }
7595
+ function parseExecuteArgs(rest) {
7596
+ const args = [...rest];
7597
+ const variablesIndex = args.indexOf("--variables-file");
7598
+ if (variablesIndex === -1) {
7599
+ throw new OrmDefinitionError("execute requires --variables-file <file>");
7600
+ }
7601
+ const variablesFile = args[variablesIndex + 1];
7602
+ if (!variablesFile) {
7603
+ throw new OrmDefinitionError("execute requires --variables-file <file>");
7604
+ }
7605
+ args.splice(variablesIndex, 2);
7606
+ const envIndex = args.indexOf("--env");
7607
+ if (envIndex !== -1) args.splice(envIndex, 2);
7608
+ if (args.some((arg) => arg.startsWith("-"))) {
7609
+ throw new OrmDefinitionError("execute accepts only <operation>, [dir], --variables-file, and --env");
7610
+ }
7611
+ const operation = args[0];
7612
+ if (!operation || args.length > 2) {
7613
+ throw new OrmDefinitionError("usage: seq-studio orm execute <operation> [dir] --variables-file <file> -e <env>");
7614
+ }
7615
+ return {
7616
+ operation,
7617
+ dir: args[1] ?? ".",
7618
+ variablesFile
7619
+ };
7620
+ }
7621
+ async function readExecuteVariables(variablesFile) {
7622
+ let parsed;
7623
+ try {
7624
+ parsed = JSON.parse(await promises.readFile(path.resolve(variablesFile), "utf8"));
7625
+ } catch (error) {
7626
+ throw new OrmDefinitionError(
7627
+ `could not read JSON variables from ${variablesFile}: ${error instanceof Error ? error.message : String(error)}`
7628
+ );
7629
+ }
7630
+ if (!isVariablesObject(parsed)) {
7631
+ throw new OrmDefinitionError("--variables-file must contain a JSON object");
7632
+ }
7633
+ return parsed;
7634
+ }
7635
+ async function executeV2Command(rest) {
7636
+ const { dir, operation, variablesFile } = parseExecuteArgs(rest);
7637
+ if (await detectProjectFormat(dir) !== "v2") {
7638
+ throw new OrmDefinitionError("execute supports TypeScript ORM v2 namespaces only");
7639
+ }
7640
+ const loaded = await loadV2(dir);
7641
+ const manifestPath = path.join(dir, OPERATIONS_MANIFEST_FILE);
7642
+ let manifest;
7643
+ try {
7644
+ manifest = operationsManifestSchema.parse(JSON.parse(await promises.readFile(manifestPath, "utf8")));
7645
+ } catch (error) {
7646
+ throw new OrmDefinitionError(
7647
+ `could not read ${OPERATIONS_MANIFEST_FILE}: ${error instanceof Error ? error.message : String(error)} \u2014 run \`seq-studio orm generate ${dir}\` first`
7648
+ );
7649
+ }
7650
+ const selected = manifest.operations[operation];
7651
+ if (!selected) {
7652
+ throw new OrmDefinitionError(
7653
+ `operation "${operation}" is not in ${OPERATIONS_MANIFEST_FILE}; run \`seq-studio orm generate ${dir}\` after editing GraphQL documents`
7654
+ );
7655
+ }
7656
+ const variables = await readExecuteVariables(variablesFile);
7657
+ const response = await apiRequestStrict(
7658
+ "POST",
7659
+ `/_data/v2/${loaded.canonical.namespace}`,
7660
+ { op: selected.hash, variables }
7661
+ );
7662
+ console.log(JSON.stringify(response, null, 2));
7663
+ return 0;
7664
+ }
6083
7665
  async function validateV2Command(dir) {
6084
7666
  const compiled = await compileV2Project(dir);
6085
7667
  printDiagnostics(compiled.ok ? compiled.project.diagnostics : compiled.diagnostics);
6086
7668
  if (!compiled.ok) return 1;
6087
7669
  const { loaded, manifest } = compiled.project;
6088
7670
  console.log(
6089
- `namespace ${loaded.canonical.namespace}: ${loaded.canonical.tables.length} table(s), ${Object.keys(manifest.operations).length} operation(s) valid`
7671
+ `namespace ${loaded.canonical.namespace}: ${loaded.canonical.tables.length} table(s), ${loaded.canonical.views?.length ?? 0} view(s), ${Object.keys(manifest.operations).length} operation(s) valid`
6090
7672
  );
6091
7673
  console.log(`content hash ${loaded.hash}`);
6092
7674
  const workspace = await readMigrationWorkspace(dir, loaded.canonical.namespace);
@@ -6216,8 +7798,8 @@ async function checkWorkspaceV2({
6216
7798
  let previousId3;
6217
7799
  let previousHash = null;
6218
7800
  for (const entry of workspace.journal.entries) {
6219
- const sql3 = await readMigrationSql(dir, entry);
6220
- if (checksumOf(sql3) !== entry.checksum) {
7801
+ const sql4 = await readMigrationSql(dir, entry);
7802
+ if (checksumOf(sql4) !== entry.checksum) {
6221
7803
  log(`${MIGRATIONS_DIRNAME}/${entry.name}.sql does not match its journal checksum \u2014 committed migrations must not be edited`);
6222
7804
  return 1;
6223
7805
  }
@@ -6404,9 +7986,15 @@ async function migrateFromYamlCommand(rest) {
6404
7986
  }
6405
7987
  const v1 = await loadNamespaceDefinition(dir);
6406
7988
  const canonical = convertV1DefinitionToCanonical(v1.definition);
7989
+ const droppedActions = droppedV1Actions(v1.definition);
6407
7990
  await writeConverted(path.join(dir, NAMESPACE_CONFIG_FILE), emitSequenceConfig(canonical));
6408
7991
  await promises.mkdir(path.join(dir, SCHEMA_DIR), { recursive: true });
6409
- await writeConverted(path.join(dir, SCHEMA_DIR, "tables.ts"), emitSchemaTables(canonical));
7992
+ await writeConverted(path.join(dir, SCHEMA_DIR, "tables.ts"), emitSchemaTables(canonical, droppedActions));
7993
+ for (const [table, names] of droppedActions) {
7994
+ console.log(
7995
+ `note: v1 actions on '${table}' were dropped (${names.join(", ")}) \u2014 v2 has no action surface; re-author them as persisted mutations (see the TODO in schema/tables.ts)`
7996
+ );
7997
+ }
6410
7998
  const reloaded = await loadV2(dir);
6411
7999
  if (JSON.stringify(sortKeys(reloaded.canonical)) !== JSON.stringify(sortKeys(canonical))) {
6412
8000
  console.error(
@@ -6470,14 +8058,15 @@ function emitSequenceConfig(canonical) {
6470
8058
  lines.push("})", "");
6471
8059
  return lines.join("\n");
6472
8060
  }
6473
- function emitSchemaTables(canonical) {
8061
+ function emitSchemaTables(canonical, droppedActions) {
6474
8062
  const columnTables2 = canonical.tables.filter((table) => table.source === void 0);
6475
8063
  const syncedTables = canonical.tables.filter((table) => table.source !== void 0);
6476
8064
  const hasLinks = canonical.tables.some((table) => table.links.length > 0);
8065
+ const syncedHasColumns = syncedTables.some((table) => table.columns.length > 0);
6477
8066
  const imports = [
6478
8067
  ...columnTables2.length > 0 ? ["defineTable"] : [],
6479
8068
  ...syncedTables.length > 0 ? ["defineSyncedTable"] : [],
6480
- ...columnTables2.length > 0 ? ["t"] : [],
8069
+ ...columnTables2.length > 0 || syncedHasColumns ? ["t"] : [],
6481
8070
  ...hasLinks ? ["toOne"] : []
6482
8071
  ].sort((a, b) => a.localeCompare(b));
6483
8072
  const parts = [
@@ -6491,18 +8080,62 @@ function emitSchemaTables(canonical) {
6491
8080
  ""
6492
8081
  ];
6493
8082
  for (const table of canonical.tables) {
8083
+ const dropped = droppedActions?.get(table.table) ?? [];
8084
+ if (dropped.length > 0) {
8085
+ parts.push(
8086
+ `// TODO: v1 actions dropped by the conversion \u2014 v2 has no action surface (writes`,
8087
+ `// are persisted mutations). Re-author for '${table.table}': ${dropped.join(", ")}`
8088
+ );
8089
+ }
6494
8090
  parts.push(table.source !== void 0 ? emitSyncedTable(table) : emitTable(table));
6495
8091
  }
6496
8092
  return parts.join("\n");
6497
8093
  }
6498
8094
  function emitSyncedTable(table) {
8095
+ const singlePrimaryKey = table.primaryKey?.length === 1 ? table.primaryKey[0] : void 0;
8096
+ if (table.columns.length === 0) {
8097
+ const lines2 = [
8098
+ `export const ${tableVarName(table.table)} = defineSyncedTable(${quote(table.table)}, {`,
8099
+ ` source: ${quote(table.source ?? "")},`
8100
+ ];
8101
+ if (table.description !== void 0) lines2.push(` description: ${quote(table.description)},`);
8102
+ if (table.policies.read !== void 0) {
8103
+ lines2.push(" policies: {", ` read: ${policyLiteral(table.policies.read)},`, " },");
8104
+ }
8105
+ lines2.push("})", "");
8106
+ return lines2.join("\n");
8107
+ }
6499
8108
  const lines = [
6500
- `export const ${tableVarName(table.table)} = defineSyncedTable(${quote(table.table)}, {`,
6501
- // `source` is guaranteed by the caller's filter.
6502
- ` source: ${quote(table.source ?? "")},`
8109
+ `export const ${tableVarName(table.table)} = defineSyncedTable(`,
8110
+ ` ${quote(table.table)},`,
8111
+ " {"
6503
8112
  ];
6504
- if (table.description !== void 0) lines.push(` description: ${quote(table.description)},`);
6505
- lines.push("})", "");
8113
+ for (const column of table.columns) lines.push(` ${columnProperty(table.table, column, singlePrimaryKey)}`);
8114
+ lines.push(" },", " {");
8115
+ lines.push(` source: ${quote(table.source ?? "")},`);
8116
+ if (table.description !== void 0) lines.push(` description: ${quote(table.description)},`);
8117
+ if (singlePrimaryKey === void 0 && table.primaryKey && table.primaryKey.length > 0) {
8118
+ lines.push(` primaryKey: ${jsonLiteral(table.primaryKey, 4)},`);
8119
+ }
8120
+ if (table.links.length > 0) {
8121
+ lines.push(" links: {");
8122
+ for (const link of table.links) {
8123
+ if (link.kind !== "toOne" || link.target === void 0) {
8124
+ throw new OrmDefinitionError(
8125
+ `link '${link.name}' on synced '${table.table}' is not a toOne link \u2014 not a v1 conversion output`
8126
+ );
8127
+ }
8128
+ const options = [`on: ${quote(link.on)}`];
8129
+ if (link.references !== void 0) options.push(`references: ${quote(link.references)}`);
8130
+ if (link.description !== void 0) options.push(`description: ${quote(link.description)}`);
8131
+ lines.push(` ${objectKey(link.name)}: toOne(() => ${tableVarName(link.target)}, { ${options.join(", ")} }),`);
8132
+ }
8133
+ lines.push(" },");
8134
+ }
8135
+ if (table.policies.read !== void 0) {
8136
+ lines.push(" policies: {", ` read: ${policyLiteral(table.policies.read)},`, " },");
8137
+ }
8138
+ lines.push(" },", ")", "");
6506
8139
  return lines.join("\n");
6507
8140
  }
6508
8141
  function emitTable(table) {
@@ -6572,7 +8205,6 @@ function emitTableConfig(table, singlePrimaryKey) {
6572
8205
  if (table.policies.write !== void 0) lines.push(` write: ${policyLiteral(table.policies.write)},`);
6573
8206
  lines.push(" },");
6574
8207
  }
6575
- if (table.actions.length > 0) lines.push(` actions: ${jsonLiteral(table.actions, 4)},`);
6576
8208
  return lines;
6577
8209
  }
6578
8210
  function policyLiteral(policy) {
@@ -6873,7 +8505,7 @@ function positional(rest) {
6873
8505
  if (envIndex !== -1) args.splice(envIndex, 2);
6874
8506
  return args.find((arg) => !arg.startsWith("-"));
6875
8507
  }
6876
- var firstLine2 = (sql3) => sql3.split("\n")[0] ?? sql3;
8508
+ var firstLine2 = (sql4) => sql4.split("\n")[0] ?? sql4;
6877
8509
 
6878
8510
  exports.BASE_URL_ENV = BASE_URL_ENV;
6879
8511
  exports.ORM_MODULE_PATH_ENV = ORM_MODULE_PATH_ENV;