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