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