@sequenceholdings/orm 0.1.2 → 0.1.3

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 (65) hide show
  1. package/README.md +11 -1
  2. package/dist/authoring.cjs +1066 -59
  3. package/dist/authoring.cjs.map +1 -1
  4. package/dist/authoring.d.cts +289 -24
  5. package/dist/authoring.d.ts +289 -24
  6. package/dist/authoring.js +1056 -60
  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 +1764 -225
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.js +1765 -226
  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 +506 -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 +504 -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 +2223 -219
  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 +2199 -220
  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 +1093 -0
  53. package/dist/predicate-v2.cjs.map +1 -0
  54. package/dist/predicate-v2.d.cts +139 -0
  55. package/dist/predicate-v2.d.ts +139 -0
  56. package/dist/predicate-v2.js +1083 -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 +16 -1
  65. package/templates/orm-namespace/CLAUDE.md +6 -5
package/dist/cli.cjs CHANGED
@@ -7,9 +7,9 @@ var url = require('url');
7
7
  var jiti = require('jiti');
8
8
  var crypto = require('crypto');
9
9
  var zod = require('zod');
10
- var graphql = require('graphql');
11
10
  var drizzleOrm = require('drizzle-orm');
12
11
  var pgCore = require('drizzle-orm/pg-core');
12
+ var graphql = require('graphql');
13
13
  var api = require('drizzle-kit/api');
14
14
  var jsYaml = require('js-yaml');
15
15
  var customRolesManifest = require('@sequenceholdings/custom-roles-manifest');
@@ -111,8 +111,190 @@ var policyExpressionSchema = zod.z.union([
111
111
  ]);
112
112
  var canonicalPoliciesSchema = zod.z.object({
113
113
  read: policyExpressionSchema.optional(),
114
- write: policyExpressionSchema.optional()
114
+ write: policyExpressionSchema.optional(),
115
+ insert: policyExpressionSchema.optional(),
116
+ update: policyExpressionSchema.optional()
115
117
  });
118
+ function isSingleViewQuerySql(value) {
119
+ const query = value.trim();
120
+ if (!/^\(*(?:select|with)\b/i.test(query) || query.includes("\0")) return false;
121
+ let state = "normal";
122
+ for (let index3 = 0; index3 < query.length; index3 += 1) {
123
+ const char = query[index3];
124
+ const next = query[index3 + 1];
125
+ if (state === "single") {
126
+ if (char === "'" && next === "'") index3 += 1;
127
+ else if (char === "'") state = "normal";
128
+ continue;
129
+ }
130
+ if (state === "double") {
131
+ if (char === '"' && next === '"') index3 += 1;
132
+ else if (char === '"') state = "normal";
133
+ continue;
134
+ }
135
+ if (char === "'") state = "single";
136
+ else if (char === '"') state = "double";
137
+ else if (char === ";" || char === "-" && next === "-" || char === "/" && next === "*") return false;
138
+ }
139
+ return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
140
+ }
141
+ var VIEW_QUERY_FUNCTION_ALLOWLIST = [
142
+ "abs",
143
+ "age",
144
+ "array_agg",
145
+ "array_append",
146
+ "array_cat",
147
+ "array_dims",
148
+ "array_length",
149
+ "array_lower",
150
+ "array_ndims",
151
+ "array_position",
152
+ "array_positions",
153
+ "array_prepend",
154
+ "array_remove",
155
+ "array_replace",
156
+ "array_to_string",
157
+ "array_trim",
158
+ "array_upper",
159
+ "ascii",
160
+ "avg",
161
+ "bit_and",
162
+ "bit_or",
163
+ "bit_xor",
164
+ "bool_and",
165
+ "bool_or",
166
+ "btrim",
167
+ "cardinality",
168
+ "cbrt",
169
+ "ceil",
170
+ "ceiling",
171
+ "char_length",
172
+ "character_length",
173
+ "chr",
174
+ "concat",
175
+ "concat_ws",
176
+ "count",
177
+ "cume_dist",
178
+ "date_bin",
179
+ "date_part",
180
+ "date_trunc",
181
+ "decode",
182
+ "degrees",
183
+ "dense_rank",
184
+ "encode",
185
+ "enum_first",
186
+ "enum_last",
187
+ "enum_range",
188
+ "every",
189
+ "exp",
190
+ "first_value",
191
+ "floor",
192
+ "format",
193
+ "grouping",
194
+ "initcap",
195
+ "json_agg",
196
+ "json_array_length",
197
+ "json_build_array",
198
+ "json_build_object",
199
+ "json_extract_path",
200
+ "json_extract_path_text",
201
+ "json_object_agg",
202
+ "json_object_keys",
203
+ "json_strip_nulls",
204
+ "json_typeof",
205
+ "jsonb_agg",
206
+ "jsonb_array_length",
207
+ "jsonb_build_array",
208
+ "jsonb_build_object",
209
+ "jsonb_extract_path",
210
+ "jsonb_extract_path_text",
211
+ "jsonb_insert",
212
+ "jsonb_object_agg",
213
+ "jsonb_object_keys",
214
+ "jsonb_path_exists",
215
+ "jsonb_path_match",
216
+ "jsonb_path_query",
217
+ "jsonb_path_query_array",
218
+ "jsonb_path_query_first",
219
+ "jsonb_pretty",
220
+ "jsonb_set",
221
+ "jsonb_set_lax",
222
+ "jsonb_strip_nulls",
223
+ "jsonb_typeof",
224
+ "justify_days",
225
+ "justify_hours",
226
+ "justify_interval",
227
+ "lag",
228
+ "last_value",
229
+ "lead",
230
+ "left",
231
+ "length",
232
+ "ln",
233
+ "log",
234
+ "lower",
235
+ "lpad",
236
+ "ltrim",
237
+ "make_date",
238
+ "make_interval",
239
+ "make_time",
240
+ "make_timestamp",
241
+ "make_timestamptz",
242
+ "max",
243
+ "md5",
244
+ "min",
245
+ "mod",
246
+ "nth_value",
247
+ "ntile",
248
+ "octet_length",
249
+ "percent_rank",
250
+ "pi",
251
+ "power",
252
+ "quote_ident",
253
+ "quote_literal",
254
+ "quote_nullable",
255
+ "radians",
256
+ "rank",
257
+ "regexp_count",
258
+ "regexp_instr",
259
+ "regexp_like",
260
+ "regexp_match",
261
+ "regexp_matches",
262
+ "regexp_replace",
263
+ "regexp_split_to_array",
264
+ "regexp_split_to_table",
265
+ "repeat",
266
+ "replace",
267
+ "reverse",
268
+ "right",
269
+ "round",
270
+ "row_number",
271
+ "rpad",
272
+ "rtrim",
273
+ "sign",
274
+ "split_part",
275
+ "sqrt",
276
+ "starts_with",
277
+ "string_agg",
278
+ "string_to_array",
279
+ "strpos",
280
+ "substr",
281
+ "substring",
282
+ "sum",
283
+ "timezone",
284
+ "to_ascii",
285
+ "to_char",
286
+ "to_date",
287
+ "to_hex",
288
+ "to_json",
289
+ "to_jsonb",
290
+ "to_number",
291
+ "to_timestamp",
292
+ "translate",
293
+ "trunc",
294
+ "unnest",
295
+ "upper",
296
+ "width_bucket"
297
+ ];
116
298
  var canonicalActionSchema = zod.z.object({
117
299
  name: identifier,
118
300
  requires: zod.z.string().max(1e3).optional(),
@@ -124,6 +306,37 @@ var canonicalActionSchema = zod.z.object({
124
306
  returns: zod.z.enum(["rows", "rowsAffected", "scalar"]).default("rowsAffected"),
125
307
  audit: zod.z.object({ action: zod.z.string(), resource: zod.z.string() }).optional()
126
308
  });
309
+ var canonicalCowMutationSchema = zod.z.object({
310
+ kind: zod.z.enum(["override", "revert"]),
311
+ name: identifier,
312
+ /** Synced table (same namespace) the overlay copies from / falls back to. */
313
+ synced: identifier,
314
+ /** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
315
+ pk: zod.z.string().min(1).max(63),
316
+ /** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
317
+ copy: zod.z.array(zod.z.string().min(1).max(63)).default([]),
318
+ /** Override: overlay properties written by the UPDATE (never the pk). Empty for revert. */
319
+ set: zod.z.array(zod.z.string().min(1).max(63)).default([])
320
+ }).superRefine((mutation, ctx) => {
321
+ if (mutation.kind === "override") {
322
+ if (mutation.set.length === 0) {
323
+ ctx.addIssue({ code: "custom", path: ["set"], message: "override mutations must set at least one column" });
324
+ }
325
+ if (!mutation.copy.includes(mutation.pk)) {
326
+ ctx.addIssue({ code: "custom", path: ["copy"], message: "override copy list must include the pk" });
327
+ }
328
+ } else if (mutation.set.length > 0 || mutation.copy.length > 0) {
329
+ ctx.addIssue({ code: "custom", path: ["set"], message: "revert mutations take no copy/set columns" });
330
+ }
331
+ if (mutation.set.includes(mutation.pk)) {
332
+ ctx.addIssue({ code: "custom", path: ["set"], message: "set must not include the pk \u2014 identity columns are pinned" });
333
+ }
334
+ for (const [field, values] of [["copy", mutation.copy], ["set", mutation.set]]) {
335
+ if (new Set(values).size !== values.length) {
336
+ ctx.addIssue({ code: "custom", path: [field], message: `${field} must not contain duplicate columns` });
337
+ }
338
+ }
339
+ });
127
340
  var canonicalTableSchema = zod.z.object({
128
341
  table: identifier,
129
342
  description: zod.z.string().max(2e3).optional(),
@@ -136,7 +349,45 @@ var canonicalTableSchema = zod.z.object({
136
349
  links: zod.z.array(canonicalLinkSchema).default([]),
137
350
  indexes: zod.z.array(canonicalIndexSchema).default([]),
138
351
  policies: canonicalPoliciesSchema.default({}),
139
- actions: zod.z.array(canonicalActionSchema).default([])
352
+ /**
353
+ * v1-conversion carry-over only. v2 has no action surface — the runtime
354
+ * neither compiles nor executes these; writes are mutations. Kept parseable
355
+ * so stored definitions and their content hashes stay stable.
356
+ */
357
+ actions: zod.z.array(canonicalActionSchema).default([]),
358
+ /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
359
+ cowMutations: zod.z.array(canonicalCowMutationSchema).optional()
360
+ });
361
+ var canonicalViewSchema = zod.z.object({
362
+ view: identifier,
363
+ description: zod.z.string().max(2e3).optional(),
364
+ columns: zod.z.array(canonicalColumnSchema).min(1),
365
+ /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
366
+ primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
367
+ links: zod.z.array(canonicalLinkSchema).default([]),
368
+ policies: zod.z.object({ read: policyExpressionSchema }).strict(),
369
+ query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
370
+ message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
371
+ }),
372
+ /** Namespace-local logical relation names, sorted for deterministic hashing. */
373
+ dependencies: zod.z.array(identifier).max(500)
374
+ }).superRefine((view, ctx) => {
375
+ for (const [index3, column] of view.columns.entries()) {
376
+ if (column.default !== void 0 || column.defaultFn !== void 0) {
377
+ ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare a default` });
378
+ }
379
+ if (column.unique) {
380
+ ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare unique` });
381
+ }
382
+ if (column.renamedFrom !== void 0 || column.cast !== void 0) {
383
+ ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare migration hints` });
384
+ }
385
+ }
386
+ for (const [index3, link] of view.links.entries()) {
387
+ if (link.enforce) {
388
+ ctx.addIssue({ code: "custom", path: ["links", index3, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
389
+ }
390
+ }
140
391
  });
141
392
  var canonicalDefinitionSchema = zod.z.object({
142
393
  definition_format: zod.z.literal(CANONICAL_FORMAT),
@@ -144,24 +395,174 @@ var canonicalDefinitionSchema = zod.z.object({
144
395
  description: zod.z.string().max(2e3).optional(),
145
396
  enums: zod.z.array(canonicalEnumSchema).default([]),
146
397
  tables: zod.z.array(canonicalTableSchema).min(0).max(500),
398
+ /** Optional to preserve byte-identical hashes for pre-view canonical v2 documents. */
399
+ views: zod.z.array(canonicalViewSchema).max(500).optional(),
147
400
  /** Verbatim custom-roles grammar (@sequenceholdings/custom-roles-manifest). */
148
401
  capabilities: zod.z.object({
149
402
  roles: zod.z.unknown().optional(),
150
403
  uses: zod.z.array(zod.z.string()).default([])
151
404
  }).default({ uses: [] })
405
+ }).superRefine((definition, ctx) => {
406
+ const seen = /* @__PURE__ */ new Set();
407
+ for (const [index3, table] of definition.tables.entries()) {
408
+ if (seen.has(table.table)) {
409
+ ctx.addIssue({ code: "custom", path: ["tables", index3, "table"], message: `duplicate relation '${table.table}'` });
410
+ }
411
+ seen.add(table.table);
412
+ }
413
+ const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
414
+ for (const [tableIndex, table] of definition.tables.entries()) {
415
+ const cowMutations = table.cowMutations ?? [];
416
+ if (cowMutations.length === 0) continue;
417
+ const path = (index3, ...rest) => ["tables", tableIndex, "cowMutations", index3, ...rest];
418
+ if (table.source !== void 0) {
419
+ ctx.addIssue({ code: "custom", path: path(0), message: `synced table '${table.table}' cannot declare cow mutations \u2014 it is read-only` });
420
+ continue;
421
+ }
422
+ for (const [index3, mutation] of cowMutations.entries()) {
423
+ const synced = tableByName.get(mutation.synced);
424
+ if (!synced || synced.source === void 0) {
425
+ ctx.addIssue({
426
+ code: "custom",
427
+ path: path(index3, "synced"),
428
+ message: `cow mutation '${mutation.name}' references '${mutation.synced}', which is not a synced table in this namespace`
429
+ });
430
+ continue;
431
+ }
432
+ const overlayColumnsByName = new Map(table.columns.map((column) => [column.name, column]));
433
+ const syncedColumnsByName = new Map(synced.columns.map((column) => [column.name, column]));
434
+ let columnsExist = true;
435
+ for (const property of [mutation.pk, ...mutation.copy, ...mutation.set]) {
436
+ if (!overlayColumnsByName.has(property)) {
437
+ columnsExist = false;
438
+ ctx.addIssue({ code: "custom", path: path(index3), message: `cow mutation '${mutation.name}': column '${property}' does not exist on overlay '${table.table}'` });
439
+ }
440
+ }
441
+ for (const property of [mutation.pk, ...mutation.copy]) {
442
+ if (!syncedColumnsByName.has(property)) {
443
+ columnsExist = false;
444
+ ctx.addIssue({ code: "custom", path: path(index3), message: `cow mutation '${mutation.name}': column '${property}' does not exist on synced '${mutation.synced}'` });
445
+ }
446
+ }
447
+ if (!columnsExist) continue;
448
+ const overlayPk = table.primaryKey ?? [];
449
+ const syncedPk = synced.primaryKey ?? [];
450
+ if (overlayPk.length !== 1 || overlayPk[0] !== mutation.pk || syncedPk.length !== 1 || syncedPk[0] !== mutation.pk) {
451
+ ctx.addIssue({
452
+ code: "custom",
453
+ path: path(index3, "pk"),
454
+ message: `cow mutation '${mutation.name}': pk '${mutation.pk}' must be the single-column primary key on both '${table.table}' and '${mutation.synced}'`
455
+ });
456
+ }
457
+ for (const property of [mutation.pk, ...mutation.copy]) {
458
+ const overlayColumn = overlayColumnsByName.get(property);
459
+ const syncedColumn = syncedColumnsByName.get(property);
460
+ if (!overlayColumn || !syncedColumn) continue;
461
+ if (JSON.stringify(overlayColumn.type) !== JSON.stringify(syncedColumn.type)) {
462
+ ctx.addIssue({
463
+ code: "custom",
464
+ path: path(index3),
465
+ message: `cow mutation '${mutation.name}': column '${property}' must have the same type on overlay and synced`
466
+ });
467
+ }
468
+ if (overlayColumn.columnName !== syncedColumn.columnName) {
469
+ ctx.addIssue({
470
+ code: "custom",
471
+ path: path(index3),
472
+ message: `cow mutation '${mutation.name}': column '${property}' maps to different Postgres names on overlay ('${overlayColumn.columnName}') and synced ('${syncedColumn.columnName}')`
473
+ });
474
+ }
475
+ if (syncedColumn.nullable !== false && overlayColumn.nullable === false) {
476
+ ctx.addIssue({
477
+ code: "custom",
478
+ path: path(index3),
479
+ message: `cow mutation '${mutation.name}': column '${property}' is nullable on synced but NOT NULL on overlay \u2014 a NULL synced value would fail the copy`
480
+ });
481
+ }
482
+ }
483
+ }
484
+ }
485
+ for (const [index3, view] of (definition.views ?? []).entries()) {
486
+ if (seen.has(view.view)) {
487
+ ctx.addIssue({ code: "custom", path: ["views", index3, "view"], message: `duplicate relation '${view.view}'` });
488
+ }
489
+ seen.add(view.view);
490
+ }
491
+ const views = definition.views ?? [];
492
+ const viewByName = new Map(views.map((view) => [view.view, view]));
493
+ for (const [viewIndex, view] of views.entries()) {
494
+ for (const [dependencyIndex, dependency] of view.dependencies.entries()) {
495
+ if (!seen.has(dependency)) {
496
+ ctx.addIssue({
497
+ code: "custom",
498
+ path: ["views", viewIndex, "dependencies", dependencyIndex],
499
+ message: `view '${view.view}' depends on unknown relation '${dependency}'`
500
+ });
501
+ }
502
+ }
503
+ }
504
+ const visiting = /* @__PURE__ */ new Set();
505
+ const visited = /* @__PURE__ */ new Set();
506
+ let cycleReported = false;
507
+ const visit = (name, path) => {
508
+ if (visited.has(name) || cycleReported) return;
509
+ if (visiting.has(name)) {
510
+ cycleReported = true;
511
+ ctx.addIssue({
512
+ code: "custom",
513
+ path: ["views"],
514
+ message: `view dependencies form a cycle: ${[...path, name].join(" -> ")}`
515
+ });
516
+ return;
517
+ }
518
+ visiting.add(name);
519
+ const view = viewByName.get(name);
520
+ for (const dependency of view?.dependencies ?? []) {
521
+ if (viewByName.has(dependency)) visit(dependency, [...path, name]);
522
+ }
523
+ visiting.delete(name);
524
+ visited.add(name);
525
+ };
526
+ for (const view of views) visit(view.view, []);
152
527
  });
528
+ function canonicalRelations(definition) {
529
+ const views = (definition.views ?? []).map((view) => ({
530
+ table: view.view,
531
+ ...view.description !== void 0 ? { description: view.description } : {},
532
+ columns: view.columns,
533
+ ...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
534
+ links: view.links,
535
+ indexes: [],
536
+ policies: { read: view.policies.read },
537
+ actions: []
538
+ }));
539
+ return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
540
+ }
153
541
  function normalizeCanonical(definition) {
154
542
  const sortByName = (items) => [...items].sort((a, b) => a.name.localeCompare(b.name));
155
- return {
543
+ const normalized = {
156
544
  ...definition,
157
545
  enums: sortByName(definition.enums),
158
- tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => ({
159
- ...t2,
160
- links: sortByName(t2.links),
161
- indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
162
- actions: sortByName(t2.actions)
163
- }))
546
+ tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
547
+ const { cowMutations, ...rest } = t2;
548
+ return {
549
+ ...rest,
550
+ links: sortByName(t2.links),
551
+ indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
552
+ actions: sortByName(t2.actions),
553
+ // Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
554
+ ...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
555
+ };
556
+ })
164
557
  };
558
+ if (definition.views !== void 0) {
559
+ normalized.views = [...definition.views].sort((a, b) => a.view.localeCompare(b.view)).map((view) => ({
560
+ ...view,
561
+ links: sortByName(view.links),
562
+ dependencies: [...view.dependencies].sort()
563
+ }));
564
+ }
565
+ return normalized;
165
566
  }
166
567
 
167
568
  // src/constants.ts
@@ -202,9 +603,13 @@ function canonicalTable(table) {
202
603
  source: table.source ?? null,
203
604
  links: emptyToNull(table.links),
204
605
  indexes: emptyToNull(table.indexes),
606
+ // insert/update are omitted rather than nulled when absent, so definitions
607
+ // predating split policies keep the hash they were registered under.
205
608
  policies: {
206
609
  read: table.policies?.read?.source ?? null,
207
- write: table.policies?.write?.source ?? null
610
+ write: table.policies?.write?.source ?? null,
611
+ ...table.policies?.insert !== void 0 ? { insert: table.policies.insert.source } : {},
612
+ ...table.policies?.update !== void 0 ? { update: table.policies.update.source } : {}
208
613
  },
209
614
  actions: emptyToNull(
210
615
  Object.fromEntries(
@@ -276,7 +681,7 @@ function contextStack() {
276
681
  return created;
277
682
  }
278
683
  function beginAuthoringContext() {
279
- const context = { currentFile: void 0, tables: [], enums: [], configs: [] };
684
+ const context = { currentFile: void 0, tables: [], views: [], enums: [], configs: [] };
280
685
  contextStack().push(context);
281
686
  return context;
282
687
  }
@@ -294,6 +699,155 @@ var TABLE_BRAND = "seq-table";
294
699
  function isSeqTable(value) {
295
700
  return typeof value === "object" && value !== null && value.kind === TABLE_BRAND;
296
701
  }
702
+ var VIEW_BRAND = "seq-view";
703
+ function isSeqView(value) {
704
+ return typeof value === "object" && value !== null && value.kind === VIEW_BRAND;
705
+ }
706
+ var QUERY_COLUMN_FACTORIES = {
707
+ uuid: (name) => pgCore.uuid(name),
708
+ text: (name) => pgCore.text(name),
709
+ integer: (name) => pgCore.integer(name),
710
+ bigint: (name) => pgCore.bigint(name, { mode: "bigint" }),
711
+ numeric: (name) => pgCore.numeric(name),
712
+ double: (name) => pgCore.doublePrecision(name),
713
+ boolean: (name) => pgCore.boolean(name),
714
+ timestamptz: (name) => pgCore.timestamp(name, { withTimezone: true }),
715
+ date: (name) => pgCore.date(name),
716
+ jsonb: (name) => pgCore.jsonb(name)
717
+ };
718
+ function compileViewQueries(input) {
719
+ const schemaName = `orm_${input.namespace}`;
720
+ const schema = pgCore.pgSchema(schemaName);
721
+ const enumFactories = /* @__PURE__ */ new Map();
722
+ for (const entry of input.enums) {
723
+ const values = entry.values;
724
+ enumFactories.set(entry.name, schema.enum(entry.name, values));
725
+ }
726
+ const handles = /* @__PURE__ */ new Map();
727
+ const physicalToLogical = /* @__PURE__ */ new Map();
728
+ const tableByName = new Map(input.tables.map((table) => [table.table, table]));
729
+ for (const authored of input.authoredTables) {
730
+ const canonical = tableByName.get(authored.tableName);
731
+ if (!canonical) continue;
732
+ const physicalSchema = schemaName;
733
+ const physicalName = canonical.table;
734
+ const relationSchema = pgCore.pgSchema(physicalSchema);
735
+ const columns = buildQueryColumns(canonical.columns, enumFactories);
736
+ const handle = relationSchema.table(physicalName, columns);
737
+ handles.set(authored, handle);
738
+ physicalToLogical.set(`${physicalSchema}.${physicalName}`, canonical.table);
739
+ }
740
+ const viewByName = new Map(input.views.map((view) => [view.view, view]));
741
+ for (const authored of input.authoredViews) {
742
+ const canonical = viewByName.get(authored.viewName);
743
+ if (!canonical) continue;
744
+ const columns = buildQueryColumns(canonical.columns, enumFactories);
745
+ const handle = schema.view(canonical.view, columns).existing();
746
+ handles.set(authored, handle);
747
+ physicalToLogical.set(`${schemaName}.${canonical.view}`, canonical.view);
748
+ }
749
+ const dialect = new pgCore.PgDialect();
750
+ const compiled = /* @__PURE__ */ new Map();
751
+ for (const authored of input.authoredViews) {
752
+ const expectedFields = Object.keys(authored.columns);
753
+ const declaredDependencies = /* @__PURE__ */ new Set();
754
+ const relation = (ref) => {
755
+ let resolved;
756
+ try {
757
+ resolved = typeof ref === "function" ? ref() : ref;
758
+ } catch (error) {
759
+ throw new OrmDefinitionError(
760
+ `view "${authored.viewName}" relation thunk threw: ${error instanceof Error ? error.message : String(error)}`,
761
+ authored.file
762
+ );
763
+ }
764
+ if (!isSeqTable(resolved) && !isSeqView(resolved)) {
765
+ throw new OrmDefinitionError(
766
+ `view "${authored.viewName}" relation() expects a table or view defined by this ORM`,
767
+ authored.file
768
+ );
769
+ }
770
+ const handle = handles.get(resolved);
771
+ if (!handle) {
772
+ const name = isSeqTable(resolved) ? resolved.tableName : resolved.viewName;
773
+ throw new OrmDefinitionError(
774
+ `view "${authored.viewName}" references relation "${name}" which is not part of this namespace`,
775
+ authored.file
776
+ );
777
+ }
778
+ declaredDependencies.add(isSeqTable(resolved) ? resolved.tableName : resolved.viewName);
779
+ return handle;
780
+ };
781
+ let queryBuilder;
782
+ try {
783
+ queryBuilder = authored.config.query({ qb: new pgCore.QueryBuilder(), relation });
784
+ } catch (error) {
785
+ if (error instanceof OrmDefinitionError) throw error;
786
+ throw new OrmDefinitionError(
787
+ `view "${authored.viewName}" query callback failed: ${error instanceof Error ? error.message : String(error)}`,
788
+ authored.file
789
+ );
790
+ }
791
+ let config;
792
+ try {
793
+ config = pgCore.getViewConfig(schema.view(authored.viewName).as(queryBuilder));
794
+ } catch (error) {
795
+ throw new OrmDefinitionError(
796
+ `view "${authored.viewName}" query must return a Drizzle select builder: ${error instanceof Error ? error.message : String(error)}`,
797
+ authored.file
798
+ );
799
+ }
800
+ const selectedFields = Object.keys(config.selectedFields);
801
+ if (selectedFields.length !== expectedFields.length || selectedFields.some((field, index3) => field !== expectedFields[index3])) {
802
+ throw new OrmDefinitionError(
803
+ `view "${authored.viewName}" query selects [${selectedFields.join(", ")}], but declared columns are [${expectedFields.join(", ")}]`,
804
+ authored.file
805
+ );
806
+ }
807
+ if (!config.query) throw new OrmDefinitionError(`view "${authored.viewName}" produced no query`, authored.file);
808
+ const tableDependencies = pgCore.extractUsedTable(config.query).map((physical) => {
809
+ const logical = physicalToLogical.get(physical);
810
+ if (!logical) {
811
+ throw new OrmDefinitionError(
812
+ `view "${authored.viewName}" query references unmanaged relation "${physical}" \u2014 use relation(...) with a namespace table or view`,
813
+ authored.file
814
+ );
815
+ }
816
+ return logical;
817
+ });
818
+ const renderedQuery = dialect.sqlToQuery(config.query.inlineParams());
819
+ if (renderedQuery.params.length > 0) {
820
+ throw new OrmDefinitionError(
821
+ `view "${authored.viewName}" query produced bind parameters which cannot be persisted in view DDL`,
822
+ authored.file
823
+ );
824
+ }
825
+ compiled.set(authored.viewName, {
826
+ query: renderedQuery.sql,
827
+ dependencies: [.../* @__PURE__ */ new Set([...declaredDependencies, ...tableDependencies])].sort()
828
+ });
829
+ }
830
+ return compiled;
831
+ }
832
+ function buildQueryColumns(columns, enumFactories) {
833
+ return Object.fromEntries(columns.map((column) => [column.name, buildQueryColumn(column, enumFactories)]));
834
+ }
835
+ function buildQueryColumn(column, enumFactories) {
836
+ const factory = typeof column.type === "string" ? QUERY_COLUMN_FACTORIES[column.type] : enumFactories.get(column.type.enum);
837
+ if (!factory) {
838
+ const type = typeof column.type === "string" ? column.type : column.type.enum;
839
+ throw new OrmDefinitionError(`view query column '${column.columnName}' references unknown type '${type}'`);
840
+ }
841
+ let builder = factory(column.columnName);
842
+ if (!column.nullable) builder = builder.notNull();
843
+ return builder;
844
+ }
845
+ function relationName(relation) {
846
+ return isSeqTable(relation) ? relation.tableName : relation.viewName;
847
+ }
848
+ function relationKind(relation) {
849
+ return isSeqTable(relation) ? "table" : "view";
850
+ }
297
851
 
298
852
  // src/authoring/namespace.ts
299
853
  var NAMESPACE_CONFIG_BRAND = "seq-namespace-config";
@@ -302,6 +856,7 @@ function isSeqNamespaceConfig(value) {
302
856
  }
303
857
  function serializeNamespace(input) {
304
858
  const { config, tables } = input;
859
+ const views = input.views ?? [];
305
860
  const enums = input.enums ?? [];
306
861
  const tablesByName = /* @__PURE__ */ new Map();
307
862
  for (const table of tables) {
@@ -314,6 +869,29 @@ function serializeNamespace(input) {
314
869
  }
315
870
  tablesByName.set(table.tableName, table);
316
871
  }
872
+ const viewsByName = /* @__PURE__ */ new Map();
873
+ for (const view of views) {
874
+ const existing = viewsByName.get(view.viewName);
875
+ if (existing) {
876
+ throw new OrmDefinitionError(
877
+ `duplicate view "${view.viewName}" (defined in ${describeFile(existing)} and ${describeFile(view)})`,
878
+ view.file
879
+ );
880
+ }
881
+ viewsByName.set(view.viewName, view);
882
+ }
883
+ const relationsByName = /* @__PURE__ */ new Map();
884
+ for (const relation of [...tables, ...views]) {
885
+ const name = relationName(relation);
886
+ const existing = relationsByName.get(name);
887
+ if (existing) {
888
+ throw new OrmDefinitionError(
889
+ `duplicate relation "${name}" (defined as ${relationKind(existing)} in ${describeFile(existing)} and ${relationKind(relation)} in ${describeFile(relation)})`,
890
+ relation.file
891
+ );
892
+ }
893
+ relationsByName.set(name, relation);
894
+ }
317
895
  const enumsByName = /* @__PURE__ */ new Map();
318
896
  for (const ref of enums) {
319
897
  if (enumsByName.has(ref.name)) {
@@ -321,11 +899,26 @@ function serializeNamespace(input) {
321
899
  }
322
900
  enumsByName.set(ref.name, ref);
323
901
  }
324
- const canonicalTables = tables.map((table) => buildCanonicalTable({ table, tablesByName, enumsByName }));
902
+ const canonicalTables = tables.map((table) => buildCanonicalTable({ table, relationsByName, enumsByName }));
325
903
  const canonicalEnums = [...enumsByName.values()].map((ref) => ({
326
904
  name: ref.name,
327
905
  values: [...ref.values]
328
906
  }));
907
+ const viewDrafts = views.map((view) => buildCanonicalView({ view, relationsByName, enumsByName }));
908
+ const compiledViews = compileViewQueries({
909
+ namespace: config.namespace,
910
+ tables: canonicalTables,
911
+ views: viewDrafts,
912
+ enums: canonicalEnums,
913
+ authoredTables: tables,
914
+ authoredViews: views
915
+ });
916
+ const canonicalViews = viewDrafts.map((draft) => {
917
+ const compiled = compiledViews.get(draft.view);
918
+ if (!compiled) throw new OrmDefinitionError(`view "${draft.view}" did not compile`, viewsByName.get(draft.view)?.file);
919
+ return canonicalViewSchema.parse({ ...draft, ...compiled });
920
+ });
921
+ assertAcyclicViewDependencies(canonicalViews);
329
922
  const candidate = {
330
923
  definition_format: CANONICAL_FORMAT,
331
924
  namespace: config.namespace,
@@ -337,6 +930,7 @@ function serializeNamespace(input) {
337
930
  }
338
931
  };
339
932
  if (config.description !== void 0) candidate.description = config.description;
933
+ if (canonicalViews.length > 0) candidate.views = canonicalViews;
340
934
  let parsed;
341
935
  try {
342
936
  parsed = canonicalDefinitionSchema.parse(candidate);
@@ -351,7 +945,7 @@ function serializeNamespace(input) {
351
945
  function computeCanonicalHash(definition) {
352
946
  return crypto.createHash("sha256").update(HASH_DOMAIN).update(stableJson(definition)).digest("hex");
353
947
  }
354
- function buildCanonicalTable({ table, tablesByName, enumsByName }) {
948
+ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
355
949
  const candidate = {
356
950
  table: table.tableName,
357
951
  columns: [],
@@ -363,6 +957,31 @@ function buildCanonicalTable({ table, tablesByName, enumsByName }) {
363
957
  if (table.config.description !== void 0) candidate.description = table.config.description;
364
958
  if (table.config.renamedFrom !== void 0) candidate.renamedFrom = table.config.renamedFrom;
365
959
  if (table.source !== void 0) candidate.source = table.source;
960
+ if (table.source !== void 0) {
961
+ if (table.config.indexes !== void 0 && table.config.indexes.length > 0) {
962
+ throw new OrmDefinitionError(
963
+ `synced table "${table.tableName}" is system-owned; indexes are not allowed`,
964
+ table.file
965
+ );
966
+ }
967
+ if (table.config.cowMutations !== void 0 && table.config.cowMutations.length > 0) {
968
+ throw new OrmDefinitionError(
969
+ `synced table "${table.tableName}" is read-only; cow mutations belong on the overlay table`,
970
+ table.file
971
+ );
972
+ }
973
+ if (table.config.policies?.write !== void 0 || table.config.policies?.insert !== void 0 || table.config.policies?.update !== void 0) {
974
+ throw new OrmDefinitionError(`synced table "${table.tableName}" is read-only`, table.file);
975
+ }
976
+ for (const [linkName, link] of Object.entries(table.config.links ?? {})) {
977
+ if (link.enforce) {
978
+ throw new OrmDefinitionError(
979
+ `synced table "${table.tableName}": link "${linkName}" cannot enforce foreign keys \u2014 its DDL is owned by the sync`,
980
+ table.file
981
+ );
982
+ }
983
+ }
984
+ }
366
985
  const columnLevelPk = [];
367
986
  for (const [propertyName, builder] of Object.entries(table.columns)) {
368
987
  const column = builder.toCanonicalColumn(propertyName);
@@ -384,40 +1003,38 @@ function buildCanonicalTable({ table, tablesByName, enumsByName }) {
384
1003
  );
385
1004
  }
386
1005
  candidate.primaryKey = table.config.primaryKey.map(
387
- (ref) => resolveColumnRef({ table, ref, role: "primaryKey" })
1006
+ (ref) => resolveColumnRef({ relation: table, ref, role: "primaryKey" })
388
1007
  );
389
1008
  } else if (columnLevelPk.length > 0) {
390
1009
  candidate.primaryKey = columnLevelPk;
391
1010
  }
392
1011
  for (const index3 of table.config.indexes ?? []) {
393
1012
  const entry = {
394
- columns: index3.columns.map((ref) => resolveColumnRef({ table, ref, role: "index" }))
1013
+ columns: index3.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
395
1014
  };
396
1015
  if (index3.unique !== void 0) entry.unique = index3.unique;
397
1016
  if (index3.name !== void 0) entry.name = index3.name;
398
1017
  candidate.indexes?.push(entry);
399
1018
  }
400
1019
  for (const [linkName, link] of Object.entries(table.config.links ?? {})) {
401
- candidate.links?.push(buildCanonicalLink({ table, linkName, link, tablesByName }));
1020
+ candidate.links?.push(buildCanonicalLink({ relation: table, linkName, link, relationsByName }));
402
1021
  }
403
1022
  const policies = {};
404
1023
  if (table.config.policies?.read !== void 0) policies.read = toCanonicalPolicy(table.config.policies.read);
405
1024
  if (table.config.policies?.write !== void 0) policies.write = toCanonicalPolicy(table.config.policies.write);
1025
+ if (table.config.policies?.insert !== void 0) policies.insert = toCanonicalPolicy(table.config.policies.insert);
1026
+ if (table.config.policies?.update !== void 0) policies.update = toCanonicalPolicy(table.config.policies.update);
406
1027
  candidate.policies = policies;
407
- for (const action of table.config.actions ?? []) {
408
- candidate.actions?.push({
409
- name: action.name,
410
- ...action.requires !== void 0 ? { requires: action.requires } : {},
411
- params: Object.fromEntries(
412
- Object.entries(action.params ?? {}).map(([name, param]) => [
413
- name,
414
- { type: param.type, nullable: param.nullable ?? false }
415
- ])
416
- ),
417
- executes: action.executes,
418
- returns: action.returns ?? "rowsAffected",
419
- ...action.audit !== void 0 ? { audit: action.audit } : {}
420
- });
1028
+ const cowMutations = table.config.cowMutations ?? [];
1029
+ if (cowMutations.length > 0) {
1030
+ candidate.cowMutations = cowMutations.map((mutation) => ({
1031
+ kind: mutation.kind,
1032
+ name: mutation.name,
1033
+ synced: mutation.synced,
1034
+ pk: mutation.pk,
1035
+ copy: [...mutation.copy],
1036
+ set: [...mutation.set]
1037
+ }));
421
1038
  }
422
1039
  try {
423
1040
  return canonicalTableSchema.parse(candidate);
@@ -428,14 +1045,71 @@ function buildCanonicalTable({ table, tablesByName, enumsByName }) {
428
1045
  throw error;
429
1046
  }
430
1047
  }
1048
+ function buildCanonicalView({
1049
+ view,
1050
+ relationsByName,
1051
+ enumsByName
1052
+ }) {
1053
+ if (view.config.policies?.read === void 0) {
1054
+ throw new OrmDefinitionError(`view "${view.viewName}" must declare policies.read`, view.file);
1055
+ }
1056
+ const columns = [];
1057
+ const columnLevelPk = [];
1058
+ for (const [propertyName, builder] of Object.entries(view.columns)) {
1059
+ const column = builder.toCanonicalColumn(propertyName);
1060
+ if (column.default !== void 0 || column.defaultFn !== void 0) {
1061
+ throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare a default`, view.file);
1062
+ }
1063
+ if (column.unique) {
1064
+ throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare unique`, view.file);
1065
+ }
1066
+ if (column.renamedFrom !== void 0 || column.cast !== void 0) {
1067
+ throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare migration hints`, view.file);
1068
+ }
1069
+ const enumName = builder.enumName;
1070
+ if (enumName !== void 0 && !enumsByName.has(enumName)) {
1071
+ throw new OrmDefinitionError(
1072
+ `view "${view.viewName}": column "${propertyName}" references enum "${enumName}" which is not part of this namespace`,
1073
+ view.file
1074
+ );
1075
+ }
1076
+ if (builder.isPrimaryKey) columnLevelPk.push(column.columnName);
1077
+ columns.push(column);
1078
+ }
1079
+ let primaryKey2;
1080
+ if (view.config.primaryKey !== void 0) {
1081
+ if (columnLevelPk.length > 0) {
1082
+ throw new OrmDefinitionError(
1083
+ `view "${view.viewName}": declare the logical primary key either on columns or with config.primaryKey \u2014 not both`,
1084
+ view.file
1085
+ );
1086
+ }
1087
+ primaryKey2 = view.config.primaryKey.map((ref) => resolveColumnRef({ relation: view, ref, role: "primaryKey" }));
1088
+ } else if (columnLevelPk.length > 0) {
1089
+ primaryKey2 = columnLevelPk;
1090
+ }
1091
+ const links = Object.entries(view.config.links ?? {}).map(
1092
+ ([linkName, link]) => buildCanonicalLink({ relation: view, linkName, link, relationsByName })
1093
+ );
1094
+ return {
1095
+ view: view.viewName,
1096
+ ...view.config.description !== void 0 ? { description: view.config.description } : {},
1097
+ columns,
1098
+ ...primaryKey2 !== void 0 ? { primaryKey: primaryKey2 } : {},
1099
+ links,
1100
+ policies: { read: toCanonicalPolicy(view.config.policies.read) }
1101
+ };
1102
+ }
431
1103
  function buildCanonicalLink({
432
- table,
1104
+ relation,
433
1105
  linkName,
434
1106
  link,
435
- tablesByName
1107
+ relationsByName
436
1108
  }) {
1109
+ const ownerKind = relationKind(relation);
1110
+ const ownerName = relationName(relation);
437
1111
  const fail2 = (message) => {
438
- throw new OrmDefinitionError(`table "${table.tableName}": link "${linkName}" ${message}`, table.file);
1112
+ throw new OrmDefinitionError(`${ownerKind} "${ownerName}": link "${linkName}" ${message}`, relation.file);
439
1113
  };
440
1114
  if (link.enforce && link.linkKind !== "toOne") {
441
1115
  return fail2("sets enforce: true, which is only supported on toOne links");
@@ -445,29 +1119,34 @@ function buildCanonicalLink({
445
1119
  return {
446
1120
  name: linkName,
447
1121
  kind: "toPlatformUser",
448
- on: resolveColumnRef({ table, ref: link.on, role: `link "${linkName}" on` }),
1122
+ on: resolveColumnRef({ relation, ref: link.on, role: `link "${linkName}" on` }),
449
1123
  ...link.references !== void 0 ? { references: deriveColumnName(link.references) } : {},
450
1124
  enforce: false,
451
1125
  ...link.description !== void 0 ? { description: link.description } : {}
452
1126
  };
453
1127
  }
454
- const target = resolveTableRef({ ref: link.target, fail: fail2 });
455
- if (!tablesByName.has(target.tableName)) {
456
- return fail2(`targets table "${target.tableName}" which is not part of this namespace`);
1128
+ const target = resolveRelationRef({ ref: link.target, fail: fail2 });
1129
+ const targetName = relationName(target);
1130
+ if (!relationsByName.has(targetName)) {
1131
+ return fail2(`targets ${relationKind(target)} "${targetName}" which is not part of this namespace`);
1132
+ }
1133
+ if (link.enforce && (!isSeqTable(relation) || !isSeqTable(target))) {
1134
+ return fail2("cannot enforce a PostgreSQL foreign key involving a view");
457
1135
  }
458
- const owningSide = link.linkKind === "toOne" ? table : target;
1136
+ const owningSide = link.linkKind === "toOne" ? relation : target;
1137
+ const referencedSide = link.linkKind === "toOne" ? target : relation;
459
1138
  return {
460
1139
  name: linkName,
461
1140
  kind: link.linkKind,
462
- target: target.tableName,
463
- on: resolveColumnRef({ table: owningSide, ref: link.on, role: `link "${linkName}" on` }),
464
- ...link.references !== void 0 ? { references: resolveColumnRef({ table: target, ref: link.references, role: `link "${linkName}" references` }) } : {},
1141
+ target: targetName,
1142
+ on: resolveColumnRef({ relation: owningSide, ref: link.on, role: `link "${linkName}" on` }),
1143
+ ...link.references !== void 0 ? { references: resolveColumnRef({ relation: referencedSide, ref: link.references, role: `link "${linkName}" references` }) } : {},
465
1144
  enforce: link.enforce,
466
1145
  ...link.description !== void 0 ? { description: link.description } : {}
467
1146
  };
468
1147
  }
469
- function resolveTableRef({ ref, fail: fail2 }) {
470
- if (ref === void 0) return fail2("declares no target table");
1148
+ function resolveRelationRef({ ref, fail: fail2 }) {
1149
+ if (ref === void 0) return fail2("declares no target relation");
471
1150
  if (typeof ref !== "function") return ref;
472
1151
  let resolved;
473
1152
  try {
@@ -477,20 +1156,35 @@ function resolveTableRef({ ref, fail: fail2 }) {
477
1156
  `target thunk threw (${error instanceof Error ? error.message : String(error)}) \u2014 lazy refs resolve at serialize time; make sure the target module ran`
478
1157
  );
479
1158
  }
480
- if (!isSeqTable(resolved)) return fail2("target thunk did not return a table defined with defineTable");
1159
+ if (!isSeqTable(resolved) && !isSeqView(resolved)) {
1160
+ return fail2("target thunk did not return a relation defined with defineTable/defineView");
1161
+ }
481
1162
  return resolved;
482
1163
  }
483
- function resolveColumnRef({ table, ref, role }) {
484
- const direct = table.columns[ref];
1164
+ function resolveColumnRef({ relation, ref, role }) {
1165
+ const direct = relation.columns[ref];
485
1166
  if (direct) return direct.columnNameFor(ref);
486
- for (const [propertyName, builder] of Object.entries(table.columns)) {
1167
+ for (const [propertyName, builder] of Object.entries(relation.columns)) {
487
1168
  if (builder.columnNameFor(propertyName) === ref) return ref;
488
1169
  }
489
- const known = Object.keys(table.columns).join(", ");
1170
+ const known = Object.keys(relation.columns).join(", ");
490
1171
  throw new OrmDefinitionError(
491
- `table "${table.tableName}": ${role} references unknown column "${ref}" (known columns: ${known || "none"})`,
492
- table.file
1172
+ `${relationKind(relation)} "${relationName(relation)}": ${role} references unknown column "${ref}" (known columns: ${known || "none"})`,
1173
+ relation.file
1174
+ );
1175
+ }
1176
+ function assertAcyclicViewDependencies(views) {
1177
+ const viewNames = new Set(views.map((view) => view.view));
1178
+ const remaining = new Map(
1179
+ views.map((view) => [view.view, new Set(view.dependencies.filter((dependency) => viewNames.has(dependency)))])
493
1180
  );
1181
+ while (remaining.size > 0) {
1182
+ const ready = [...remaining.entries()].filter(([, dependencies]) => [...dependencies].every((dependency) => !remaining.has(dependency))).map(([name]) => name);
1183
+ if (ready.length === 0) {
1184
+ throw new OrmDefinitionError(`view dependencies form a cycle among: ${[...remaining.keys()].sort().join(", ")}`);
1185
+ }
1186
+ for (const name of ready) remaining.delete(name);
1187
+ }
494
1188
  }
495
1189
  function toCanonicalPolicy(policy) {
496
1190
  if (typeof policy === "string") return policy;
@@ -503,8 +1197,8 @@ function toCanonicalPolicy(policy) {
503
1197
  }))
504
1198
  };
505
1199
  }
506
- function describeFile(table) {
507
- return table.file ?? "<unknown file>";
1200
+ function describeFile(relation) {
1201
+ return relation.file ?? "<unknown file>";
508
1202
  }
509
1203
  function formatZodIssues(error) {
510
1204
  return error.issues.map((issue) => issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message).join("; ");
@@ -569,16 +1263,20 @@ async function loadNamespaceFromDir(dir, options = {}) {
569
1263
  for (const entry of context.tables) {
570
1264
  if (isSeqTable(entry.value)) tables.push(entry.value);
571
1265
  }
572
- if (tables.length === 0) {
1266
+ const views = [];
1267
+ for (const entry of context.views) {
1268
+ if (isSeqView(entry.value)) views.push(entry.value);
1269
+ }
1270
+ if (tables.length === 0 && views.length === 0) {
573
1271
  throw new OrmDefinitionError(
574
- `no tables registered \u2014 ${SCHEMA_DIR}/*.ts modules must call defineTable(...) at import time`
1272
+ `no relations registered \u2014 ${SCHEMA_DIR}/*.ts modules must call defineTable(...) or defineView(...) at import time`
575
1273
  );
576
1274
  }
577
1275
  const enums = [];
578
1276
  for (const entry of context.enums) {
579
1277
  if (isEnumRef(entry.value)) enums.push(entry.value);
580
1278
  }
581
- const canonical = serializeNamespace({ config: configEntry.value, tables, enums });
1279
+ const canonical = serializeNamespace({ config: configEntry.value, tables, views, enums });
582
1280
  return { canonical, hash: computeCanonicalHash(canonical), files };
583
1281
  }
584
1282
  async function listSchemaFiles(root) {
@@ -612,7 +1310,7 @@ var manifestRequiresSchema = zod.z.object({
612
1310
  }).nullable();
613
1311
  var manifestOperationSchema = zod.z.object({
614
1312
  hash: zod.z.string().regex(OP_HASH_RE),
615
- kind: zod.z.enum(["query", "mutation", "action"]),
1313
+ kind: zod.z.enum(["query", "mutation"]),
616
1314
  requires: manifestRequiresSchema
617
1315
  });
618
1316
  zod.z.object({
@@ -775,14 +1473,14 @@ function generateTypePolicies(canonical) {
775
1473
  const def = normalizeCanonical(canonicalDefinitionSchema.parse(canonical));
776
1474
  const entries = [];
777
1475
  let hasPlatformUser = false;
778
- for (const table of def.tables) {
1476
+ for (const table of canonicalRelations(def)) {
779
1477
  if (table.source !== void 0) continue;
780
1478
  if (table.links.some((link) => link.kind === "toPlatformUser")) hasPlatformUser = true;
781
1479
  if (!table.primaryKey || table.primaryKey.length === 0) continue;
782
1480
  const keyFields = table.primaryKey.map((identifier2) => {
783
1481
  const column = table.columns.find((c) => c.columnName === identifier2 || c.name === identifier2);
784
1482
  if (!column) {
785
- throw new CodegenGqlError(`Primary key column "${identifier2}" not found on table "${table.table}".`);
1483
+ throw new CodegenGqlError(`Primary key column "${identifier2}" not found on relation "${table.table}".`);
786
1484
  }
787
1485
  return fieldName(column.name);
788
1486
  });
@@ -936,9 +1634,23 @@ var onConflictIrSchema = zod.z.union([
936
1634
  ]);
937
1635
  var mutationRootSchema = zod.z.object({
938
1636
  kind: zod.z.literal("mutation"),
939
- op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk"]),
1637
+ /**
1638
+ * CRUD verbs plus the structured copy-on-write ops. `cowOverride` copies the
1639
+ * synced row into the overlay (INSERT…SELECT, ON CONFLICT DO NOTHING) then
1640
+ * applies `set` — both statements generated by the executor from this
1641
+ * metadata, never authored SQL. `cowRevert` deletes the override so the
1642
+ * coalesce view falls back to synced. Both are gated on a matching synced
1643
+ * row and carry the write policy in `where` like update/delete.
1644
+ */
1645
+ op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
940
1646
  alias: zod.z.string(),
941
1647
  table: zod.z.string(),
1648
+ /** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
1649
+ cowSynced: zod.z.string().optional(),
1650
+ /** cowOverride: logical properties copied INSERT…SELECT from synced (pk included). */
1651
+ cowCopyColumns: zod.z.array(zod.z.string()).optional(),
1652
+ /** cowOverride: insert-policy WITH CHECK evaluated over the copied rows. */
1653
+ cowCopyCheck: predIrSchema.optional(),
942
1654
  /** insert/insertOne: rows come from a slot ($object/$objects) or literals. */
943
1655
  objects: zod.z.union([
944
1656
  zod.z.object({ slot: zod.z.string() }),
@@ -959,19 +1671,11 @@ var mutationRootSchema = zod.z.object({
959
1671
  affectedRows: zod.z.boolean().default(false),
960
1672
  includeIf: includeIfSchema.optional()
961
1673
  });
962
- var actionRootSchema = zod.z.object({
963
- kind: zod.z.literal("action"),
964
- alias: zod.z.string(),
965
- name: zod.z.string(),
966
- params: zod.z.array(zod.z.object({ name: zod.z.string(), value: valueIrSchema })),
967
- returns: zod.z.enum(["rows", "rowsAffected", "scalar"])
968
- });
969
1674
  var rootIrSchema = zod.z.discriminatedUnion("kind", [
970
1675
  selectRootSchema,
971
1676
  byPkRootSchema,
972
1677
  aggregateRootSchema,
973
- mutationRootSchema,
974
- actionRootSchema
1678
+ mutationRootSchema
975
1679
  ]);
976
1680
  var INPUT_REF_KINDS = [
977
1681
  "boolExp",
@@ -1003,7 +1707,7 @@ var compiledOperationSchema = zod.z.object({
1003
1707
  irVersion: zod.z.literal(IR_VERSION),
1004
1708
  name: zod.z.string(),
1005
1709
  opHash: zod.z.string().regex(/^sha256:[0-9a-f]{64}$/),
1006
- kind: zod.z.enum(["query", "mutation", "action"]),
1710
+ kind: zod.z.enum(["query", "mutation"]),
1007
1711
  requires: zod.z.object({
1008
1712
  capability: zod.z.string().optional(),
1009
1713
  role: zod.z.string().optional()
@@ -1017,13 +1721,24 @@ var tableMetaIrSchema = zod.z.object({
1017
1721
  table: zod.z.string(),
1018
1722
  schema: zod.z.string(),
1019
1723
  typename: zod.z.string(),
1724
+ /**
1725
+ * True when statements over this relation evaluate a claims-gated synced
1726
+ * view: synced twins (`source: gold.*`) materialize as views whose baked
1727
+ * policy reads `current_setting('request.jwt.claims')`, and namespace views
1728
+ * inherit the gate transitively through their declared dependencies. The
1729
+ * executor must stamp that GUC (transaction-scoped) before any statement
1730
+ * touching such a relation, or the synced arm yields zero rows regardless
1731
+ * of the compiled predicates.
1732
+ */
1733
+ claimsGated: zod.z.boolean().default(false),
1020
1734
  columns: zod.z.array(zod.z.object({
1021
1735
  name: zod.z.string(),
1022
1736
  columnName: zod.z.string(),
1023
1737
  type: zod.z.union([zod.z.enum(COLUMN_TYPES), zod.z.object({ enum: zod.z.string(), values: zod.z.array(zod.z.string()) })]),
1024
1738
  nullable: zod.z.boolean()
1025
1739
  })),
1026
- primaryKey: zod.z.array(zod.z.string()).min(1),
1740
+ /** Empty for keyless read-only views; keyed tables and views list logical columns. */
1741
+ primaryKey: zod.z.array(zod.z.string()),
1027
1742
  links: zod.z.array(zod.z.object({
1028
1743
  name: zod.z.string(),
1029
1744
  kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
@@ -1562,6 +2277,7 @@ var NamespaceCompileError = class extends Error {
1562
2277
 
1563
2278
  // src/compiler-gql/pred-utils.ts
1564
2279
  var CONST_TRUE = { kind: "const", value: true };
2280
+ var CONST_FALSE = { kind: "const", value: false };
1565
2281
  function andPred(...preds) {
1566
2282
  const operands = preds.filter(
1567
2283
  (pred) => pred !== void 0 && !(pred.kind === "const" && pred.value)
@@ -1647,6 +2363,12 @@ var PolicyInjector = class {
1647
2363
  write(table) {
1648
2364
  return this.ctx.writePolicies.get(table) ?? { kind: "const", value: true };
1649
2365
  }
2366
+ insert(table) {
2367
+ return this.ctx.insertPolicies.get(table) ?? this.write(table);
2368
+ }
2369
+ update(table) {
2370
+ return this.ctx.updatePolicies.get(table) ?? this.write(table);
2371
+ }
1650
2372
  linkTarget(table, link) {
1651
2373
  const meta = this.ctx.tables[table];
1652
2374
  const resolved = meta?.links.find((l) => l.name === link);
@@ -1721,31 +2443,50 @@ var PolicyInjector = class {
1721
2443
  };
1722
2444
  case "mutation":
1723
2445
  return this.injectMutation(root);
1724
- case "action":
1725
- return root;
1726
2446
  }
1727
2447
  }
1728
2448
  injectMutation(root) {
1729
2449
  const table = root.table;
1730
- const writePolicy = this.write(table);
1731
2450
  const returning = root.returning ? this.injectSelection(root.returning, table) : root.returning;
1732
2451
  if (root.op === "insert" || root.op === "insertOne") {
1733
- if (predContainsExists(writePolicy)) {
2452
+ const insertPolicy = this.insert(table);
2453
+ if (predContainsExists(insertPolicy)) {
1734
2454
  throw new DocumentCompileError(
1735
- `table '${table}' cannot be inserted into: v2.0 insert checks are claims + inserted-values only, but its write policy traverses a link (exists)`,
2455
+ `table '${table}' cannot be inserted into: v2.0 insert checks are claims + inserted-values only, but its insert policy traverses a link (exists)`,
1736
2456
  "insert-check-unrepresentable"
1737
2457
  );
1738
2458
  }
1739
2459
  const pins = (this.ctx.pins.get(table) ?? []).map(
1740
2460
  (pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
1741
2461
  );
1742
- return { ...root, pinnedColumns: pins, insertCheck: writePolicy, returning };
2462
+ if (root.onConflict && this.ctx.splitWritePolicyTables.has(table)) {
2463
+ throw new DocumentCompileError(
2464
+ `table '${table}' cannot use onConflict while insert and update policies are split \u2014 use separate insert and update mutations`,
2465
+ "on-conflict-split-policies"
2466
+ );
2467
+ }
2468
+ return { ...root, pinnedColumns: pins, insertCheck: insertPolicy, returning };
1743
2469
  }
1744
- const where = andPred(writePolicy, root.where ? this.injectDocPred(root.where, table) : void 0);
2470
+ const updatePolicy = this.update(table);
2471
+ const where = andPred(updatePolicy, root.where ? this.injectDocPred(root.where, table) : void 0);
1745
2472
  if (root.op === "update" || root.op === "updateByPk") {
1746
- const check = predContainsExists(writePolicy) ? {} : { insertCheck: writePolicy };
2473
+ const check = predContainsExists(updatePolicy) ? {} : { insertCheck: updatePolicy };
1747
2474
  return { ...root, where, ...check, returning };
1748
2475
  }
2476
+ if (root.op === "cowOverride") {
2477
+ const insertPolicy = this.insert(table);
2478
+ if (predContainsExists(insertPolicy)) {
2479
+ throw new DocumentCompileError(
2480
+ `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)`,
2481
+ "insert-check-unrepresentable"
2482
+ );
2483
+ }
2484
+ const pins = (this.ctx.pins.get(table) ?? []).map(
2485
+ (pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
2486
+ );
2487
+ const check = predContainsExists(updatePolicy) ? {} : { insertCheck: updatePolicy };
2488
+ return { ...root, pinnedColumns: pins, cowCopyCheck: insertPolicy, where, ...check, returning };
2489
+ }
1749
2490
  return { ...root, where, returning };
1750
2491
  }
1751
2492
  };
@@ -1791,7 +2532,6 @@ function collectTableSet(roots, ctx) {
1791
2532
  }
1792
2533
  };
1793
2534
  for (const root of roots) {
1794
- if (root.kind === "action") continue;
1795
2535
  tables.add(root.table);
1796
2536
  if (root.kind === "select" || root.kind === "byPk") {
1797
2537
  visitPred(root.where, root.table);
@@ -1802,8 +2542,10 @@ function collectTableSet(roots, ctx) {
1802
2542
  if (root.nodes) visitSelection(root.nodes, root.table);
1803
2543
  }
1804
2544
  if (root.kind === "mutation") {
2545
+ if (root.cowSynced !== void 0) tables.add(root.cowSynced);
1805
2546
  if (root.where) visitPred(root.where, root.table);
1806
2547
  if (root.insertCheck) visitPred(root.insertCheck, root.table);
2548
+ if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
1807
2549
  if (root.returning) visitSelection(root.returning, root.table);
1808
2550
  }
1809
2551
  }
@@ -1870,6 +2612,7 @@ function platformUserMeta() {
1870
2612
  table: "users_directory",
1871
2613
  schema: "platform",
1872
2614
  typename: PLATFORM_USER_TYPENAME,
2615
+ claimsGated: false,
1873
2616
  columns: columns.map((c) => ({ ...c })),
1874
2617
  primaryKey: ["sub"],
1875
2618
  links: [],
@@ -1912,15 +2655,30 @@ function buildCompileContext(rawCanonical) {
1912
2655
  const canonical = normalizeCanonical(canonicalDefinitionSchema.parse(rawCanonical));
1913
2656
  const diagnostics = [];
1914
2657
  const enums = new Map(canonical.enums.map((e) => [e.name, e]));
2658
+ const relations = canonicalRelations(canonical);
2659
+ const viewNames = new Set((canonical.views ?? []).map((view) => view.view));
1915
2660
  const columnEntries = /* @__PURE__ */ new Map();
1916
2661
  const tables = {};
1917
- const usesPlatformUsers = canonical.tables.some((t2) => t2.links.some((l) => l.kind === "toPlatformUser"));
2662
+ const usesPlatformUsers = relations.some((t2) => t2.links.some((l) => l.kind === "toPlatformUser"));
1918
2663
  const platformMeta = usesPlatformUsers ? platformUserMeta() : void 0;
1919
2664
  if (platformMeta) tables[PLATFORM_USER_TABLE] = platformMeta;
1920
- for (const table of canonical.tables) {
2665
+ const syncedTableNames = new Set(canonical.tables.filter((t2) => t2.source !== void 0).map((t2) => t2.table));
2666
+ const viewDependencies = new Map((canonical.views ?? []).map((view) => [view.view, view.dependencies]));
2667
+ const claimsGatedMemo = /* @__PURE__ */ new Map();
2668
+ const relationIsClaimsGated = (name) => {
2669
+ if (syncedTableNames.has(name)) return true;
2670
+ const memo = claimsGatedMemo.get(name);
2671
+ if (memo !== void 0) return memo;
2672
+ claimsGatedMemo.set(name, false);
2673
+ const gated = (viewDependencies.get(name) ?? []).some(relationIsClaimsGated);
2674
+ claimsGatedMemo.set(name, gated);
2675
+ return gated;
2676
+ };
2677
+ for (const table of relations) {
2678
+ const isView = viewNames.has(table.table);
1921
2679
  const entries = resolveColumnEntries(table, enums, diagnostics);
1922
2680
  columnEntries.set(table.table, entries);
1923
- const pkRefs = table.primaryKey ?? ["id"];
2681
+ const pkRefs = table.primaryKey ?? (isView ? [] : ["id"]);
1924
2682
  const primaryKey2 = [];
1925
2683
  for (const ref of pkRefs) {
1926
2684
  const resolved = logicalColumn(entries, ref);
@@ -1941,7 +2699,7 @@ function buildCompileContext(rawCanonical) {
1941
2699
  for (const column of table.columns) {
1942
2700
  if (column.unique) {
1943
2701
  const logical = logicalColumn(entries, column.columnName);
1944
- constraints.push({ name: `${table.table}_${column.columnName}_key`, columns: [logical] });
2702
+ constraints.push({ name: `${table.table}_${column.columnName}_unique`, columns: [logical] });
1945
2703
  }
1946
2704
  }
1947
2705
  for (const index3 of table.indexes) {
@@ -1953,20 +2711,23 @@ function buildCompileContext(rawCanonical) {
1953
2711
  table: table.table,
1954
2712
  schema: ormSchemaName(canonical.namespace),
1955
2713
  typename: typeNameForTable(table.table),
2714
+ // Synced twins (and views reading them, resolved transitively below)
2715
+ // are claims-gated — see tableMetaIrSchema.claimsGated.
2716
+ claimsGated: relationIsClaimsGated(table.table),
1956
2717
  columns: entries.map(({ deprecated: _deprecated, ...column }) => column),
1957
- primaryKey: primaryKey2.length > 0 ? primaryKey2 : ["id"],
2718
+ primaryKey: isView ? primaryKey2 : primaryKey2.length > 0 ? primaryKey2 : ["id"],
1958
2719
  links: [],
1959
2720
  filterable: entries.map((c) => c.name),
1960
2721
  orderable: entries.filter((c) => c.type !== "jsonb").map((c) => c.name),
1961
- insertable: active.map((c) => c.name),
1962
- settable: active.map((c) => c.name),
1963
- appendable: active.filter((c) => c.type === "jsonb").map((c) => c.name),
1964
- constraints,
2722
+ insertable: isView ? [] : active.map((c) => c.name),
2723
+ settable: isView ? [] : active.map((c) => c.name),
2724
+ appendable: isView ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
2725
+ constraints: isView ? [] : constraints,
1965
2726
  dynamicFilterMaxDepth: 6,
1966
2727
  dynamicFilterMaxNodes: 64
1967
2728
  };
1968
2729
  }
1969
- for (const table of canonical.tables) {
2730
+ for (const table of relations) {
1970
2731
  const meta = tables[table.table];
1971
2732
  const entries = columnEntries.get(table.table);
1972
2733
  for (const link of table.links) {
@@ -2001,17 +2762,17 @@ function buildCompileContext(rawCanonical) {
2001
2762
  }
2002
2763
  const readPolicies = /* @__PURE__ */ new Map();
2003
2764
  const writePolicies = /* @__PURE__ */ new Map();
2765
+ const insertPolicies = /* @__PURE__ */ new Map();
2766
+ const updatePolicies = /* @__PURE__ */ new Map();
2004
2767
  const pins = /* @__PURE__ */ new Map();
2768
+ const splitWritePolicyTables = /* @__PURE__ */ new Set();
2005
2769
  const resolver = { tables };
2006
- for (const table of canonical.tables) {
2007
- for (const [access, store] of [
2008
- ["read", readPolicies],
2009
- ["write", writePolicies]
2010
- ]) {
2770
+ for (const table of relations) {
2771
+ const isView = viewNames.has(table.table);
2772
+ const compilePolicyFor = (access, store) => {
2011
2773
  const expression = table.policies[access];
2012
2774
  if (expression === void 0) {
2013
- store.set(table.table, CONST_TRUE);
2014
- continue;
2775
+ return;
2015
2776
  }
2016
2777
  try {
2017
2778
  store.set(table.table, compilePolicy({ expression, table: table.table, resolver }));
@@ -2024,37 +2785,117 @@ function buildCompileContext(rawCanonical) {
2024
2785
  table: table.table
2025
2786
  });
2026
2787
  store.set(table.table, CONST_TRUE);
2027
- continue;
2788
+ return;
2028
2789
  }
2029
2790
  throw error;
2030
2791
  }
2792
+ };
2793
+ compilePolicyFor("read", readPolicies);
2794
+ compilePolicyFor("write", writePolicies);
2795
+ compilePolicyFor("insert", insertPolicies);
2796
+ compilePolicyFor("update", updatePolicies);
2797
+ if (isView) {
2798
+ writePolicies.set(table.table, CONST_FALSE);
2799
+ insertPolicies.set(table.table, CONST_FALSE);
2800
+ updatePolicies.set(table.table, CONST_FALSE);
2801
+ pins.set(table.table, []);
2802
+ continue;
2031
2803
  }
2032
- const tablePins = collectPinnedColumns(writePolicies.get(table.table));
2804
+ if (table.policies.insert !== void 0 || table.policies.update !== void 0) {
2805
+ splitWritePolicyTables.add(table.table);
2806
+ }
2807
+ const fallbackWrite = writePolicies.get(table.table);
2808
+ const hasSplitPolicy = table.policies.insert !== void 0 || table.policies.update !== void 0;
2809
+ if (!insertPolicies.has(table.table)) {
2810
+ if (fallbackWrite !== void 0) insertPolicies.set(table.table, fallbackWrite);
2811
+ else if (hasSplitPolicy) insertPolicies.set(table.table, CONST_FALSE);
2812
+ else insertPolicies.set(table.table, CONST_TRUE);
2813
+ }
2814
+ if (!updatePolicies.has(table.table)) {
2815
+ if (fallbackWrite !== void 0) updatePolicies.set(table.table, fallbackWrite);
2816
+ else if (hasSplitPolicy) updatePolicies.set(table.table, CONST_FALSE);
2817
+ else updatePolicies.set(table.table, CONST_TRUE);
2818
+ }
2819
+ const tablePins = collectPinnedColumns(insertPolicies.get(table.table));
2033
2820
  pins.set(table.table, tablePins);
2034
2821
  const pinned = new Set(tablePins.map((pin) => pin.column));
2035
2822
  const meta = tables[table.table];
2036
2823
  meta.insertable = meta.insertable.filter((column) => !pinned.has(column));
2037
- meta.settable = meta.settable.filter((column) => !pinned.has(column));
2038
2824
  }
2039
2825
  if (platformMeta) {
2040
2826
  readPolicies.set(PLATFORM_USER_TABLE, CONST_TRUE);
2041
2827
  writePolicies.set(PLATFORM_USER_TABLE, { kind: "const", value: false });
2828
+ insertPolicies.set(PLATFORM_USER_TABLE, { kind: "const", value: false });
2829
+ updatePolicies.set(PLATFORM_USER_TABLE, { kind: "const", value: false });
2042
2830
  pins.set(PLATFORM_USER_TABLE, []);
2043
2831
  }
2044
2832
  const tableByTypename = new Map(Object.entries(tables).map(([key, meta]) => [meta.typename, key]));
2045
2833
  const queryRoots = /* @__PURE__ */ new Map();
2046
2834
  const mutationRoots = /* @__PURE__ */ new Map();
2047
- for (const table of canonical.tables) {
2835
+ for (const table of relations) {
2048
2836
  const roots = queryRootNames(table.table);
2049
2837
  queryRoots.set(roots.select, { kind: "select", table: table.table });
2050
- queryRoots.set(roots.byPk, { kind: "byPk", table: table.table });
2838
+ if ((tables[table.table]?.primaryKey.length ?? 0) > 0) {
2839
+ queryRoots.set(roots.byPk, { kind: "byPk", table: table.table });
2840
+ }
2051
2841
  queryRoots.set(roots.aggregate, { kind: "aggregate", table: table.table });
2052
- if (table.source === void 0) {
2842
+ const hasWritePolicy = table.policies.write !== void 0 || table.policies.insert !== void 0 || table.policies.update !== void 0;
2843
+ if (!viewNames.has(table.table) && table.source === void 0 && hasWritePolicy) {
2053
2844
  for (const [op, field] of Object.entries(mutationRootNames(table.table))) {
2054
2845
  mutationRoots.set(field, { op, table: table.table });
2055
2846
  }
2056
2847
  }
2057
2848
  }
2849
+ for (const table of relations) {
2850
+ for (const mutation of table.cowMutations ?? []) {
2851
+ const hasWritePolicy = table.policies.write !== void 0 || table.policies.insert !== void 0 || table.policies.update !== void 0;
2852
+ if (!hasWritePolicy) {
2853
+ diagnostics.push({
2854
+ severity: "error",
2855
+ code: "cow-mutation-unpoliced",
2856
+ message: `cow mutation '${mutation.name}' on '${table.table}' requires a write (or insert/update) policy on the overlay table`,
2857
+ table: table.table
2858
+ });
2859
+ continue;
2860
+ }
2861
+ const field = camelCase(mutation.name);
2862
+ if (mutationRoots.has(field) || queryRoots.has(field)) {
2863
+ diagnostics.push({
2864
+ severity: "error",
2865
+ code: "cow-mutation-name-collision",
2866
+ message: `cow mutation '${mutation.name}' on '${table.table}' collides with the generated root '${field}'`,
2867
+ table: table.table
2868
+ });
2869
+ continue;
2870
+ }
2871
+ if (mutation.kind === "override") {
2872
+ const pinned = new Set((pins.get(table.table) ?? []).map((pin) => pin.column));
2873
+ const covered = /* @__PURE__ */ new Set([mutation.pk, ...mutation.copy, ...pinned]);
2874
+ const gaps = table.columns.filter((column) => {
2875
+ if (covered.has(column.name)) return false;
2876
+ const hasDefault = column.default !== void 0 || column.defaultFn !== void 0;
2877
+ return column.nullable === false && !hasDefault;
2878
+ });
2879
+ if (gaps.length > 0) {
2880
+ diagnostics.push({
2881
+ severity: "error",
2882
+ code: "cow-copy-uncovered-column",
2883
+ 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`,
2884
+ table: table.table
2885
+ });
2886
+ continue;
2887
+ }
2888
+ }
2889
+ mutationRoots.set(field, {
2890
+ op: mutation.kind === "override" ? "cowOverride" : "cowRevert",
2891
+ table: table.table,
2892
+ synced: mutation.synced,
2893
+ pk: mutation.pk,
2894
+ set: mutation.set,
2895
+ copyColumns: mutation.copy
2896
+ });
2897
+ }
2898
+ }
2058
2899
  const enumsByTypeName = new Map(canonical.enums.map((e) => [enumTypeName(e.name), e]));
2059
2900
  return {
2060
2901
  ctx: {
@@ -2066,7 +2907,10 @@ function buildCompileContext(rawCanonical) {
2066
2907
  enumsByTypeName,
2067
2908
  readPolicies,
2068
2909
  writePolicies,
2069
- pins
2910
+ insertPolicies,
2911
+ updatePolicies,
2912
+ pins,
2913
+ splitWritePolicyTables
2070
2914
  },
2071
2915
  diagnostics
2072
2916
  };
@@ -2618,6 +3462,9 @@ var OperationLowerer = class {
2618
3462
  const alias = node.alias?.value ?? name;
2619
3463
  const rootInfo = this.ctx.mutationRoots.get(name);
2620
3464
  if (!rootInfo) fail(`unknown mutation root '${name}'`, "unknown-root", [...this.ctx.mutationRoots.keys()]);
3465
+ if (rootInfo.op === "cowOverride" || rootInfo.op === "cowRevert") {
3466
+ return this.lowerCowRoot(node, rootInfo);
3467
+ }
2621
3468
  const { op, table } = rootInfo;
2622
3469
  const args = argsByName(node);
2623
3470
  if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
@@ -2687,6 +3534,55 @@ var OperationLowerer = class {
2687
3534
  }
2688
3535
  }
2689
3536
  }
3537
+ /**
3538
+ * Copy-on-write roots: flat args — the pk plus (override only) every
3539
+ * declared set column. Overrides are full snapshots of the set columns, so
3540
+ * every set argument must be supplied explicitly (pass null to clear a
3541
+ * nullable column); the executor generates the copy/update/delete SQL from
3542
+ * the structured fields, never from authored statements.
3543
+ */
3544
+ lowerCowRoot(node, rootInfo) {
3545
+ const name = node.name.value;
3546
+ const alias = node.alias?.value ?? name;
3547
+ const { table } = rootInfo;
3548
+ if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
3549
+ const meta = this.meta(table);
3550
+ const args = argsByName(node);
3551
+ const pkArg = args.get(rootInfo.pk);
3552
+ if (!pkArg) fail(`'${name}' is missing primary-key argument '${rootInfo.pk}'`, "missing-pk-argument", [rootInfo.pk]);
3553
+ const pk = [{ column: rootInfo.pk, value: this.lowerValue(pkArg.value) }];
3554
+ const base = {
3555
+ kind: "mutation",
3556
+ op: rootInfo.op,
3557
+ alias,
3558
+ table,
3559
+ pinnedColumns: [],
3560
+ affectedRows: false,
3561
+ pk,
3562
+ cowSynced: rootInfo.synced,
3563
+ returning: this.lowerSelection(node.selectionSet, table)
3564
+ };
3565
+ if (rootInfo.op === "cowRevert") {
3566
+ assertKnownArgs(node, [rootInfo.pk]);
3567
+ return base;
3568
+ }
3569
+ assertKnownArgs(node, [rootInfo.pk, ...rootInfo.set]);
3570
+ const entries = rootInfo.set.map((column) => {
3571
+ const arg = args.get(column);
3572
+ if (!arg) {
3573
+ fail(
3574
+ `'${name}' is missing set argument '${column}' \u2014 a cow override writes every declared set column (pass null to clear a nullable one)`,
3575
+ "missing-argument",
3576
+ [...rootInfo.set]
3577
+ );
3578
+ }
3579
+ if (!meta.settable.includes(column)) {
3580
+ fail(`column '${table}.${column}' is not updatable (pinned by the write policy or deprecated)`, "column-not-settable", meta.settable);
3581
+ }
3582
+ return { column, value: this.lowerValue(arg.value) };
3583
+ });
3584
+ return { ...base, set: { entries }, cowCopyColumns: [...rootInfo.copyColumns] };
3585
+ }
2690
3586
  lowerMutationResponse(selectionSet, table) {
2691
3587
  let affectedRows = false;
2692
3588
  let returning = null;
@@ -2780,8 +3676,8 @@ var OperationLowerer = class {
2780
3676
  if (inc.value.kind === graphql.Kind.VARIABLE) {
2781
3677
  fail("_inc has no dynamic input type \u2014 inline the columns with variable leaf values", "invalid-inc");
2782
3678
  }
2783
- const numeric3 = meta.columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type) && meta.settable.includes(c.name)).map((c) => c.name);
2784
- result.inc = { entries: this.lowerColumnEntries(inc.value, table, numeric3, "_inc") };
3679
+ const numeric4 = meta.columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type) && meta.settable.includes(c.name)).map((c) => c.name);
3680
+ result.inc = { entries: this.lowerColumnEntries(inc.value, table, numeric4, "_inc") };
2785
3681
  }
2786
3682
  if (!result.set && !result.append && !result.inc) {
2787
3683
  fail(`an update on '${table}' needs at least one of _set/_append/_inc`, "missing-argument");
@@ -3132,13 +4028,17 @@ function generateNamespaceTypes(definition) {
3132
4028
  `export type ${typesInterfaceName(definition.namespace)} = {`
3133
4029
  ];
3134
4030
  for (const table of definition.tables) {
3135
- if (table.source) {
4031
+ const columns = Object.entries(table.columns ?? {});
4032
+ if (table.source && columns.length === 0) {
3136
4033
  lines.push(` /** Synced from ${table.source} \u2014 columns reflect the gold source. */`);
3137
4034
  lines.push(` ${table.table}: Record<string, unknown>`);
3138
4035
  continue;
3139
4036
  }
4037
+ if (table.source) {
4038
+ lines.push(` /** Synced from ${table.source} \u2014 read-only typed contract. */`);
4039
+ }
3140
4040
  lines.push(` ${table.table}: {`);
3141
- for (const [name, column] of Object.entries(table.columns ?? {})) {
4041
+ for (const [name, column] of columns) {
3142
4042
  if (column.deprecated) continue;
3143
4043
  lines.push(` ${name}: ${columnWireType(column)}`);
3144
4044
  }
@@ -3604,8 +4504,8 @@ var actionSchema = zod.z.strictObject({
3604
4504
  }
3605
4505
  }
3606
4506
  });
3607
- function extractNamedParams(sql3) {
3608
- const withoutStrings = sql3.replace(QUOTED_STRING_PATTERN, "''");
4507
+ function extractNamedParams(sql4) {
4508
+ const withoutStrings = sql4.replace(QUOTED_STRING_PATTERN, "''");
3609
4509
  const names = [...withoutStrings.matchAll(NAMED_PARAM_PATTERN)].map((match) => match[1]);
3610
4510
  return [...new Set(names)];
3611
4511
  }
@@ -3659,7 +4559,9 @@ var indexSchema = zod.z.union([
3659
4559
  ]).transform((index3) => Array.isArray(index3) ? { columns: index3 } : index3);
3660
4560
  var policiesSchema = zod.z.strictObject({
3661
4561
  read: parsedPredicateSchema.optional(),
3662
- write: parsedPredicateSchema.optional()
4562
+ write: parsedPredicateSchema.optional(),
4563
+ insert: parsedPredicateSchema.optional(),
4564
+ update: parsedPredicateSchema.optional()
3663
4565
  });
3664
4566
  var tableFileSchema = zod.z.strictObject({
3665
4567
  namespace: namespaceSchema,
@@ -3695,7 +4597,6 @@ var policyName = (table, suffix) => `${table}_${suffix}`;
3695
4597
  var foreignKeyName = (table, link) => `fk_${table}_${link}`;
3696
4598
  function validateSyncedTable(table, ctx) {
3697
4599
  const forbidden = [
3698
- ["columns", "synced tables reflect their source; columns are not declared"],
3699
4600
  ["actions", "synced tables are read-only; actions are not allowed"],
3700
4601
  ["indexes", "synced tables are system-owned; indexes are not allowed"]
3701
4602
  ];
@@ -3705,6 +4606,39 @@ function validateSyncedTable(table, ctx) {
3705
4606
  if (table.policies?.write) {
3706
4607
  ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "write"] });
3707
4608
  }
4609
+ if (table.policies?.insert) {
4610
+ ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "insert"] });
4611
+ }
4612
+ if (table.policies?.update) {
4613
+ ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "update"] });
4614
+ }
4615
+ const columns = table.columns ?? {};
4616
+ const names = Object.keys(columns);
4617
+ if (names.length === 0) return;
4618
+ const primaryKeys = names.filter((name) => columns[name]?.primary_key);
4619
+ if (primaryKeys.length !== 1) {
4620
+ ctx.addIssue({ code: "custom", message: "exactly one column must be primary_key", path: ["columns"] });
4621
+ }
4622
+ validateRenames(columns, names, ctx);
4623
+ for (const name of names) {
4624
+ const column = columns[name];
4625
+ if (!column) continue;
4626
+ const cast = column.cast ? castParts(column.cast) : null;
4627
+ if (column.cast && cast?.column !== name) {
4628
+ ctx.addIssue({
4629
+ code: "custom",
4630
+ message: "cast must reference the column it is declared on",
4631
+ path: ["columns", name, "cast"]
4632
+ });
4633
+ }
4634
+ if (cast && cast.type !== column.type) {
4635
+ ctx.addIssue({
4636
+ code: "custom",
4637
+ message: `cast target '${cast.type}' must match the declared type '${column.type}'`,
4638
+ path: ["columns", name, "cast"]
4639
+ });
4640
+ }
4641
+ }
3708
4642
  }
3709
4643
  function validateRenames(columns, names, ctx) {
3710
4644
  const renameSources = /* @__PURE__ */ new Map();
@@ -3782,7 +4716,7 @@ function validateColumnTable(table, ctx) {
3782
4716
  function validateLinks(table, ctx) {
3783
4717
  const columnNames = Object.keys(table.columns ?? {});
3784
4718
  for (const [name, link] of Object.entries(table.links ?? {})) {
3785
- if (!table.source && !columnNames.includes(link.on)) {
4719
+ if (columnNames.length > 0 && !columnNames.includes(link.on)) {
3786
4720
  ctx.addIssue({ code: "custom", message: `link references unknown column '${link.on}'`, path: ["links", name, "on"] });
3787
4721
  }
3788
4722
  if (link.enforce && link.to.includes(".")) {
@@ -3855,10 +4789,10 @@ var TYPE_GROUPS = {
3855
4789
  };
3856
4790
  var columnTypeGroup = (type) => TYPE_GROUPS[type];
3857
4791
  function validatePolicyColumns(table, ctx) {
3858
- if (table.source) return;
3859
4792
  const columns = table.columns ?? {};
3860
4793
  const columnNames = Object.keys(columns);
3861
- for (const kind of ["read", "write"]) {
4794
+ if (columnNames.length === 0) return;
4795
+ for (const kind of ["read", "write", "insert", "update"]) {
3862
4796
  const predicate = table.policies?.[kind];
3863
4797
  if (!predicate) continue;
3864
4798
  for (const column of referencedColumns(predicate.ast)) {
@@ -4013,10 +4947,15 @@ function policyStatements(schema, table) {
4013
4947
  if (read) {
4014
4948
  statements.push(`CREATE POLICY ${policyName2("read")} ON ${qualified} FOR SELECT USING (${compilePredicate(read.ast, columnTypes)})`);
4015
4949
  }
4016
- const write = table.policies?.write;
4017
- if (write) {
4018
- const predicate = compilePredicate(write.ast, columnTypes);
4019
- statements.push(`CREATE POLICY ${policyName2("insert")} ON ${qualified} FOR INSERT WITH CHECK (${predicate})`);
4950
+ const insertPolicy = table.policies?.insert ?? table.policies?.write;
4951
+ const updatePolicy = table.policies?.update ?? table.policies?.write;
4952
+ if (insertPolicy) {
4953
+ statements.push(
4954
+ `CREATE POLICY ${policyName2("insert")} ON ${qualified} FOR INSERT WITH CHECK (${compilePredicate(insertPolicy.ast, columnTypes)})`
4955
+ );
4956
+ }
4957
+ if (updatePolicy) {
4958
+ const predicate = compilePredicate(updatePolicy.ast, columnTypes);
4020
4959
  statements.push(
4021
4960
  `CREATE POLICY ${policyName2("update")} ON ${qualified} FOR UPDATE USING (${predicate}) WITH CHECK (${predicate})`
4022
4961
  );
@@ -4240,11 +5179,268 @@ function buildDrizzleEntitiesV2(definition) {
4240
5179
  imports[table.table] = drizzleTable;
4241
5180
  tables[table.table] = drizzleTable;
4242
5181
  }
5182
+ for (const view of definition.views ?? []) {
5183
+ const columns = Object.fromEntries(
5184
+ view.columns.map((column) => [column.columnName, buildViewColumnV2({ view: view.view, column, enums })])
5185
+ );
5186
+ imports[view.view] = schema.view(view.view, columns).as(
5187
+ drizzleOrm.sql.raw(
5188
+ rewriteSyncedSourceRelationsInViewQuery({
5189
+ query: view.query,
5190
+ canonical: definition,
5191
+ schemaName,
5192
+ dependencies: view.dependencies
5193
+ })
5194
+ )
5195
+ );
5196
+ }
4243
5197
  return { imports, tables, schemaName };
4244
5198
  }
5199
+ function rewriteSyncedSourceRelationsInViewQuery({
5200
+ query,
5201
+ canonical,
5202
+ schemaName,
5203
+ dependencies
5204
+ }) {
5205
+ const tablesBySource = /* @__PURE__ */ new Map();
5206
+ for (const table of canonical.tables) {
5207
+ if (!table.source) continue;
5208
+ tablesBySource.set(table.source, [...tablesBySource.get(table.source) ?? [], table.table]);
5209
+ }
5210
+ const declared = new Set(dependencies ?? []);
5211
+ const boundTableBySource = /* @__PURE__ */ new Map();
5212
+ for (const [source, tables] of tablesBySource) {
5213
+ if (tables.length === 1) {
5214
+ boundTableBySource.set(source, tables[0]);
5215
+ continue;
5216
+ }
5217
+ const [sourceSchema, sourceName] = source.split(".");
5218
+ if (!sourceSchema || !sourceName) continue;
5219
+ const referenced = sqlReferencesQualifiedRelation(query, sourceSchema, sourceName);
5220
+ if (!referenced) continue;
5221
+ const declaredMatches = tables.filter((table) => declared.has(table));
5222
+ if (declaredMatches.length === 1) {
5223
+ boundTableBySource.set(source, declaredMatches[0]);
5224
+ continue;
5225
+ }
5226
+ throw new OrmDefinitionError(
5227
+ `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`
5228
+ );
5229
+ }
5230
+ return transformSqlOutsideLiterals(query, (segment) => {
5231
+ let rewritten = segment;
5232
+ for (const table of canonical.tables) {
5233
+ if (!table.source) continue;
5234
+ const boundTable = boundTableBySource.get(table.source);
5235
+ if (boundTable === void 0 || boundTable !== table.table) continue;
5236
+ const [sourceSchema, sourceName] = table.source.split(".");
5237
+ if (!sourceSchema || !sourceName) continue;
5238
+ rewritten = replaceQualifiedRelation(
5239
+ rewritten,
5240
+ sourceSchema,
5241
+ sourceName,
5242
+ schemaName,
5243
+ table.table
5244
+ );
5245
+ }
5246
+ return rewritten;
5247
+ });
5248
+ }
5249
+ function transformSqlOutsideLiterals(sql4, transform) {
5250
+ const out = [];
5251
+ let state = { kind: "normal" };
5252
+ let i = 0;
5253
+ let codeStart = 0;
5254
+ const emitCode = (end) => {
5255
+ if (end > codeStart) out.push(transform(sql4.slice(codeStart, end)));
5256
+ codeStart = end;
5257
+ };
5258
+ while (i < sql4.length) {
5259
+ const c = sql4[i];
5260
+ const next = i + 1 < sql4.length ? sql4[i + 1] : "";
5261
+ switch (state.kind) {
5262
+ case "normal": {
5263
+ if (c === "-" && next === "-") {
5264
+ emitCode(i);
5265
+ state = { kind: "line-comment" };
5266
+ out.push("--");
5267
+ i += 2;
5268
+ codeStart = i;
5269
+ break;
5270
+ }
5271
+ if (c === "/" && next === "*") {
5272
+ emitCode(i);
5273
+ state = { kind: "block-comment", depth: 1 };
5274
+ out.push("/*");
5275
+ i += 2;
5276
+ codeStart = i;
5277
+ break;
5278
+ }
5279
+ if ((c === "E" || c === "e") && next === "'") {
5280
+ const prev = i > 0 ? sql4[i - 1] : "";
5281
+ if (!/[A-Za-z0-9_]/.test(prev)) {
5282
+ emitCode(i);
5283
+ state = { kind: "e-single" };
5284
+ out.push("E'");
5285
+ i += 2;
5286
+ codeStart = i;
5287
+ break;
5288
+ }
5289
+ }
5290
+ if (c === "'") {
5291
+ emitCode(i);
5292
+ state = { kind: "single" };
5293
+ out.push("'");
5294
+ i += 1;
5295
+ codeStart = i;
5296
+ break;
5297
+ }
5298
+ if (c === "$") {
5299
+ const prev = i > 0 ? sql4[i - 1] : "";
5300
+ if (!/[A-Za-z0-9_$]/.test(prev)) {
5301
+ const tagMatch = /^\$([A-Za-z_][A-Za-z0-9_]*)?\$/.exec(sql4.slice(i));
5302
+ if (tagMatch) {
5303
+ emitCode(i);
5304
+ state = { kind: "dollar", tag: tagMatch[1] ?? "" };
5305
+ out.push(tagMatch[0]);
5306
+ i += tagMatch[0].length;
5307
+ codeStart = i;
5308
+ break;
5309
+ }
5310
+ }
5311
+ }
5312
+ i += 1;
5313
+ break;
5314
+ }
5315
+ case "single": {
5316
+ if (c === "'" && next === "'") {
5317
+ out.push("''");
5318
+ i += 2;
5319
+ break;
5320
+ }
5321
+ if (c === "'") {
5322
+ out.push("'");
5323
+ state = { kind: "normal" };
5324
+ i += 1;
5325
+ codeStart = i;
5326
+ break;
5327
+ }
5328
+ out.push(c);
5329
+ i += 1;
5330
+ break;
5331
+ }
5332
+ case "e-single": {
5333
+ if (c === "\\" && i + 1 < sql4.length) {
5334
+ out.push(c, sql4[i + 1]);
5335
+ i += 2;
5336
+ break;
5337
+ }
5338
+ if (c === "'" && next === "'") {
5339
+ out.push("''");
5340
+ i += 2;
5341
+ break;
5342
+ }
5343
+ if (c === "'") {
5344
+ out.push("'");
5345
+ state = { kind: "normal" };
5346
+ i += 1;
5347
+ codeStart = i;
5348
+ break;
5349
+ }
5350
+ out.push(c);
5351
+ i += 1;
5352
+ break;
5353
+ }
5354
+ case "dollar": {
5355
+ const closer = `$${state.tag}$`;
5356
+ if (sql4.startsWith(closer, i)) {
5357
+ out.push(closer);
5358
+ state = { kind: "normal" };
5359
+ i += closer.length;
5360
+ codeStart = i;
5361
+ break;
5362
+ }
5363
+ out.push(c);
5364
+ i += 1;
5365
+ break;
5366
+ }
5367
+ case "line-comment": {
5368
+ if (c === "\n") {
5369
+ out.push("\n");
5370
+ state = { kind: "normal" };
5371
+ i += 1;
5372
+ codeStart = i;
5373
+ break;
5374
+ }
5375
+ out.push(c);
5376
+ i += 1;
5377
+ break;
5378
+ }
5379
+ case "block-comment": {
5380
+ if (c === "/" && next === "*") {
5381
+ out.push("/*");
5382
+ state = { kind: "block-comment", depth: state.depth + 1 };
5383
+ i += 2;
5384
+ break;
5385
+ }
5386
+ if (c === "*" && next === "/") {
5387
+ out.push("*/");
5388
+ const newDepth = state.depth - 1;
5389
+ state = newDepth === 0 ? { kind: "normal" } : { kind: "block-comment", depth: newDepth };
5390
+ i += 2;
5391
+ if (state.kind === "normal") codeStart = i;
5392
+ break;
5393
+ }
5394
+ out.push(c);
5395
+ i += 1;
5396
+ break;
5397
+ }
5398
+ }
5399
+ }
5400
+ emitCode(sql4.length);
5401
+ return out.join("");
5402
+ }
5403
+ function escapeRegExp(value) {
5404
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5405
+ }
5406
+ function sqlReferencesQualifiedRelation(sql4, sourceSchema, sourceName) {
5407
+ let referenced = false;
5408
+ transformSqlOutsideLiterals(sql4, (segment) => {
5409
+ if (qualifiedRelationPattern(sourceSchema, sourceName).test(segment)) referenced = true;
5410
+ return segment;
5411
+ });
5412
+ return referenced;
5413
+ }
5414
+ function qualifiedRelationPattern(sourceSchema, sourceName) {
5415
+ const schemaPattern = `(?:"${escapeRegExp(sourceSchema)}"|${escapeRegExp(sourceSchema)})`;
5416
+ const namePattern = `(?:"${escapeRegExp(sourceName)}"|${escapeRegExp(sourceName)})`;
5417
+ return new RegExp(`(?<![\\w."])${schemaPattern}\\.${namePattern}(?![\\w"])`, "g");
5418
+ }
5419
+ function replaceQualifiedRelation(sql4, sourceSchema, sourceName, boundSchema, boundTable) {
5420
+ return sql4.replace(qualifiedRelationPattern(sourceSchema, sourceName), `"${boundSchema}"."${boundTable}"`);
5421
+ }
4245
5422
  function generateSnapshotV2(definition, previousId3) {
4246
5423
  const { imports, schemaName } = buildDrizzleEntitiesV2(definition);
4247
- return api.generateDrizzleJson(imports, previousId3, [schemaName]);
5424
+ const snapshot = api.generateDrizzleJson(imports, previousId3, [schemaName]);
5425
+ if ((definition.views?.length ?? 0) > 0) {
5426
+ const extended = snapshot;
5427
+ extended._sequenceOrm = {
5428
+ viewDependencies: Object.fromEntries(
5429
+ (definition.views ?? []).map((view) => [view.view, [...view.dependencies].sort()])
5430
+ )
5431
+ };
5432
+ }
5433
+ return snapshot;
5434
+ }
5435
+ function buildViewColumnV2({
5436
+ view,
5437
+ column,
5438
+ enums
5439
+ }) {
5440
+ const factory = resolveColumnFactory(view, column, enums);
5441
+ let builder = factory(column.columnName);
5442
+ if (!column.nullable) builder = builder.notNull();
5443
+ return builder;
4248
5444
  }
4249
5445
  function buildTable2(schema, table, built, enums, byName) {
4250
5446
  const primaryKeyColumns2 = table.primaryKey ?? [];
@@ -4488,16 +5684,270 @@ async function planMigrationV2({
4488
5684
  }) {
4489
5685
  const schema = ormSchemaName(canonical.namespace);
4490
5686
  const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
5687
+ assertValidViewDependencies(canonical);
4491
5688
  const hints = applyMigrationHintsV2(previous, canonical);
4492
5689
  const snapshot = generateSnapshotV2(canonical, previousId(previous));
4493
- const ddl = await diffStatements(hints.adjusted, snapshot);
5690
+ const baseDdl = await diffStatements(withoutViews(hints.adjusted), withoutViews(snapshot));
5691
+ const previousViews = snapshotViews(hints.adjusted);
5692
+ const currentViews = snapshotViews(snapshot);
5693
+ const previousDependencies = snapshotViewDependencies(hints.adjusted);
5694
+ const currentDependencies = snapshotViewDependencies(snapshot);
5695
+ const affected = affectedViewNames({
5696
+ previousViews,
5697
+ currentViews,
5698
+ previousDependencies,
5699
+ currentDependencies,
5700
+ baseChanged: hints.statements.length > 0 || baseDdl.length > 0
5701
+ });
5702
+ const dropOrder = topologicalViewNames(
5703
+ new Set([...affected].filter((name) => previousViews.has(name))),
5704
+ previousDependencies
5705
+ ).reverse();
5706
+ const createOrder = topologicalViewNames(
5707
+ new Set([...affected].filter((name) => currentViews.has(name))),
5708
+ currentDependencies
5709
+ );
5710
+ const dropDdl = await generatedViewStatements({
5711
+ snapshot: hints.adjusted,
5712
+ names: new Set(dropOrder),
5713
+ mode: "drop",
5714
+ schema
5715
+ });
5716
+ const createDdl = await generatedViewStatements({
5717
+ snapshot,
5718
+ names: new Set(createOrder),
5719
+ mode: "create",
5720
+ schema
5721
+ });
5722
+ const currentViewDefinitions = new Map((canonical.views ?? []).map((view) => [view.view, view]));
4494
5723
  const statements = [
5724
+ ...dropOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(dropDdl, name, schema, "DROP") })),
4495
5725
  ...hints.statements.map((statement) => ({ kind: "hint", sql: statement })),
4496
- ...ddl.map((statement) => ({ kind: "ddl", sql: statement.replace(/;\s*$/, "") }))
5726
+ ...baseDdl.map((statement) => ({ kind: "ddl", sql: statement.replace(/;\s*$/, "") })),
5727
+ ...createOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(createDdl, name, schema, "CREATE") })),
5728
+ ...createOrder.map((name) => {
5729
+ const view = currentViewDefinitions.get(name);
5730
+ if (!view) throw new OrmDefinitionError(`missing canonical definition for view '${name}'`);
5731
+ return { kind: "ddl", sql: validateViewCatalogStatement({ canonical, view, schema }) };
5732
+ })
4497
5733
  ];
4498
5734
  return { namespace: canonical.namespace, schema, statements, snapshot };
4499
5735
  }
5736
+ function validateViewCatalogStatement({
5737
+ canonical,
5738
+ view,
5739
+ schema
5740
+ }) {
5741
+ const qualified = `${schema}.${view.view}`;
5742
+ const expectedColumns = sqlTextArray(view.columns.map((column) => column.columnName));
5743
+ const expectedTypes = `ARRAY[${view.columns.map((column) => `${viewTypeRegtype(column, schema)}::oid`).join(", ")}]::oid[]`;
5744
+ const relationIdentity = /* @__PURE__ */ new Map();
5745
+ for (const table of canonical.tables) {
5746
+ relationIdentity.set(table.table, `${schema}.${table.table}`);
5747
+ }
5748
+ for (const candidate of canonical.views ?? []) {
5749
+ relationIdentity.set(candidate.view, `${schema}.${candidate.view}`);
5750
+ }
5751
+ const expectedDependencies = sqlTextArray(
5752
+ view.dependencies.map((dependency) => {
5753
+ const physical = relationIdentity.get(dependency);
5754
+ if (!physical) throw new OrmDefinitionError(`view '${view.view}' depends on unknown relation '${dependency}'`);
5755
+ return physical;
5756
+ }).sort()
5757
+ );
5758
+ const allowedFunctions = sqlTextArray(VIEW_QUERY_FUNCTION_ALLOWLIST);
5759
+ return `DO $sequence_orm_validate_view$
5760
+ DECLARE
5761
+ actual_columns text[];
5762
+ actual_types oid[];
5763
+ actual_dependencies text[];
5764
+ BEGIN
5765
+ SELECT
5766
+ array_agg(attribute.attname ORDER BY attribute.attnum),
5767
+ array_agg(attribute.atttypid ORDER BY attribute.attnum)
5768
+ INTO actual_columns, actual_types
5769
+ FROM pg_catalog.pg_attribute AS attribute
5770
+ WHERE attribute.attrelid = pg_catalog.to_regclass(${quoteLiteral(qualified)})
5771
+ AND attribute.attnum > 0
5772
+ AND NOT attribute.attisdropped;
5773
+
5774
+ IF actual_columns IS DISTINCT FROM ${expectedColumns} THEN
5775
+ RAISE EXCEPTION 'managed view ${qualified} columns do not match its declaration: expected %, got %', ${expectedColumns}, actual_columns
5776
+ USING ERRCODE = '42804';
5777
+ END IF;
5778
+ IF actual_types IS DISTINCT FROM ${expectedTypes} THEN
5779
+ RAISE EXCEPTION 'managed view ${qualified} types do not match its declaration: expected %, got %', ${expectedTypes}, actual_types
5780
+ USING ERRCODE = '42804';
5781
+ END IF;
5782
+
5783
+ SELECT coalesce(array_agg(found.name ORDER BY found.name), ARRAY[]::text[])
5784
+ INTO actual_dependencies
5785
+ FROM (
5786
+ SELECT DISTINCT dependency_namespace.nspname || '.' || dependency_relation.relname AS name
5787
+ FROM pg_catalog.pg_rewrite AS rewrite
5788
+ JOIN pg_catalog.pg_depend AS dependency
5789
+ ON dependency.classid = 'pg_catalog.pg_rewrite'::regclass
5790
+ AND dependency.objid = rewrite.oid
5791
+ AND dependency.refclassid = 'pg_catalog.pg_class'::regclass
5792
+ JOIN pg_catalog.pg_class AS dependency_relation ON dependency_relation.oid = dependency.refobjid
5793
+ JOIN pg_catalog.pg_namespace AS dependency_namespace ON dependency_namespace.oid = dependency_relation.relnamespace
5794
+ WHERE rewrite.ev_class = pg_catalog.to_regclass(${quoteLiteral(qualified)})
5795
+ AND dependency.refobjid <> rewrite.ev_class
5796
+ AND dependency_relation.relkind IN ('r', 'p', 'v', 'm', 'f')
5797
+ ) AS found;
5798
+
5799
+ IF actual_dependencies IS DISTINCT FROM ${expectedDependencies} THEN
5800
+ RAISE EXCEPTION 'managed view ${qualified} dependencies do not match its declaration: expected %, got %', ${expectedDependencies}, actual_dependencies
5801
+ USING ERRCODE = '2BP01';
5802
+ END IF;
5803
+
5804
+ IF EXISTS (
5805
+ SELECT 1
5806
+ FROM pg_catalog.pg_rewrite AS rewrite
5807
+ JOIN pg_catalog.pg_depend AS dependency
5808
+ ON dependency.classid = 'pg_catalog.pg_rewrite'::regclass
5809
+ AND dependency.objid = rewrite.oid
5810
+ AND dependency.refclassid = 'pg_catalog.pg_proc'::regclass
5811
+ JOIN pg_catalog.pg_proc AS dependency_function ON dependency_function.oid = dependency.refobjid
5812
+ JOIN pg_catalog.pg_namespace AS function_namespace ON function_namespace.oid = dependency_function.pronamespace
5813
+ WHERE rewrite.ev_class = pg_catalog.to_regclass(${quoteLiteral(qualified)})
5814
+ AND (
5815
+ function_namespace.nspname <> 'pg_catalog'
5816
+ OR dependency_function.proname <> ALL (${allowedFunctions})
5817
+ )
5818
+ ) THEN
5819
+ RAISE EXCEPTION 'managed view ${qualified} may only call explicitly allowlisted side-effect-free pg_catalog functions'
5820
+ USING ERRCODE = '42501';
5821
+ END IF;
5822
+
5823
+ IF EXISTS (
5824
+ SELECT 1
5825
+ FROM pg_catalog.pg_rewrite AS rewrite
5826
+ JOIN pg_catalog.pg_depend AS dependency
5827
+ ON dependency.classid = 'pg_catalog.pg_rewrite'::regclass
5828
+ AND dependency.objid = rewrite.oid
5829
+ AND dependency.refclassid = 'pg_catalog.pg_operator'::regclass
5830
+ JOIN pg_catalog.pg_operator AS dependency_operator ON dependency_operator.oid = dependency.refobjid
5831
+ JOIN pg_catalog.pg_namespace AS operator_namespace ON operator_namespace.oid = dependency_operator.oprnamespace
5832
+ WHERE rewrite.ev_class = pg_catalog.to_regclass(${quoteLiteral(qualified)})
5833
+ AND operator_namespace.nspname <> 'pg_catalog'
5834
+ ) THEN
5835
+ RAISE EXCEPTION 'managed view ${qualified} may only use pg_catalog operators'
5836
+ USING ERRCODE = '42501';
5837
+ END IF;
5838
+ END
5839
+ $sequence_orm_validate_view$`;
5840
+ }
5841
+ function sqlTextArray(values) {
5842
+ return values.length === 0 ? "ARRAY[]::text[]" : `ARRAY[${values.map((value) => quoteLiteral(value)).join(", ")}]::text[]`;
5843
+ }
5844
+ function viewTypeRegtype(column, schema) {
5845
+ if (typeof column.type === "string") return `${quoteLiteral(sqlTypeName(column.type))}::regtype`;
5846
+ return `${quoteLiteral(`${quoteIdentifier(schema)}.${quoteIdentifier(column.type.enum)}`)}::regtype`;
5847
+ }
4500
5848
  var previousId = (snapshot) => snapshot.id;
5849
+ function snapshotViews(snapshot) {
5850
+ const views = snapshot.views ?? {};
5851
+ return new Map(Object.values(views).map((view) => [view.name, view]));
5852
+ }
5853
+ function snapshotViewDependencies(snapshot) {
5854
+ const metadata = snapshot._sequenceOrm;
5855
+ return metadata?.viewDependencies ?? {};
5856
+ }
5857
+ function withoutViews(snapshot) {
5858
+ const clone = structuredClone(snapshot);
5859
+ clone.views = {};
5860
+ delete clone._sequenceOrm;
5861
+ return clone;
5862
+ }
5863
+ function affectedViewNames({
5864
+ previousViews,
5865
+ currentViews,
5866
+ previousDependencies,
5867
+ currentDependencies,
5868
+ baseChanged
5869
+ }) {
5870
+ const allNames = /* @__PURE__ */ new Set([...previousViews.keys(), ...currentViews.keys()]);
5871
+ if (baseChanged) return allNames;
5872
+ const affected = new Set(
5873
+ [...allNames].filter((name) => {
5874
+ const before = previousViews.get(name);
5875
+ const after = currentViews.get(name);
5876
+ return before === void 0 || after === void 0 || stableJson(before) !== stableJson(after);
5877
+ })
5878
+ );
5879
+ let grew = true;
5880
+ while (grew) {
5881
+ grew = false;
5882
+ for (const name of allNames) {
5883
+ if (affected.has(name)) continue;
5884
+ const dependencies = /* @__PURE__ */ new Set([...previousDependencies[name] ?? [], ...currentDependencies[name] ?? []]);
5885
+ if ([...dependencies].some((dependency) => affected.has(dependency))) {
5886
+ affected.add(name);
5887
+ grew = true;
5888
+ }
5889
+ }
5890
+ }
5891
+ return affected;
5892
+ }
5893
+ function topologicalViewNames(names, dependencies) {
5894
+ const remaining = new Set(names);
5895
+ const ordered = [];
5896
+ while (remaining.size > 0) {
5897
+ const ready = [...remaining].filter((name) => (dependencies[name] ?? []).every((dependency) => !remaining.has(dependency))).sort();
5898
+ if (ready.length === 0) {
5899
+ throw new OrmDefinitionError(`view dependencies form a cycle among: ${[...remaining].sort().join(", ")}`);
5900
+ }
5901
+ for (const name of ready) {
5902
+ ordered.push(name);
5903
+ remaining.delete(name);
5904
+ }
5905
+ }
5906
+ return ordered;
5907
+ }
5908
+ async function generatedViewStatements({
5909
+ snapshot,
5910
+ names,
5911
+ mode,
5912
+ schema
5913
+ }) {
5914
+ if (names.size === 0) return /* @__PURE__ */ new Map();
5915
+ const without = structuredClone(snapshot);
5916
+ const holder = without;
5917
+ const views = holder.views ?? {};
5918
+ holder.views = Object.fromEntries(Object.entries(views).filter(([, view]) => !names.has(view.name)));
5919
+ const generated = mode === "drop" ? await diffStatements(snapshot, without) : await diffStatements(without, snapshot);
5920
+ const statements = /* @__PURE__ */ new Map();
5921
+ for (const name of names) {
5922
+ const qualified = `${quoteIdentifier(schema)}.${quoteIdentifier(name)}`;
5923
+ const prefix = mode === "drop" ? "DROP VIEW" : "CREATE VIEW";
5924
+ const statement = generated.find((candidate) => candidate.trimStart().startsWith(`${prefix} ${qualified}`));
5925
+ if (statement) statements.set(name, statement.replace(/;\s*$/, ""));
5926
+ }
5927
+ return statements;
5928
+ }
5929
+ function requiredViewStatement(statements, name, schema, operation) {
5930
+ const statement = statements.get(name);
5931
+ if (!statement) {
5932
+ throw new OrmDefinitionError(`drizzle-kit did not generate ${operation} VIEW for '${schema}.${name}'`);
5933
+ }
5934
+ return statement;
5935
+ }
5936
+ function assertValidViewDependencies(canonical) {
5937
+ const views = canonical.views ?? [];
5938
+ const known = /* @__PURE__ */ new Set([...canonical.tables.map((table) => table.table), ...views.map((view) => view.view)]);
5939
+ for (const view of views) {
5940
+ for (const dependency of view.dependencies) {
5941
+ if (!known.has(dependency)) {
5942
+ throw new OrmDefinitionError(`view '${view.view}' depends on unknown relation '${dependency}'`);
5943
+ }
5944
+ }
5945
+ }
5946
+ topologicalViewNames(
5947
+ new Set(views.map((view) => view.view)),
5948
+ Object.fromEntries(views.map((view) => [view.view, view.dependencies]))
5949
+ );
5950
+ }
4501
5951
  var QUOTED_STRING_PATTERN2 = /'(?:[^']|'')*'/g;
4502
5952
  var TOKEN_PATTERN = /\bclaim\(\s*'((?:[^']|'')*)'\s*\)|\bcapability\(\s*'((?:[^']|'')*)'\s*\)|'(?:[^']|'')*'|(?<!:):([a-z][a-z0-9_]*)/g;
4503
5953
  var BARE_CALL_PATTERN = /\b(?:claim|capability)\s*\(/;
@@ -4518,8 +5968,8 @@ var actionParamArg = (name) => `p_${name}`;
4518
5968
  function compileActionStatements(action, renderParam = actionParamArg) {
4519
5969
  return actionStatementSources(action.executes).map((statement) => compileStatement(statement, renderParam));
4520
5970
  }
4521
- function compileStatement(sql3, renderParam) {
4522
- const compiled = sql3.replace(TOKEN_PATTERN, (token, claimKey, capabilityKey, paramName) => {
5971
+ function compileStatement(sql4, renderParam) {
5972
+ const compiled = sql4.replace(TOKEN_PATTERN, (token, claimKey, capabilityKey, paramName) => {
4523
5973
  if (claimKey !== void 0) {
4524
5974
  if (!CLAIM_KEY_PATTERN.test(claimKey)) throw new OrmDefinitionError(`invalid claim key in executes: ${token}`);
4525
5975
  return claimExpression(claimKey);
@@ -4572,8 +6022,8 @@ function actionPrepareChecks(definition) {
4572
6022
  const order = actionParamOrder(action);
4573
6023
  const signature = order.length > 0 ? ` (${order.map((param) => sqlTypeName(action.params[param].type)).join(", ")})` : "";
4574
6024
  const compiled = compileActionStatements(action, (param) => `$${order.indexOf(param) + 1}`);
4575
- compiled.forEach((sql3, index3) => {
4576
- const checked = action.returns && index3 === compiled.length - 1 ? returnProjection(action.returns, sql3) : sql3;
6025
+ compiled.forEach((sql4, index3) => {
6026
+ const checked = action.returns && index3 === compiled.length - 1 ? returnProjection(action.returns, sql4) : sql4;
4577
6027
  statements.push(`PREPARE ${quoteIdentifier(`orm_check_${name}_${index3}`)}${signature} AS ${checked}`);
4578
6028
  });
4579
6029
  }
@@ -4689,15 +6139,15 @@ async function planMigration({
4689
6139
  const ddl = await diffStatements(hints.adjusted, snapshot);
4690
6140
  const policyDrops = policyDropStatements(schema, snapshotTableNames(previous, schema));
4691
6141
  const statements = [
4692
- ...policyDrops.map((sql3) => ({ kind: "security", sql: sql3 })),
4693
- ...hints.statements.map((sql3) => ({ kind: "hint", sql: sql3 })),
4694
- ...ddl.map((sql3) => ({ kind: "ddl", sql: sql3.replace(/;\s*$/, "") })),
4695
- ...securityStatements(definition).map((sql3) => ({ kind: "security", sql: sql3 })),
6142
+ ...policyDrops.map((sql4) => ({ kind: "security", sql: sql4 })),
6143
+ ...hints.statements.map((sql4) => ({ kind: "hint", sql: sql4 })),
6144
+ ...ddl.map((sql4) => ({ kind: "ddl", sql: sql4.replace(/;\s*$/, "") })),
6145
+ ...securityStatements(definition).map((sql4) => ({ kind: "security", sql: sql4 })),
4696
6146
  // PREPARE each action's executes first (plpgsql bodies don't check columns
4697
6147
  // at CREATE), then create the functions. Both reference the tables the DDL
4698
6148
  // above creates, so they close the plan.
4699
- ...actionPrepareChecks(definition).map((sql3) => ({ kind: "function", sql: sql3 })),
4700
- ...actionFunctionStatements(definition).map((sql3) => ({ kind: "function", sql: sql3 }))
6149
+ ...actionPrepareChecks(definition).map((sql4) => ({ kind: "function", sql: sql4 })),
6150
+ ...actionFunctionStatements(definition).map((sql4) => ({ kind: "function", sql: sql4 }))
4701
6151
  ];
4702
6152
  return { namespace: definition.namespace, schema, statements, snapshot };
4703
6153
  }
@@ -4826,6 +6276,8 @@ function validateCapabilityRefs(files, declared) {
4826
6276
  const referenced = [
4827
6277
  ...table.policies?.read ? referencedCapabilities(table.policies.read.ast) : [],
4828
6278
  ...table.policies?.write ? referencedCapabilities(table.policies.write.ast) : [],
6279
+ ...table.policies?.insert ? referencedCapabilities(table.policies.insert.ast) : [],
6280
+ ...table.policies?.update ? referencedCapabilities(table.policies.update.ast) : [],
4829
6281
  ...Object.values(table.actions ?? {}).flatMap((action) => [
4830
6282
  ...action.requires ? referencedCapabilities(action.requires.ast) : [],
4831
6283
  ...referencedExecuteCapabilities(action)
@@ -4983,7 +6435,7 @@ async function readMigrationSql(dir, entry) {
4983
6435
  throw new OrmDefinitionError("missing migration file", path$1);
4984
6436
  });
4985
6437
  }
4986
- var checksumOf = (text3) => crypto.createHash("sha256").update(text3).digest("hex");
6438
+ var checksumOf = (text4) => crypto.createHash("sha256").update(text4).digest("hex");
4987
6439
  var migrationName = (ordinal, contentHash) => `${pad(ordinal)}_${contentHash.slice(0, 12)}`;
4988
6440
  function renderMigrationSql({
4989
6441
  name,
@@ -5009,12 +6461,12 @@ async function writeMigration({
5009
6461
  const tip = workspace.journal.entries.at(-1);
5010
6462
  const ordinal = (tip?.ordinal ?? 0) + 1;
5011
6463
  const name = migrationName(ordinal, contentHash);
5012
- const sql3 = renderMigrationSql({ name, namespace: workspace.journal.namespace, contentHash, plan });
6464
+ const sql4 = renderMigrationSql({ name, namespace: workspace.journal.namespace, contentHash, plan });
5013
6465
  const metaDir = path.join(dir, MIGRATIONS_DIRNAME, META_DIRNAME);
5014
6466
  await promises.mkdir(metaDir, { recursive: true });
5015
6467
  const snapshotText = `${JSON.stringify(plan.snapshot, null, 2)}
5016
6468
  `;
5017
- await promises.writeFile(path.join(dir, MIGRATIONS_DIRNAME, `${name}.sql`), sql3);
6469
+ await promises.writeFile(path.join(dir, MIGRATIONS_DIRNAME, `${name}.sql`), sql4);
5018
6470
  await promises.writeFile(path.join(metaDir, `${pad(ordinal)}.snapshot.json`), snapshotText);
5019
6471
  const journal = {
5020
6472
  ...workspace.journal,
@@ -5025,7 +6477,7 @@ async function writeMigration({
5025
6477
  name,
5026
6478
  content_hash: contentHash,
5027
6479
  previous_content_hash: tip?.content_hash ?? null,
5028
- checksum: checksumOf(sql3),
6480
+ checksum: checksumOf(sql4),
5029
6481
  snapshot_checksum: checksumOf(snapshotText),
5030
6482
  destructive
5031
6483
  }
@@ -5140,8 +6592,8 @@ async function checkWorkspace({
5140
6592
  let previousId3;
5141
6593
  let previousHash = null;
5142
6594
  for (const entry of workspace.journal.entries) {
5143
- const sql3 = await readMigrationSql(dir, entry);
5144
- if (checksumOf(sql3) !== entry.checksum) {
6595
+ const sql4 = await readMigrationSql(dir, entry);
6596
+ if (checksumOf(sql4) !== entry.checksum) {
5145
6597
  log(`${MIGRATIONS_DIRNAME}/${entry.name}.sql does not match its journal checksum \u2014 committed migrations must not be edited`);
5146
6598
  return 1;
5147
6599
  }
@@ -5181,7 +6633,7 @@ async function checkWorkspace({
5181
6633
  log(`migrations in sync \u2014 ${workspace.journal.entries.length} committed, tip ${tip.name}`);
5182
6634
  return 0;
5183
6635
  }
5184
- var firstLine = (sql3) => sql3.split("\n")[0] ?? sql3;
6636
+ var firstLine = (sql4) => sql4.split("\n")[0] ?? sql4;
5185
6637
 
5186
6638
  // src/migrations/from-yaml.ts
5187
6639
  function convertV1DefinitionToCanonical(definition) {
@@ -5213,7 +6665,7 @@ function convertTable(table) {
5213
6665
  description: table.description,
5214
6666
  source: table.source,
5215
6667
  columns: columns.map(([name, column]) => convertColumn(name, column)),
5216
- // Synced tables reflect their source — no columns, no primary key.
6668
+ // Synced tables may declare a typed column/PK contract; untyped remain empty.
5217
6669
  primaryKey: primaryKey2.length > 0 ? primaryKey2 : void 0,
5218
6670
  links: Object.entries(table.links ?? {}).map(([name, link]) => convertLink(table.table, name, link)),
5219
6671
  indexes: (table.indexes ?? []).map((index3) => ({
@@ -5226,9 +6678,20 @@ function convertTable(table) {
5226
6678
  read: table.policies?.read?.source,
5227
6679
  write: table.policies?.write?.source
5228
6680
  },
5229
- actions: Object.entries(table.actions ?? {}).map(([name, action]) => convertAction(table.table, name, action))
6681
+ // v2 has no action surface (writes are mutations); the transition sweep
6682
+ // drops the plpgsql action_* functions, so carrying the metadata would be
6683
+ // dead weight. droppedV1Actions() surfaces them for the emitter's TODO.
6684
+ actions: []
5230
6685
  };
5231
6686
  }
6687
+ function droppedV1Actions(definition) {
6688
+ const dropped = /* @__PURE__ */ new Map();
6689
+ for (const table of definition.tables) {
6690
+ const names = Object.keys(table.actions ?? {});
6691
+ if (names.length > 0) dropped.set(table.table, names);
6692
+ }
6693
+ return dropped;
6694
+ }
5232
6695
  function convertColumn(name, column) {
5233
6696
  const isDefaultFn = column.default !== void 0 && isFunctionDefault(column.default, column.type);
5234
6697
  return {
@@ -5262,25 +6725,6 @@ function convertLink(table, name, link) {
5262
6725
  description: link.description
5263
6726
  };
5264
6727
  }
5265
- function convertAction(table, name, action) {
5266
- return {
5267
- name,
5268
- requires: action.requires?.source,
5269
- params: Object.fromEntries(
5270
- Object.entries(action.params ?? {}).map(([param, definition]) => [
5271
- param,
5272
- { type: definition.type, nullable: definition.optional === true }
5273
- ])
5274
- ),
5275
- // The v2 contract carries one SQL string; statement boundaries survive as
5276
- // `;\n` (v1 validation guarantees no bare `;` inside a single statement).
5277
- executes: actionStatementSources(action.executes).join(";\n"),
5278
- // v1 declared the RETURNING columns inline; v2 types them at compile time,
5279
- // so the shape collapses to rows-vs-rowsAffected.
5280
- returns: action.returns ? "rows" : "rowsAffected",
5281
- audit: action.audit ? { action: action.audit.action, resource: action.audit.resource ?? table } : void 0
5282
- };
5283
- }
5284
6728
 
5285
6729
  // src/migrations/v2.ts
5286
6730
  var SHA256_HEX = /^[0-9a-f]{64}$/;
@@ -5303,7 +6747,7 @@ function buildRebaseJournalEntry({ namespace, previousHash, newHash, ordinal, sn
5303
6747
  throw new OrmDefinitionError(`rebase ordinal must be an integer \u2265 2 (a rebase sits on an existing journal tip); got ${ordinal}`);
5304
6748
  }
5305
6749
  const name = migrationName(ordinal, newHash);
5306
- const sql3 = renderRebaseMigrationSql({ name, namespace, previousHash, newHash });
6750
+ const sql4 = renderRebaseMigrationSql({ name, namespace, previousHash, newHash });
5307
6751
  const snapshotText = `${JSON.stringify(snapshot, null, 2)}
5308
6752
  `;
5309
6753
  const entry = {
@@ -5311,11 +6755,11 @@ function buildRebaseJournalEntry({ namespace, previousHash, newHash, ordinal, sn
5311
6755
  name,
5312
6756
  content_hash: newHash,
5313
6757
  previous_content_hash: previousHash,
5314
- checksum: checksumOf(sql3),
6758
+ checksum: checksumOf(sql4),
5315
6759
  snapshot_checksum: checksumOf(snapshotText),
5316
6760
  destructive: false
5317
6761
  };
5318
- return { name, entry, sql: sql3, snapshotText };
6762
+ return { name, entry, sql: sql4, snapshotText };
5319
6763
  }
5320
6764
  function renderRebaseMigrationSql({
5321
6765
  name,
@@ -5353,15 +6797,15 @@ var PLATFORM_USER_FIELDS = [
5353
6797
  ];
5354
6798
  function generateSdl(canonical, options = {}) {
5355
6799
  const def = normalizeCanonical(canonicalDefinitionSchema.parse(canonical));
5356
- const tables = def.tables.filter((table) => table.source === void 0);
6800
+ const tables = canonicalRelations(def).filter((table) => table.source === void 0);
5357
6801
  if (tables.length === 0) {
5358
6802
  throw new SdlGenerationError(
5359
- `Namespace "${def.namespace}" exposes no tables (synced tables are excluded from SDL in v2.0) \u2014 nothing to generate.`
6803
+ `Namespace "${def.namespace}" exposes no relations (synced tables are excluded from SDL in v2.0) \u2014 nothing to generate.`
5360
6804
  );
5361
6805
  }
5362
6806
  for (const table of tables) {
5363
6807
  if (table.columns.length === 0) {
5364
- throw new SdlGenerationError(`Table "${table.table}" has no columns \u2014 cannot generate an object type.`);
6808
+ throw new SdlGenerationError(`Relation "${table.table}" has no columns \u2014 cannot generate an object type.`);
5365
6809
  }
5366
6810
  }
5367
6811
  const namesByTable = new Map(tables.map((table) => [table.table, tableNames(table.table)]));
@@ -5455,8 +6899,8 @@ function renderEnum(name, values) {
5455
6899
  ${values.map((value) => ` ${value}`).join("\n")}
5456
6900
  }`;
5457
6901
  }
5458
- function descriptionLines(text3, indent) {
5459
- const escaped = text3.replace(/"""/g, '\\"""');
6902
+ function descriptionLines(text4, indent) {
6903
+ const escaped = text4.replace(/"""/g, '\\"""');
5460
6904
  return [`${indent}"""`, ...escaped.split("\n").map((line) => `${indent}${line}`), `${indent}"""`];
5461
6905
  }
5462
6906
  function renderComparisonExp(gqlTypeName) {
@@ -5541,10 +6985,20 @@ function renderTableBlocks(ctx) {
5541
6985
  ${fields}
5542
6986
  }`);
5543
6987
  }
5544
- if (isWritable(table)) blocks.push(...renderWriteBlocks(ctx));
6988
+ if (allowsInsert(table) || allowsUpdate(table)) {
6989
+ blocks.push(...renderWriteBlocks(ctx, { insert: allowsInsert(table), update: allowsUpdate(table) }));
6990
+ }
5545
6991
  return blocks;
5546
6992
  }
5547
- function isWritable(table) {
6993
+ function allowsInsert(table) {
6994
+ return table.policies.write !== void 0 || table.policies.insert !== void 0;
6995
+ }
6996
+ function allowsUpdate(table) {
6997
+ return table.policies.write !== void 0 || table.policies.update !== void 0;
6998
+ }
6999
+ function allowsOnConflict(table) {
7000
+ if (!allowsInsert(table) || !allowsUpdate(table)) return false;
7001
+ if (table.policies.insert !== void 0 || table.policies.update !== void 0) return false;
5548
7002
  return table.policies.write !== void 0;
5549
7003
  }
5550
7004
  function renderObjectType(ctx) {
@@ -5676,46 +7130,50 @@ function constraintValues(table) {
5676
7130
  const values = /* @__PURE__ */ new Set();
5677
7131
  if (table.primaryKey && table.primaryKey.length > 0) values.add(`${table.table}_pkey`);
5678
7132
  for (const column of table.columns) {
5679
- if (column.unique) values.add(`${table.table}_${column.columnName}_key`);
7133
+ if (column.unique) values.add(`${table.table}_${column.columnName}_unique`);
5680
7134
  }
5681
7135
  for (const index3 of table.indexes) {
5682
7136
  if (index3.unique) values.add(index3.name ?? `${table.table}_${index3.columns.join("_")}_key`);
5683
7137
  }
5684
7138
  return [...values].sort();
5685
7139
  }
5686
- function renderWriteBlocks(ctx) {
7140
+ function renderWriteBlocks(ctx, access) {
5687
7141
  const { table, names, pinned } = ctx;
5688
7142
  const blocks = [];
5689
- const pinnedSet = new Set(pinned);
5690
- const insertable = table.columns.filter(
5691
- (column) => !pinnedSet.has(column.name) && !pinnedSet.has(column.columnName)
5692
- );
5693
- if (insertable.length === 0) {
5694
- throw new SdlGenerationError(
5695
- `Table "${table.table}" has no insertable columns after excluding pinned columns [${pinned.join(", ")}].`
7143
+ if (access.insert) {
7144
+ const pinnedSet = new Set(pinned);
7145
+ const insertable = table.columns.filter(
7146
+ (column) => !pinnedSet.has(column.name) && !pinnedSet.has(column.columnName)
5696
7147
  );
5697
- }
5698
- const insertLines = insertable.map(
5699
- (column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
5700
- );
5701
- blocks.push(`input ${names.insertInput} {
7148
+ if (insertable.length === 0) {
7149
+ throw new SdlGenerationError(
7150
+ `Table "${table.table}" has no insertable columns after excluding pinned columns [${pinned.join(", ")}].`
7151
+ );
7152
+ }
7153
+ const insertLines = insertable.map(
7154
+ (column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
7155
+ );
7156
+ blocks.push(`input ${names.insertInput} {
5702
7157
  ${insertLines.join("\n")}
5703
7158
  }`);
5704
- const setLines = table.columns.map(
5705
- (column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
5706
- );
5707
- blocks.push(`input ${names.setInput} {
7159
+ }
7160
+ if (access.update) {
7161
+ const setLines = table.columns.map(
7162
+ (column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
7163
+ );
7164
+ blocks.push(`input ${names.setInput} {
5708
7165
  ${setLines.join("\n")}
5709
7166
  }`);
5710
- const jsonb3 = jsonbColumns(table);
5711
- if (jsonb3.length > 0) {
5712
- const appendLines = jsonb3.map((column) => ` ${fieldName(column.name)}: jsonb`);
7167
+ }
7168
+ const jsonb4 = jsonbColumns(table);
7169
+ if (access.update && jsonb4.length > 0) {
7170
+ const appendLines = jsonb4.map((column) => ` ${fieldName(column.name)}: jsonb`);
5713
7171
  blocks.push(`input ${names.appendInput} {
5714
7172
  ${appendLines.join("\n")}
5715
7173
  }`);
5716
7174
  }
5717
7175
  const numericish = numericishColumns(table);
5718
- if (numericish.length > 0) {
7176
+ if (access.update && numericish.length > 0) {
5719
7177
  const incLines = numericish.map(
5720
7178
  (column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
5721
7179
  );
@@ -5724,7 +7182,7 @@ ${incLines.join("\n")}
5724
7182
  }`);
5725
7183
  }
5726
7184
  const constraints = constraintValues(table);
5727
- if (constraints.length > 0) {
7185
+ if (access.insert && allowsOnConflict(table) && constraints.length > 0) {
5728
7186
  for (const value of constraints) assertEnumValue(value, `constraint enum for table "${table.table}"`);
5729
7187
  blocks.push(renderEnum(names.constraintEnum, constraints));
5730
7188
  const updateColumns = table.columns.map((column) => fieldName(column.name));
@@ -5737,12 +7195,14 @@ ${incLines.join("\n")}
5737
7195
  }`
5738
7196
  );
5739
7197
  }
5740
- blocks.push(
5741
- `type ${names.mutationResponse} {
7198
+ if (access.insert || access.update) {
7199
+ blocks.push(
7200
+ `type ${names.mutationResponse} {
5742
7201
  affectedRows: Int!
5743
7202
  returning: [${names.typename}!]!
5744
7203
  }`
5745
- );
7204
+ );
7205
+ }
5746
7206
  return blocks;
5747
7207
  }
5748
7208
  function renderQueryRoot(tables, namesByTable) {
@@ -5767,35 +7227,64 @@ function columnGqlTypeOf(table, column) {
5767
7227
  function renderMutationRoot(tables, namesByTable) {
5768
7228
  const lines = [];
5769
7229
  for (const table of tables) {
5770
- if (!isWritable(table)) continue;
7230
+ const canInsert = allowsInsert(table);
7231
+ const canUpdate = allowsUpdate(table);
7232
+ if (!canInsert && !canUpdate) continue;
5771
7233
  const names = namesByTable.get(table.table);
5772
7234
  if (!names) continue;
5773
7235
  const constraints = constraintValues(table);
5774
- const onConflictArg = constraints.length > 0 ? `, onConflict: ${names.onConflict}` : "";
7236
+ const onConflictArg = canInsert && allowsOnConflict(table) && constraints.length > 0 ? `, onConflict: ${names.onConflict}` : "";
5775
7237
  const updateArgs = [`_set: ${names.setInput}`];
5776
7238
  if (jsonbColumns(table).length > 0) updateArgs.push(`_append: ${names.appendInput}`);
5777
7239
  if (numericishColumns(table).length > 0) updateArgs.push(`_inc: ${names.incInput}`);
5778
7240
  const updateArgList = updateArgs.join(", ");
5779
- lines.push(` ${names.insertRoot}(objects: [${names.insertInput}!]!${onConflictArg}): ${names.mutationResponse}`);
5780
- lines.push(` ${names.insertOneRoot}(object: ${names.insertInput}!${onConflictArg}): ${names.typename}`);
5781
- lines.push(` ${names.updateRoot}(where: ${names.boolExp}!, ${updateArgList}): ${names.mutationResponse}`);
5782
- const pk = primaryKeyColumns(table);
5783
- if (pk.length > 0) {
5784
- lines.push(
5785
- ` ${names.updateByPkRoot}(pkColumns: ${names.pkColumnsInput}!, ${updateArgList}): ${names.typename}`
5786
- );
5787
- }
5788
- lines.push(` ${names.deleteRoot}(where: ${names.boolExp}!): ${names.mutationResponse}`);
5789
- if (pk.length > 0) {
5790
- const args = pk.map((column) => `${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}!`).join(", ");
5791
- lines.push(` ${names.deleteByPkRoot}(${args}): ${names.typename}`);
7241
+ if (canInsert) {
7242
+ lines.push(` ${names.insertRoot}(objects: [${names.insertInput}!]!${onConflictArg}): ${names.mutationResponse}`);
7243
+ lines.push(` ${names.insertOneRoot}(object: ${names.insertInput}!${onConflictArg}): ${names.typename}`);
7244
+ }
7245
+ if (canUpdate) {
7246
+ lines.push(` ${names.updateRoot}(where: ${names.boolExp}!, ${updateArgList}): ${names.mutationResponse}`);
7247
+ const pk = primaryKeyColumns(table);
7248
+ if (pk.length > 0) {
7249
+ lines.push(
7250
+ ` ${names.updateByPkRoot}(pkColumns: ${names.pkColumnsInput}!, ${updateArgList}): ${names.typename}`
7251
+ );
7252
+ }
7253
+ lines.push(` ${names.deleteRoot}(where: ${names.boolExp}!): ${names.mutationResponse}`);
7254
+ if (pk.length > 0) {
7255
+ const args = pk.map((column) => `${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}!`).join(", ");
7256
+ lines.push(` ${names.deleteByPkRoot}(${args}): ${names.typename}`);
7257
+ }
5792
7258
  }
7259
+ lines.push(...renderCowMutationFields(table, names));
5793
7260
  }
5794
7261
  if (lines.length === 0) return null;
5795
7262
  return `type Mutation {
5796
7263
  ${lines.join("\n")}
5797
7264
  }`;
5798
7265
  }
7266
+ function renderCowMutationFields(table, names) {
7267
+ const lines = [];
7268
+ const columnByName = new Map(table.columns.map((column) => [column.name, column]));
7269
+ for (const mutation of table.cowMutations ?? []) {
7270
+ const pkColumn = columnByName.get(mutation.pk);
7271
+ if (!pkColumn) continue;
7272
+ const pkArg = `${fieldName(mutation.pk)}: ${columnGqlTypeOf(table, pkColumn)}!`;
7273
+ if (mutation.kind === "revert") {
7274
+ lines.push(` ${fieldName(mutation.name)}(${pkArg}): ${names.typename}`);
7275
+ continue;
7276
+ }
7277
+ const setArgs = mutation.set.map((property) => {
7278
+ const column = columnByName.get(property);
7279
+ if (!column) return null;
7280
+ const bang = column.nullable === false ? "!" : "";
7281
+ return `${fieldName(property)}: ${columnGqlTypeOf(table, column)}${bang}`;
7282
+ });
7283
+ if (setArgs.some((arg) => arg === null)) continue;
7284
+ lines.push(` ${fieldName(mutation.name)}(${pkArg}, ${setArgs.join(", ")}): ${names.typename}`);
7285
+ }
7286
+ return lines;
7287
+ }
5799
7288
 
5800
7289
  // src/cli.ts
5801
7290
  var BASE_URL_ENV = "SEQUENCE_ORM_BASE_URL";
@@ -6033,7 +7522,7 @@ async function generateCommand(rest) {
6033
7522
  await writeGenerateOutputs(dir, compiled.project);
6034
7523
  const { loaded, manifest } = compiled.project;
6035
7524
  console.log(
6036
- `wrote ${SCHEMA_GRAPHQL_FILE}, ${OPERATIONS_MANIFEST_FILE}, ${TYPE_POLICIES_FILE} (${loaded.canonical.tables.length} table(s), ${Object.keys(manifest.operations).length} operation(s))`
7525
+ `wrote ${SCHEMA_GRAPHQL_FILE}, ${OPERATIONS_MANIFEST_FILE}, ${TYPE_POLICIES_FILE} (${loaded.canonical.tables.length} table(s), ${loaded.canonical.views?.length ?? 0} view(s), ${Object.keys(manifest.operations).length} operation(s))`
6037
7526
  );
6038
7527
  return runConsumerCodegen(dir);
6039
7528
  }
@@ -6086,7 +7575,7 @@ async function validateV2Command(dir) {
6086
7575
  if (!compiled.ok) return 1;
6087
7576
  const { loaded, manifest } = compiled.project;
6088
7577
  console.log(
6089
- `namespace ${loaded.canonical.namespace}: ${loaded.canonical.tables.length} table(s), ${Object.keys(manifest.operations).length} operation(s) valid`
7578
+ `namespace ${loaded.canonical.namespace}: ${loaded.canonical.tables.length} table(s), ${loaded.canonical.views?.length ?? 0} view(s), ${Object.keys(manifest.operations).length} operation(s) valid`
6090
7579
  );
6091
7580
  console.log(`content hash ${loaded.hash}`);
6092
7581
  const workspace = await readMigrationWorkspace(dir, loaded.canonical.namespace);
@@ -6216,8 +7705,8 @@ async function checkWorkspaceV2({
6216
7705
  let previousId3;
6217
7706
  let previousHash = null;
6218
7707
  for (const entry of workspace.journal.entries) {
6219
- const sql3 = await readMigrationSql(dir, entry);
6220
- if (checksumOf(sql3) !== entry.checksum) {
7708
+ const sql4 = await readMigrationSql(dir, entry);
7709
+ if (checksumOf(sql4) !== entry.checksum) {
6221
7710
  log(`${MIGRATIONS_DIRNAME}/${entry.name}.sql does not match its journal checksum \u2014 committed migrations must not be edited`);
6222
7711
  return 1;
6223
7712
  }
@@ -6404,9 +7893,15 @@ async function migrateFromYamlCommand(rest) {
6404
7893
  }
6405
7894
  const v1 = await loadNamespaceDefinition(dir);
6406
7895
  const canonical = convertV1DefinitionToCanonical(v1.definition);
7896
+ const droppedActions = droppedV1Actions(v1.definition);
6407
7897
  await writeConverted(path.join(dir, NAMESPACE_CONFIG_FILE), emitSequenceConfig(canonical));
6408
7898
  await promises.mkdir(path.join(dir, SCHEMA_DIR), { recursive: true });
6409
- await writeConverted(path.join(dir, SCHEMA_DIR, "tables.ts"), emitSchemaTables(canonical));
7899
+ await writeConverted(path.join(dir, SCHEMA_DIR, "tables.ts"), emitSchemaTables(canonical, droppedActions));
7900
+ for (const [table, names] of droppedActions) {
7901
+ console.log(
7902
+ `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)`
7903
+ );
7904
+ }
6410
7905
  const reloaded = await loadV2(dir);
6411
7906
  if (JSON.stringify(sortKeys(reloaded.canonical)) !== JSON.stringify(sortKeys(canonical))) {
6412
7907
  console.error(
@@ -6470,14 +7965,15 @@ function emitSequenceConfig(canonical) {
6470
7965
  lines.push("})", "");
6471
7966
  return lines.join("\n");
6472
7967
  }
6473
- function emitSchemaTables(canonical) {
7968
+ function emitSchemaTables(canonical, droppedActions) {
6474
7969
  const columnTables2 = canonical.tables.filter((table) => table.source === void 0);
6475
7970
  const syncedTables = canonical.tables.filter((table) => table.source !== void 0);
6476
7971
  const hasLinks = canonical.tables.some((table) => table.links.length > 0);
7972
+ const syncedHasColumns = syncedTables.some((table) => table.columns.length > 0);
6477
7973
  const imports = [
6478
7974
  ...columnTables2.length > 0 ? ["defineTable"] : [],
6479
7975
  ...syncedTables.length > 0 ? ["defineSyncedTable"] : [],
6480
- ...columnTables2.length > 0 ? ["t"] : [],
7976
+ ...columnTables2.length > 0 || syncedHasColumns ? ["t"] : [],
6481
7977
  ...hasLinks ? ["toOne"] : []
6482
7978
  ].sort((a, b) => a.localeCompare(b));
6483
7979
  const parts = [
@@ -6491,18 +7987,62 @@ function emitSchemaTables(canonical) {
6491
7987
  ""
6492
7988
  ];
6493
7989
  for (const table of canonical.tables) {
7990
+ const dropped = droppedActions?.get(table.table) ?? [];
7991
+ if (dropped.length > 0) {
7992
+ parts.push(
7993
+ `// TODO: v1 actions dropped by the conversion \u2014 v2 has no action surface (writes`,
7994
+ `// are persisted mutations). Re-author for '${table.table}': ${dropped.join(", ")}`
7995
+ );
7996
+ }
6494
7997
  parts.push(table.source !== void 0 ? emitSyncedTable(table) : emitTable(table));
6495
7998
  }
6496
7999
  return parts.join("\n");
6497
8000
  }
6498
8001
  function emitSyncedTable(table) {
8002
+ const singlePrimaryKey = table.primaryKey?.length === 1 ? table.primaryKey[0] : void 0;
8003
+ if (table.columns.length === 0) {
8004
+ const lines2 = [
8005
+ `export const ${tableVarName(table.table)} = defineSyncedTable(${quote(table.table)}, {`,
8006
+ ` source: ${quote(table.source ?? "")},`
8007
+ ];
8008
+ if (table.description !== void 0) lines2.push(` description: ${quote(table.description)},`);
8009
+ if (table.policies.read !== void 0) {
8010
+ lines2.push(" policies: {", ` read: ${policyLiteral(table.policies.read)},`, " },");
8011
+ }
8012
+ lines2.push("})", "");
8013
+ return lines2.join("\n");
8014
+ }
6499
8015
  const lines = [
6500
- `export const ${tableVarName(table.table)} = defineSyncedTable(${quote(table.table)}, {`,
6501
- // `source` is guaranteed by the caller's filter.
6502
- ` source: ${quote(table.source ?? "")},`
8016
+ `export const ${tableVarName(table.table)} = defineSyncedTable(`,
8017
+ ` ${quote(table.table)},`,
8018
+ " {"
6503
8019
  ];
6504
- if (table.description !== void 0) lines.push(` description: ${quote(table.description)},`);
6505
- lines.push("})", "");
8020
+ for (const column of table.columns) lines.push(` ${columnProperty(table.table, column, singlePrimaryKey)}`);
8021
+ lines.push(" },", " {");
8022
+ lines.push(` source: ${quote(table.source ?? "")},`);
8023
+ if (table.description !== void 0) lines.push(` description: ${quote(table.description)},`);
8024
+ if (singlePrimaryKey === void 0 && table.primaryKey && table.primaryKey.length > 0) {
8025
+ lines.push(` primaryKey: ${jsonLiteral(table.primaryKey, 4)},`);
8026
+ }
8027
+ if (table.links.length > 0) {
8028
+ lines.push(" links: {");
8029
+ for (const link of table.links) {
8030
+ if (link.kind !== "toOne" || link.target === void 0) {
8031
+ throw new OrmDefinitionError(
8032
+ `link '${link.name}' on synced '${table.table}' is not a toOne link \u2014 not a v1 conversion output`
8033
+ );
8034
+ }
8035
+ const options = [`on: ${quote(link.on)}`];
8036
+ if (link.references !== void 0) options.push(`references: ${quote(link.references)}`);
8037
+ if (link.description !== void 0) options.push(`description: ${quote(link.description)}`);
8038
+ lines.push(` ${objectKey(link.name)}: toOne(() => ${tableVarName(link.target)}, { ${options.join(", ")} }),`);
8039
+ }
8040
+ lines.push(" },");
8041
+ }
8042
+ if (table.policies.read !== void 0) {
8043
+ lines.push(" policies: {", ` read: ${policyLiteral(table.policies.read)},`, " },");
8044
+ }
8045
+ lines.push(" },", ")", "");
6506
8046
  return lines.join("\n");
6507
8047
  }
6508
8048
  function emitTable(table) {
@@ -6572,7 +8112,6 @@ function emitTableConfig(table, singlePrimaryKey) {
6572
8112
  if (table.policies.write !== void 0) lines.push(` write: ${policyLiteral(table.policies.write)},`);
6573
8113
  lines.push(" },");
6574
8114
  }
6575
- if (table.actions.length > 0) lines.push(` actions: ${jsonLiteral(table.actions, 4)},`);
6576
8115
  return lines;
6577
8116
  }
6578
8117
  function policyLiteral(policy) {
@@ -6873,7 +8412,7 @@ function positional(rest) {
6873
8412
  if (envIndex !== -1) args.splice(envIndex, 2);
6874
8413
  return args.find((arg) => !arg.startsWith("-"));
6875
8414
  }
6876
- var firstLine2 = (sql3) => sql3.split("\n")[0] ?? sql3;
8415
+ var firstLine2 = (sql4) => sql4.split("\n")[0] ?? sql4;
6877
8416
 
6878
8417
  exports.BASE_URL_ENV = BASE_URL_ENV;
6879
8418
  exports.ORM_MODULE_PATH_ENV = ORM_MODULE_PATH_ENV;