@sequenceholdings/orm 2.4.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/authoring.cjs +53 -7
  2. package/dist/authoring.cjs.map +1 -1
  3. package/dist/authoring.d.cts +11 -7
  4. package/dist/authoring.d.ts +11 -7
  5. package/dist/authoring.js +53 -7
  6. package/dist/authoring.js.map +1 -1
  7. package/dist/canonical.cjs +48 -2
  8. package/dist/canonical.cjs.map +1 -1
  9. package/dist/canonical.d.cts +72 -72
  10. package/dist/canonical.d.ts +72 -72
  11. package/dist/canonical.js +48 -2
  12. package/dist/canonical.js.map +1 -1
  13. package/dist/cli.cjs +237 -20
  14. package/dist/cli.cjs.map +1 -1
  15. package/dist/cli.js +237 -20
  16. package/dist/cli.js.map +1 -1
  17. package/dist/client.cjs +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 +1 -1
  22. package/dist/client.js.map +1 -1
  23. package/dist/compiler.cjs +386 -14
  24. package/dist/compiler.cjs.map +1 -1
  25. package/dist/compiler.d.cts +16 -3
  26. package/dist/compiler.d.ts +16 -3
  27. package/dist/compiler.js +386 -15
  28. package/dist/compiler.js.map +1 -1
  29. package/dist/{constants-C6EG6Wq7.d.cts → constants-Dou1RkTo.d.cts} +7 -2
  30. package/dist/{constants-C6EG6Wq7.d.ts → constants-Dou1RkTo.d.ts} +7 -2
  31. package/dist/dynamic-input.d.cts +1 -1
  32. package/dist/dynamic-input.d.ts +1 -1
  33. package/dist/index.cjs +329 -130
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.cts +66 -58
  36. package/dist/index.d.ts +66 -58
  37. package/dist/index.js +329 -131
  38. package/dist/index.js.map +1 -1
  39. package/dist/ir.cjs +50 -3
  40. package/dist/ir.cjs.map +1 -1
  41. package/dist/ir.d.cts +71 -71
  42. package/dist/ir.d.ts +71 -71
  43. package/dist/ir.js +50 -3
  44. package/dist/ir.js.map +1 -1
  45. package/dist/predicate-v2.cjs +50 -3
  46. package/dist/predicate-v2.cjs.map +1 -1
  47. package/dist/predicate-v2.d.cts +1 -1
  48. package/dist/predicate-v2.d.ts +1 -1
  49. package/dist/predicate-v2.js +50 -3
  50. package/dist/predicate-v2.js.map +1 -1
  51. package/dist/sdl.cjs +53 -3
  52. package/dist/sdl.cjs.map +1 -1
  53. package/dist/sdl.d.cts +3 -1
  54. package/dist/sdl.d.ts +3 -1
  55. package/dist/sdl.js +53 -3
  56. package/dist/sdl.js.map +1 -1
  57. package/package.json +1 -1
  58. package/dist/{definition-DjfC7Lll.d.cts → definition-B99BsOAO.d.cts} +4 -4
  59. package/dist/{definition-DjfC7Lll.d.ts → definition-B99BsOAO.d.ts} +4 -4
package/dist/index.cjs CHANGED
@@ -9,6 +9,7 @@ var promises = require('fs/promises');
9
9
  var module$1 = require('module');
10
10
  var path = require('path');
11
11
  var api = require('drizzle-kit/api');
12
+ var pgParser = require('@supabase/pg-parser');
12
13
  var jsYaml = require('js-yaml');
13
14
  var customRolesManifest = require('@sequenceholdings/custom-roles-manifest');
14
15
 
@@ -157,20 +158,24 @@ var t = {
157
158
  };
158
159
 
159
160
  // src/constants.ts
160
- var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
161
+ var NAMESPACE_PATTERN = /^(?!(?:events|native_sources)$)[a-z][a-z0-9_]{0,40}$/;
161
162
  var ORM_DATA_TOKEN_HEADER = "x-sequence-data-token";
162
163
  var ORM_SERVICE_DATA_TOKEN_HEADER = "x-sequence-data-token-service";
163
164
  var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
164
165
  var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
165
166
  var MAX_IDENTIFIER_LENGTH = 63;
166
167
  var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
168
+ var MAX_PERSISTED_READ_ROWS = 50001;
167
169
  var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
168
170
  var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]{0,62}\.[a-z][a-z0-9_]{0,62}$/;
169
171
  var SYNCED_SOURCE_PATTERN = QUALIFIED_TABLE_PATTERN;
170
172
  var ORM_SCHEMA_PREFIX = "orm_";
171
173
  var CONSUMER_ROLE_SUFFIX = "_consumer";
172
- var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
173
- var consumerRoleName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}${CONSUMER_ROLE_SUFFIX}`;
174
+ var ormSchemaName = (namespace) => {
175
+ if (!NAMESPACE_PATTERN.test(namespace)) throw new Error(`invalid or reserved ORM namespace '${namespace}'`);
176
+ return `${ORM_SCHEMA_PREFIX}${namespace}`;
177
+ };
178
+ var consumerRoleName = (namespace) => `${ormSchemaName(namespace)}${CONSUMER_ROLE_SUFFIX}`;
174
179
  var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
175
180
  var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
176
181
  var HASH_DOMAIN = "sequence-orm-definitions\0";
@@ -399,10 +404,10 @@ function defineSyncedTable(name2, columnsOrConfig, maybeConfig) {
399
404
  if (builder.isPrimaryKey) columnLevelPk.push(propertyName);
400
405
  }
401
406
  if (entries.length > 0) {
402
- const hasConfigPk = config.primaryKey !== void 0 && config.primaryKey.length > 0;
407
+ const hasConfigPk = config.primaryKey !== void 0;
403
408
  if (columnLevelPk.length === 0 && !hasConfigPk) {
404
409
  throw new OrmDefinitionError(
405
- `synced table "${name2}" declares columns but no primary key \u2014 use .primaryKey() or config.primaryKey`,
410
+ `synced table "${name2}" declares columns but no primary key \u2014 use .primaryKey(), config.primaryKey, or explicit primaryKey: [] for an unkeyed read-only source`,
406
411
  file
407
412
  );
408
413
  }
@@ -1255,10 +1260,10 @@ function validateResolvedView({ view, tables, views = [] }) {
1255
1260
  const resolved = view.resolved;
1256
1261
  if (!resolved) return [];
1257
1262
  const errors = [];
1258
- const fail = (message) => errors.push(`resolved table '${view.view}': ${message}`);
1263
+ const fail2 = (message) => errors.push(`resolved table '${view.view}': ${message}`);
1259
1264
  const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
1260
1265
  if (!pkColumn) {
1261
- fail("requires exactly one primary-key column");
1266
+ fail2("requires exactly one primary-key column");
1262
1267
  return errors;
1263
1268
  }
1264
1269
  const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
@@ -1268,92 +1273,93 @@ function validateResolvedView({ view, tables, views = [] }) {
1268
1273
  pkColumn,
1269
1274
  dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
1270
1275
  sourceByName: /* @__PURE__ */ new Map(),
1271
- fail
1276
+ fail: fail2
1272
1277
  };
1273
- if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail("requires exactly one manual source");
1278
+ if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail2("requires exactly one manual source");
1274
1279
  const tableByName = new Map(tables.map((table) => [table.table, table]));
1275
1280
  for (const source of resolved.sources) {
1276
- if (scope.sourceByName.has(source.name)) fail(`duplicate source '${source.name}'`);
1281
+ if (scope.sourceByName.has(source.name)) fail2(`duplicate source '${source.name}'`);
1277
1282
  scope.sourceByName.set(source.name, source);
1278
1283
  const table = tableByName.get(source.table);
1279
- if (!table) fail(`source '${source.name}' references unknown table '${source.table}'`);
1284
+ if (!table) fail2(`source '${source.name}' references unknown table '${source.table}'`);
1280
1285
  else validateSource({ scope, source, table });
1281
1286
  }
1282
1287
  validateRules(scope);
1283
1288
  validateViewCells(scope);
1284
1289
  if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
1285
1290
  for (const name2 of resolved.deletedBy) {
1286
- if (scope.sourceByName.get(name2)?.kind !== "manual") fail(`deletedBy '${name2}' must be the manual source`);
1291
+ if (scope.sourceByName.get(name2)?.kind !== "manual") fail2(`deletedBy '${name2}' must be the manual source`);
1287
1292
  }
1288
1293
  return errors;
1289
1294
  }
1290
1295
  function validateSource({ scope, source, table }) {
1291
- const { fail } = scope;
1292
- if (source.kind === "etl" && table.source === void 0) fail(`etl source '${source.name}' must reference a synced table`);
1296
+ const { fail: fail2 } = scope;
1297
+ if (source.kind === "etl" && table.source === void 0) fail2(`etl source '${source.name}' must reference a synced table`);
1298
+ if (source.kind === "etl" && table.primaryKey?.length === 0) fail2(`etl source '${source.name}' cannot use an unkeyed synced table for resolved row identity`);
1293
1299
  if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
1294
- fail(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
1300
+ fail2(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
1295
1301
  }
1296
1302
  const hasRev = table.columns.some((column) => column.cell?.part === "rev");
1297
- if (source.kind === "manual" && !hasRev) fail(`manual source '${source.name}' must carry '__rev' cell columns`);
1298
- if (source.kind === "prefetch" && hasRev) fail(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
1303
+ if (source.kind === "manual" && !hasRev) fail2(`manual source '${source.name}' must carry '__rev' cell columns`);
1304
+ if (source.kind === "prefetch" && hasRev) fail2(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
1299
1305
  const byName = new Map(table.columns.map((c) => [c.name, c]));
1300
1306
  validateSourceColumns({ scope, source, byName });
1301
1307
  if (source.kind === "etl") {
1302
1308
  const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
1303
1309
  if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
1304
- fail(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
1310
+ fail2(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
1305
1311
  }
1306
1312
  }
1307
1313
  validatePrefetchFields({ scope, source });
1308
1314
  }
1309
1315
  function validatePrefetchFields({ scope, source }) {
1310
- const { fail, pkColumn } = scope;
1316
+ const { fail: fail2, pkColumn } = scope;
1311
1317
  if (source.kind !== "prefetch") {
1312
1318
  if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
1313
- fail(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
1319
+ fail2(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
1314
1320
  }
1315
1321
  return;
1316
1322
  }
1317
1323
  if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
1318
- fail(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
1324
+ fail2(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
1319
1325
  } else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
1320
- fail(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
1326
+ fail2(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
1321
1327
  }
1322
1328
  for (const column of source.writeback?.columns ?? []) {
1323
- if (!source.columns.includes(column)) fail(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
1329
+ if (!source.columns.includes(column)) fail2(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
1324
1330
  }
1325
1331
  }
1326
1332
  function validateSourceColumns({ scope, source, byName }) {
1327
- const { fail, pkColumn, dataColumns } = scope;
1333
+ const { fail: fail2, pkColumn, dataColumns } = scope;
1328
1334
  for (const column of [pkColumn.name, ...source.columns]) {
1329
1335
  const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
1330
1336
  const theirs = byName.get(column);
1331
- if (!own) fail(`source '${source.name}' supplies unknown column '${column}'`);
1332
- else if (!theirs) fail(`source '${source.name}' table '${source.table}' has no column '${column}'`);
1333
- else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail(`column '${column}' has a different type on source '${source.name}'`);
1337
+ if (!own) fail2(`source '${source.name}' supplies unknown column '${column}'`);
1338
+ else if (!theirs) fail2(`source '${source.name}' table '${source.table}' has no column '${column}'`);
1339
+ else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail2(`column '${column}' has a different type on source '${source.name}'`);
1334
1340
  }
1335
1341
  }
1336
- function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail }) {
1342
+ function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail: fail2 }) {
1337
1343
  const ruled = /* @__PURE__ */ new Set();
1338
1344
  for (const rule of resolved.resolve) {
1339
1345
  if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
1340
- fail(`resolve rule targets unknown column '${rule.column}'`);
1346
+ fail2(`resolve rule targets unknown column '${rule.column}'`);
1341
1347
  continue;
1342
1348
  }
1343
- if (ruled.has(rule.column)) fail(`column '${rule.column}' has more than one resolve rule`);
1349
+ if (ruled.has(rule.column)) fail2(`column '${rule.column}' has more than one resolve rule`);
1344
1350
  ruled.add(rule.column);
1345
1351
  if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
1346
- fail(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
1352
+ fail2(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
1347
1353
  }
1348
- if (new Set(rule.candidates).size !== rule.candidates.length) fail(`column '${rule.column}' repeats a candidate`);
1354
+ if (new Set(rule.candidates).size !== rule.candidates.length) fail2(`column '${rule.column}' repeats a candidate`);
1349
1355
  for (const candidate of rule.candidates) {
1350
1356
  const source = sourceByName.get(candidate);
1351
- if (!source) fail(`column '${rule.column}' names unknown candidate source '${candidate}'`);
1352
- else if (!source.columns.includes(rule.column)) fail(`column '${rule.column}' candidate '${candidate}' does not supply it`);
1357
+ if (!source) fail2(`column '${rule.column}' names unknown candidate source '${candidate}'`);
1358
+ else if (!source.columns.includes(rule.column)) fail2(`column '${rule.column}' candidate '${candidate}' does not supply it`);
1353
1359
  }
1354
1360
  }
1355
1361
  for (const column of dataColumns.keys()) {
1356
- if (column !== pkColumn.name && !ruled.has(column)) fail(`column '${column}' has no resolve rule`);
1362
+ if (column !== pkColumn.name && !ruled.has(column)) fail2(`column '${column}' has no resolve rule`);
1357
1363
  }
1358
1364
  }
1359
1365
  var VIEW_CELL_TYPES = {
@@ -1362,7 +1368,7 @@ var VIEW_CELL_TYPES = {
1362
1368
  by: "text",
1363
1369
  conflicts: "jsonb"
1364
1370
  };
1365
- function validateViewCells({ view, dataColumns, pkColumn, fail }) {
1371
+ function validateViewCells({ view, dataColumns, pkColumn, fail: fail2 }) {
1366
1372
  const expected = /* @__PURE__ */ new Map();
1367
1373
  for (const column of dataColumns.values()) {
1368
1374
  if (column.name === pkColumn.name) continue;
@@ -1374,34 +1380,34 @@ function validateViewCells({ view, dataColumns, pkColumn, fail }) {
1374
1380
  if (!column.cell) continue;
1375
1381
  const want = expected.get(column.name);
1376
1382
  if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
1377
- fail(`unexpected cell column '${column.name}'`);
1383
+ fail2(`unexpected cell column '${column.name}'`);
1378
1384
  continue;
1379
1385
  }
1380
- if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
1386
+ if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail2(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
1381
1387
  expected.delete(column.name);
1382
1388
  }
1383
- for (const name2 of expected.keys()) fail(`missing cell column '${name2}'`);
1389
+ for (const name2 of expected.keys()) fail2(`missing cell column '${name2}'`);
1384
1390
  }
1385
1391
  var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
1386
1392
  function validateMaterialized({ scope, tableByName, views }) {
1387
- const { view, resolved, fail } = scope;
1393
+ const { view, resolved, fail: fail2 } = scope;
1388
1394
  const { table: tableName, compute: computeName } = resolved.materialized;
1389
1395
  const table = tableByName.get(tableName);
1390
1396
  if (!table || table.internal !== true || table.materializes !== view.view) {
1391
- fail(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
1397
+ fail2(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
1392
1398
  } else {
1393
- if (columnSignature(table.columns) !== columnSignature(view.columns)) fail(`materialized table '${tableName}' must have exactly the view's columns`);
1394
- if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail(`materialized table '${tableName}' must share the view's primary key`);
1399
+ if (columnSignature(table.columns) !== columnSignature(view.columns)) fail2(`materialized table '${tableName}' must have exactly the view's columns`);
1400
+ if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail2(`materialized table '${tableName}' must share the view's primary key`);
1395
1401
  }
1396
1402
  const compute = views.find((candidate) => candidate.view === computeName);
1397
- if (!compute || compute.internal !== true) fail(`compute view '${computeName}' must be an internal view`);
1398
- else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail(`compute view '${computeName}' must have exactly the view's columns`);
1403
+ if (!compute || compute.internal !== true) fail2(`compute view '${computeName}' must be an internal view`);
1404
+ else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail2(`compute view '${computeName}' must have exactly the view's columns`);
1399
1405
  for (const source of resolved.sources) {
1400
1406
  if (source.kind !== "etl") continue;
1401
1407
  const read = tableByName.get(source.table)?.policies?.read;
1402
1408
  const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
1403
1409
  if (!unconditional) {
1404
- fail(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
1410
+ fail2(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
1405
1411
  }
1406
1412
  }
1407
1413
  }
@@ -1738,7 +1744,7 @@ var canonicalTableSchema = zod.z.object({
1738
1744
  /** Synced/reflected read-only tables (`source: schema.table`) — v1 semantics. */
1739
1745
  source: zod.z.string().regex(QUALIFIED_TABLE_PATTERN).optional(),
1740
1746
  columns: zod.z.array(canonicalColumnSchema).default([]),
1741
- primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
1747
+ primaryKey: zod.z.array(columnIdentifier).max(8).optional(),
1742
1748
  links: zod.z.array(canonicalLinkSchema).default([]),
1743
1749
  indexes: zod.z.array(canonicalIndexSchema).default([]),
1744
1750
  /** Omitted by old definitions, preserving their existing content hashes. */
@@ -1774,6 +1780,29 @@ var canonicalTableSchema = zod.z.object({
1774
1780
  * data plane. Omitted otherwise.
1775
1781
  */
1776
1782
  materializes: identifier2.optional()
1783
+ }).superRefine((table, ctx) => {
1784
+ if (table.primaryKey?.length !== 0) return;
1785
+ const fail2 = (message, path = ["primaryKey"]) => ctx.addIssue({ code: "custom", path, message: `unkeyed table '${table.table}' ${message}` });
1786
+ if (table.source === void 0 || table.columns.length === 0) {
1787
+ fail2("requires a typed synced source");
1788
+ }
1789
+ if ([table.policies.write, table.policies.insert, table.policies.update].some((policy) => policy !== void 0)) {
1790
+ fail2("is read-only and cannot declare write policies", ["policies"]);
1791
+ }
1792
+ const ownedCollections = [table.indexes, table.actions, table.checks, table.foreignKeys, table.cowMutations];
1793
+ const ownedMetadata = [table.cellMetadata, table.internal, table.materializes, table.renamedFrom];
1794
+ if (ownedCollections.some((items) => items !== void 0 && items.length > 0) || ownedMetadata.some((value) => value !== void 0)) {
1795
+ fail2("cannot declare owned storage, constraints or identity operations");
1796
+ }
1797
+ for (const [index3, column] of table.columns.entries()) {
1798
+ const ownedColumnMetadata = [column.default, column.defaultFn, column.onUpdateFn, column.renamedFrom, column.cast, column.cell];
1799
+ if (column.unique || ownedColumnMetadata.some((value) => value !== void 0)) {
1800
+ fail2("cannot declare column uniqueness, mutation defaults or migration metadata", ["columns", index3]);
1801
+ }
1802
+ }
1803
+ for (const [index3, link] of table.links.entries()) {
1804
+ if (link.enforce || link.kind === "toMany") fail2("cannot provide an identity for this link", ["links", index3]);
1805
+ }
1777
1806
  });
1778
1807
  var MAX_VIEW_QUERY = 1e5;
1779
1808
  var MAX_RESOLVED_VIEW_QUERY = 5e6;
@@ -1830,6 +1859,26 @@ var canonicalViewSchema = zod.z.object({
1830
1859
  }
1831
1860
  }
1832
1861
  });
1862
+ function validateUnkeyedLinkIdentities({ definition, ctx }) {
1863
+ const tables = new Map(definition.tables.map((table) => [table.table, table]));
1864
+ const relations = [
1865
+ ...definition.tables.map((table, index3) => ({ kind: "tables", index: index3, links: table.links })),
1866
+ ...(definition.views ?? []).map((view, index3) => ({ kind: "views", index: index3, links: view.links }))
1867
+ ];
1868
+ for (const relation of relations) {
1869
+ for (const [linkIndex, link] of relation.links.entries()) {
1870
+ if (link.kind !== "toOne" || link.namespace !== void 0 && link.namespace !== definition.namespace) continue;
1871
+ const target = link.target === void 0 ? void 0 : tables.get(link.target);
1872
+ if (target?.source !== void 0 && target.primaryKey?.length === 0) {
1873
+ ctx.addIssue({
1874
+ code: "custom",
1875
+ path: [relation.kind, relation.index, "links", linkIndex],
1876
+ message: `link '${link.name}' cannot use unkeyed table '${target.table}' as a to-one identity`
1877
+ });
1878
+ }
1879
+ }
1880
+ }
1881
+ }
1833
1882
  var CELL_PART_TYPES = {
1834
1883
  at: "timestamptz",
1835
1884
  by: "text",
@@ -1843,49 +1892,49 @@ function trackedCellColumns(table) {
1843
1892
  }
1844
1893
  var SUFFIXES = Object.values(CELL_PART_SUFFIX);
1845
1894
  function validateCellMetadata(table, index3, ctx) {
1846
- const fail = (message, path = ["tables", index3]) => ctx.addIssue({ code: "custom", path, message });
1895
+ const fail2 = (message, path = ["tables", index3]) => ctx.addIssue({ code: "custom", path, message });
1847
1896
  const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
1848
1897
  if (table.materializes !== void 0) return;
1849
1898
  if (table.cellMetadata !== true) {
1850
- for (const { position } of cells) fail(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index3, "columns", position]);
1899
+ for (const { position } of cells) fail2(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index3, "columns", position]);
1851
1900
  return;
1852
1901
  }
1853
1902
  if (table.source !== void 0) {
1854
- fail(`synced table '${table.table}' cannot track cell metadata`);
1903
+ fail2(`synced table '${table.table}' cannot track cell metadata`);
1855
1904
  return;
1856
1905
  }
1857
1906
  const tracked = trackedCellColumns(table);
1858
1907
  const byName = new Map(tracked.map((column) => [column.name, column]));
1859
1908
  const withRev = cells.some(({ cell }) => cell.part === "rev");
1860
- if (withRev && table.internal !== true) fail(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
1909
+ if (withRev && table.internal !== true) fail2(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
1861
1910
  const seen = /* @__PURE__ */ new Set();
1862
1911
  for (const { column, position, cell } of cells) {
1863
1912
  const path = ["tables", index3, "columns", position];
1864
1913
  const owner = byName.get(cell.of);
1865
1914
  if (!owner) {
1866
- fail(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
1915
+ fail2(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
1867
1916
  continue;
1868
1917
  }
1869
- if (cell.part !== "at" && cell.part !== "by" && cell.part !== "rev") fail(`a table cell column must be part at/by/rev, not '${cell.part}'`, path);
1918
+ if (cell.part !== "at" && cell.part !== "by" && cell.part !== "rev") fail2(`a table cell column must be part at/by/rev, not '${cell.part}'`, path);
1870
1919
  if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
1871
- fail(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
1920
+ fail2(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
1872
1921
  }
1873
1922
  if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
1874
- fail(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
1923
+ fail2(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
1875
1924
  }
1876
1925
  const key = `${cell.of}:${cell.part}`;
1877
- if (seen.has(key)) fail(`duplicate cell column '${column.name}'`, path);
1926
+ if (seen.has(key)) fail2(`duplicate cell column '${column.name}'`, path);
1878
1927
  seen.add(key);
1879
1928
  }
1880
1929
  for (const column of tracked) {
1881
1930
  if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
1882
- fail(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
1931
+ fail2(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
1883
1932
  }
1884
1933
  if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
1885
- fail(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
1934
+ fail2(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
1886
1935
  }
1887
1936
  for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
1888
- if (!seen.has(`${column.name}:${part}`)) fail(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
1937
+ if (!seen.has(`${column.name}:${part}`)) fail2(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
1889
1938
  }
1890
1939
  }
1891
1940
  }
@@ -1893,7 +1942,7 @@ var exportGateSchema = zod.z.union([zod.z.literal(true), zod.z.object({ capabili
1893
1942
  var relationExportSchema = zod.z.object({ read: exportGateSchema, write: exportGateSchema.optional() }).strict();
1894
1943
  var localDefinitionSchema = zod.z.object({
1895
1944
  definition_format: zod.z.literal(CANONICAL_FORMAT),
1896
- namespace: identifier2.max(41),
1945
+ namespace: identifier2.max(41).regex(NAMESPACE_PATTERN, "invalid or reserved ORM namespace"),
1897
1946
  description: zod.z.string().max(2e3).optional(),
1898
1947
  enums: zod.z.array(canonicalEnumSchema).default([]),
1899
1948
  tables: zod.z.array(canonicalTableSchema).min(0).max(500),
@@ -1927,6 +1976,7 @@ var localDefinitionSchema = zod.z.object({
1927
1976
  }
1928
1977
  }
1929
1978
  const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
1979
+ validateUnkeyedLinkIdentities({ definition, ctx });
1930
1980
  const relations = /* @__PURE__ */ new Set([...fkTables.keys(), ...(definition.views ?? []).map((view) => view.view)]);
1931
1981
  for (const name2 of Object.keys(definition.exports ?? {})) {
1932
1982
  if (!relations.has(name2)) ctx.addIssue({ code: "custom", path: ["exports", name2], message: `export '${name2}' is not a local object` });
@@ -1952,38 +2002,38 @@ var localDefinitionSchema = zod.z.object({
1952
2002
  }
1953
2003
  }
1954
2004
  for (const [index3, check] of (table.checks ?? []).entries()) {
1955
- const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index3], message });
1956
- if (table.source) fail("synced tables cannot declare checks");
1957
- if (names.has(check.name)) fail(`duplicate constraint '${check.name}'`);
2005
+ const fail2 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index3], message });
2006
+ if (table.source) fail2("synced tables cannot declare checks");
2007
+ if (names.has(check.name)) fail2(`duplicate constraint '${check.name}'`);
1958
2008
  names.add(check.name);
1959
2009
  try {
1960
2010
  compileCheckExpression(check.expression, table.columns);
1961
2011
  } catch (error) {
1962
- fail(error instanceof Error ? error.message : String(error));
2012
+ fail2(error instanceof Error ? error.message : String(error));
1963
2013
  }
1964
2014
  }
1965
2015
  for (const [index3, fk] of (table.foreignKeys ?? []).entries()) {
1966
- const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index3], message });
1967
- if (names.has(fk.name)) fail(`duplicate foreign key '${fk.name}'`);
2016
+ const fail2 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index3], message });
2017
+ if (names.has(fk.name)) fail2(`duplicate foreign key '${fk.name}'`);
1968
2018
  names.add(fk.name);
1969
2019
  const target = fkTables.get(fk.target);
1970
2020
  if (table.source || !target || target.source) {
1971
- fail("foreign keys must connect owned tables in the same namespace");
2021
+ fail2("foreign keys must connect owned tables in the same namespace");
1972
2022
  continue;
1973
2023
  }
1974
- if (fk.columns.length !== fk.references.length) fail("foreign key columns and references must have the same number of columns");
1975
- if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail("foreign key columns must not repeat");
2024
+ if (fk.columns.length !== fk.references.length) fail2("foreign key columns and references must have the same number of columns");
2025
+ if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail2("foreign key columns must not repeat");
1976
2026
  for (const [position, name2] of fk.columns.entries()) {
1977
2027
  const sourceColumn = table.columns.find((column) => column.columnName === name2);
1978
2028
  const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
1979
- if (!sourceColumn || !targetColumn) fail("foreign key references an unknown column");
1980
- else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail("foreign key column types must match");
2029
+ if (!sourceColumn || !targetColumn) fail2("foreign key references an unknown column");
2030
+ else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail2("foreign key column types must match");
1981
2031
  }
1982
2032
  if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
1983
- if (fk.columns.some((name2) => table.columns.find((column) => column.columnName === name2)?.nullable === false || table.primaryKey?.includes(name2))) fail("SET NULL requires nullable source columns");
2033
+ if (fk.columns.some((name2) => table.columns.find((column) => column.columnName === name2)?.nullable === false || table.primaryKey?.includes(name2))) fail2("SET NULL requires nullable source columns");
1984
2034
  }
1985
2035
  const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index4) => index4.unique).map((index4) => index4.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
1986
- if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column)))) fail("foreign key target columns must form a unique key");
2036
+ if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column)))) fail2("foreign key target columns must form a unique key");
1987
2037
  }
1988
2038
  }
1989
2039
  for (const [viewIndex, view] of (definition.views ?? []).entries()) {
@@ -2171,8 +2221,8 @@ function cloneColumn({ builder, property, notNull, primaryKey: primaryKey2 }) {
2171
2221
  return clone;
2172
2222
  }
2173
2223
  function planSource(input) {
2174
- const { sourceName, input: source, fail } = input;
2175
- if (!IDENTIFIER_PATTERN.test(sourceName)) fail(`source name "${sourceName}" must match ${IDENTIFIER_PATTERN}`);
2224
+ const { sourceName, input: source, fail: fail2 } = input;
2225
+ if (!IDENTIFIER_PATTERN.test(sourceName)) fail2(`source name "${sourceName}" must match ${IDENTIFIER_PATTERN}`);
2176
2226
  switch (source.kind) {
2177
2227
  case "manual":
2178
2228
  return {
@@ -2208,30 +2258,31 @@ function internalTable(input, supplied, system, options = {}) {
2208
2258
  internal: true
2209
2259
  });
2210
2260
  }
2211
- function assertColumns(input, list, what) {
2212
- for (const column of list) if (!input.dataColumns.includes(column)) input.fail(`${what} names unknown column "${column}"`);
2261
+ function assertColumns(input, list2, what) {
2262
+ for (const column of list2) if (!input.dataColumns.includes(column)) input.fail(`${what} names unknown column "${column}"`);
2213
2263
  }
2214
2264
  function planEtl(input, source) {
2215
- const { sourceName, pk, dataColumns, fail } = input;
2265
+ const { sourceName, pk, dataColumns, fail: fail2 } = input;
2216
2266
  const table = typeof source.table === "function" ? source.table() : source.table;
2217
- if (!isSeqTable(table) || table.source === void 0) fail(`etl source "${sourceName}" must reference a defineSyncedTable`);
2267
+ if (!isSeqTable(table) || table.source === void 0) fail2(`etl source "${sourceName}" must reference a defineSyncedTable`);
2218
2268
  const synced = table;
2219
- if (!(pk in synced.columns)) fail(`etl source "${sourceName}" table "${synced.tableName}" has no "${pk}" column`);
2269
+ if (synced.config.primaryKey?.length === 0) fail2(`etl source "${sourceName}" cannot use an unkeyed synced table for resolved row identity`);
2270
+ if (!(pk in synced.columns)) fail2(`etl source "${sourceName}" table "${synced.tableName}" has no "${pk}" column`);
2220
2271
  const supplied = source.columns ? [...source.columns] : dataColumns.filter((column) => column in synced.columns);
2221
2272
  assertColumns(input, supplied, `etl source "${sourceName}"`);
2222
2273
  if (source.updatedAt !== void 0 && !(source.updatedAt in synced.columns)) {
2223
- fail(`etl source "${sourceName}" updatedAt "${source.updatedAt}" is not a column of "${synced.tableName}"`);
2274
+ fail2(`etl source "${sourceName}" updatedAt "${source.updatedAt}" is not a column of "${synced.tableName}"`);
2224
2275
  }
2225
2276
  return { name: sourceName, kind: "etl", table: synced, columns: supplied, ...source.updatedAt ? { updatedAt: source.updatedAt } : {} };
2226
2277
  }
2227
2278
  function planPrefetch(input, source) {
2228
- const { sourceName, pk, fail } = input;
2279
+ const { sourceName, pk, fail: fail2 } = input;
2229
2280
  assertColumns(input, source.columns, `prefetch source "${sourceName}"`);
2230
2281
  if (source.key !== pk && !source.columns.includes(source.key)) {
2231
- fail(`prefetch source "${sourceName}" key "${source.key}" must be the primary key or one of its columns`);
2282
+ fail2(`prefetch source "${sourceName}" key "${source.key}" must be the primary key or one of its columns`);
2232
2283
  }
2233
2284
  for (const column of source.writeback?.columns ?? []) {
2234
- if (!source.columns.includes(column)) fail(`prefetch source "${sourceName}" writes back "${column}", which it does not supply`);
2285
+ if (!source.columns.includes(column)) fail2(`prefetch source "${sourceName}" writes back "${column}", which it does not supply`);
2235
2286
  }
2236
2287
  const writeback = source.writeback;
2237
2288
  return {
@@ -2437,34 +2488,34 @@ var SYSTEM_VIEW_COLUMNS = {
2437
2488
  };
2438
2489
  function defineResolvedTable(name2, columns, config) {
2439
2490
  const file = activeAuthoringContext()?.currentFile;
2440
- const fail = (message) => {
2491
+ const fail2 = (message) => {
2441
2492
  throw new OrmDefinitionError(`resolved table "${name2}": ${message}`, file);
2442
2493
  };
2443
- if (!IDENTIFIER_PATTERN.test(name2)) fail(`name must match ${IDENTIFIER_PATTERN}`);
2494
+ if (!IDENTIFIER_PATTERN.test(name2)) fail2(`name must match ${IDENTIFIER_PATTERN}`);
2444
2495
  for (const property of Object.keys(columns)) {
2445
2496
  if (RESERVED_RESOLVED_COLUMN_NAMES.has(property) || RESERVED_RESOLVED_COLUMN_NAMES.has(columns[property].columnNameFor(property))) {
2446
- fail(`column "${property}" collides with a reserved system column`);
2497
+ fail2(`column "${property}" collides with a reserved system column`);
2447
2498
  }
2448
2499
  }
2449
2500
  for (const property of Object.keys(columns)) {
2450
2501
  if (columns[property].columnNameFor(property).length > MAX_TRACKED_COLUMN_NAME) {
2451
- fail(`column "${property}" is too long for a resolved table (max ${MAX_TRACKED_COLUMN_NAME} characters, room for cell parts)`);
2502
+ fail2(`column "${property}" is too long for a resolved table (max ${MAX_TRACKED_COLUMN_NAME} characters, room for cell parts)`);
2452
2503
  }
2453
2504
  }
2454
2505
  const pkEntries = Object.entries(columns).filter(([, builder]) => builder.isPrimaryKey);
2455
- if (pkEntries.length !== 1) fail("declare exactly one .primaryKey() column");
2506
+ if (pkEntries.length !== 1) fail2("declare exactly one .primaryKey() column");
2456
2507
  const [pk, pkBuilder] = pkEntries[0];
2457
2508
  const dataColumns = Object.keys(columns).filter((property) => property !== pk);
2458
- if (dataColumns.length === 0) fail("declare at least one non-key column");
2509
+ if (dataColumns.length === 0) fail2("declare at least one non-key column");
2459
2510
  const sourceEntries = Object.entries(config.sources);
2460
- if (sourceEntries.filter(([, source]) => source.kind === "manual").length !== 1) fail("declare exactly one manualSource()");
2511
+ if (sourceEntries.filter(([, source]) => source.kind === "manual").length !== 1) fail2("declare exactly one manualSource()");
2461
2512
  const plans = sourceEntries.map(
2462
- ([sourceName, input]) => planSource({ viewName: name2, sourceName, input, columns, pk, pkBuilder, dataColumns, fail })
2513
+ ([sourceName, input]) => planSource({ viewName: name2, sourceName, input, columns, pk, pkBuilder, dataColumns, fail: fail2 })
2463
2514
  );
2464
- const rules = planRules({ config, plans, dataColumns, fail });
2515
+ const rules = planRules({ config, plans, dataColumns, fail: fail2 });
2465
2516
  const deletedBy = [...config.rows?.deletedBy ?? []];
2466
2517
  for (const source of deletedBy) {
2467
- if (plans.find((plan) => plan.name === source)?.kind !== "manual") fail(`rows.deletedBy "${source}" must be the manual source`);
2518
+ if (plans.find((plan) => plan.name === source)?.kind !== "manual") fail2(`rows.deletedBy "${source}" must be the manual source`);
2468
2519
  }
2469
2520
  const partType = { source: t.text, at: t.timestamptz, by: t.text, conflicts: t.jsonb };
2470
2521
  const viewColumns = {};
@@ -2527,11 +2578,11 @@ function defineResolvedTable(name2, columns, config) {
2527
2578
  }
2528
2579
  });
2529
2580
  }
2530
- function planRules({ config, plans, dataColumns, fail }) {
2581
+ function planRules({ config, plans, dataColumns, fail: fail2 }) {
2531
2582
  const kindOrder = { manual: 0, prefetch: 1, etl: 2 };
2532
2583
  const fallback = config.default ?? priority([...plans].sort((a, b) => kindOrder[a.kind] - kindOrder[b.kind]).map((plan) => plan.name));
2533
2584
  for (const column of Object.keys(config.resolve ?? {})) {
2534
- if (!dataColumns.includes(column)) fail(`resolve names unknown column "${column}"`);
2585
+ if (!dataColumns.includes(column)) fail2(`resolve names unknown column "${column}"`);
2535
2586
  }
2536
2587
  return dataColumns.map((column) => {
2537
2588
  const explicit = config.resolve?.[column];
@@ -2539,11 +2590,11 @@ function planRules({ config, plans, dataColumns, fail }) {
2539
2590
  const candidates = explicit ? [...explicit.candidates] : comparator.candidates.filter((name2) => plans.find((plan) => plan.name === name2)?.columns.includes(column));
2540
2591
  for (const candidate of candidates) {
2541
2592
  const plan = plans.find((entry) => entry.name === candidate);
2542
- if (!plan) fail(`column "${column}" names unknown source "${candidate}"`);
2543
- else if (!plan.columns.includes(column)) fail(`column "${column}" candidate "${candidate}" does not supply it`);
2593
+ if (!plan) fail2(`column "${column}" names unknown source "${candidate}"`);
2594
+ else if (!plan.columns.includes(column)) fail2(`column "${column}" candidate "${candidate}" does not supply it`);
2544
2595
  }
2545
- if (candidates.length === 0) fail(`column "${column}" has no candidate source`);
2546
- if (comparator.kind === "custom") assertSafeOrderBy({ orderBy: comparator.orderBy ?? "", column, fail });
2596
+ if (candidates.length === 0) fail2(`column "${column}" has no candidate source`);
2597
+ if (comparator.kind === "custom") assertSafeOrderBy({ orderBy: comparator.orderBy ?? "", column, fail: fail2 });
2547
2598
  return {
2548
2599
  column,
2549
2600
  comparator: comparator.kind,
@@ -2594,18 +2645,18 @@ var ORDER_BY_KEYWORDS = /* @__PURE__ */ new Set([
2594
2645
  ]);
2595
2646
  var ALLOWED_FUNCTIONS = new Set(VIEW_QUERY_FUNCTION_ALLOWLIST);
2596
2647
  var ORDER_BY_TOKEN = /\s+|'(?:[^']|'')*'|\d+(?:\.\d+)?|[a-z_][a-z0-9_]*|::|<=|>=|<>|!=|[-+*/%<>=(),|]/giy;
2597
- function assertSafeOrderBy({ orderBy, column, fail }) {
2648
+ function assertSafeOrderBy({ orderBy, column, fail: fail2 }) {
2598
2649
  ORDER_BY_TOKEN.lastIndex = 0;
2599
2650
  let consumed = 0;
2600
2651
  for (let match = ORDER_BY_TOKEN.exec(orderBy); match; match = ORDER_BY_TOKEN.exec(orderBy)) {
2601
2652
  consumed = ORDER_BY_TOKEN.lastIndex;
2602
2653
  const token = match[0].toLowerCase();
2603
2654
  if (/^[a-z_]/.test(token) && !ORDER_BY_KEYWORDS.has(token) && !ALLOWED_FUNCTIONS.has(token)) {
2604
- fail(`column "${column}" custom orderBy uses "${match[0]}" \u2014 only value, updated_at, source, actor, ord, SQL keywords and allowlisted functions`);
2655
+ fail2(`column "${column}" custom orderBy uses "${match[0]}" \u2014 only value, updated_at, source, actor, ord, SQL keywords and allowlisted functions`);
2605
2656
  }
2606
2657
  }
2607
2658
  if (consumed !== orderBy.length || orderBy.trim() === "") {
2608
- fail(`column "${column}" custom orderBy contains an unsupported character near "${orderBy.slice(consumed, consumed + 10)}"`);
2659
+ fail2(`column "${column}" custom orderBy contains an unsupported character near "${orderBy.slice(consumed, consumed + 10)}"`);
2609
2660
  }
2610
2661
  }
2611
2662
  function stableJson(value) {
@@ -3133,17 +3184,17 @@ function buildCanonicalLink({
3133
3184
  }) {
3134
3185
  const ownerKind = relationKind(relation);
3135
3186
  const ownerName = relationName(relation);
3136
- const fail = (message) => {
3187
+ const fail2 = (message) => {
3137
3188
  throw new OrmDefinitionError(`${ownerKind} "${ownerName}": link "${linkName}" ${message}`, relation.file);
3138
3189
  };
3139
3190
  if (link.enforce && link.linkKind !== "toOne") {
3140
- return fail("sets enforce: true, which is only supported on toOne links");
3191
+ return fail2("sets enforce: true, which is only supported on toOne links");
3141
3192
  }
3142
3193
  if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && !link.enforce) {
3143
- return fail("sets a referential action without enforce: true");
3194
+ return fail2("sets a referential action without enforce: true");
3144
3195
  }
3145
3196
  if (link.linkKind === "toPlatformUser") {
3146
- if (link.target !== void 0) return fail("is toPlatformUser and must not declare a target table");
3197
+ if (link.target !== void 0) return fail2("is toPlatformUser and must not declare a target table");
3147
3198
  return {
3148
3199
  name: linkName,
3149
3200
  kind: "toPlatformUser",
@@ -3154,7 +3205,7 @@ function buildCanonicalLink({
3154
3205
  };
3155
3206
  }
3156
3207
  if (typeof link.target === "object" && link.target?.kind === "seq-external-object") {
3157
- if (link.enforce) return fail("cannot enforce a cross-namespace physical foreign key; use an ORM link without enforce");
3208
+ if (link.enforce) return fail2("cannot enforce a cross-namespace physical foreign key; use an ORM link without enforce");
3158
3209
  return {
3159
3210
  name: linkName,
3160
3211
  kind: link.linkKind,
@@ -3166,13 +3217,13 @@ function buildCanonicalLink({
3166
3217
  ...link.description !== void 0 ? { description: link.description } : {}
3167
3218
  };
3168
3219
  }
3169
- const target = resolveRelationRef({ ref: link.target, fail });
3220
+ const target = resolveRelationRef({ ref: link.target, fail: fail2 });
3170
3221
  const targetName = relationName(target);
3171
3222
  if (!relationsByName.has(targetName)) {
3172
- return fail(`targets ${relationKind(target)} "${targetName}" which is not part of this namespace`);
3223
+ return fail2(`targets ${relationKind(target)} "${targetName}" which is not part of this namespace`);
3173
3224
  }
3174
3225
  if (link.enforce && (!isSeqTable(relation) || !isSeqTable(target))) {
3175
- return fail("cannot enforce a PostgreSQL foreign key involving a view");
3226
+ return fail2("cannot enforce a PostgreSQL foreign key involving a view");
3176
3227
  }
3177
3228
  const owningSide = link.linkKind === "toOne" ? relation : target;
3178
3229
  const referencedSide = link.linkKind === "toOne" ? target : relation;
@@ -3188,19 +3239,19 @@ function buildCanonicalLink({
3188
3239
  ...link.description !== void 0 ? { description: link.description } : {}
3189
3240
  };
3190
3241
  }
3191
- function resolveRelationRef({ ref, fail }) {
3192
- if (ref === void 0) return fail("declares no target relation");
3242
+ function resolveRelationRef({ ref, fail: fail2 }) {
3243
+ if (ref === void 0) return fail2("declares no target relation");
3193
3244
  if (typeof ref !== "function") return ref;
3194
3245
  let resolved;
3195
3246
  try {
3196
3247
  resolved = ref();
3197
3248
  } catch (error) {
3198
- return fail(
3249
+ return fail2(
3199
3250
  `target thunk threw (${error instanceof Error ? error.message : String(error)}) \u2014 lazy refs resolve at serialize time; make sure the target module ran`
3200
3251
  );
3201
3252
  }
3202
3253
  if (!isSeqTable(resolved) && !isSeqView(resolved)) {
3203
- return fail("target thunk did not return a relation defined with defineTable/defineView");
3254
+ return fail2("target thunk did not return a relation defined with defineTable/defineView");
3204
3255
  }
3205
3256
  return resolved;
3206
3257
  }
@@ -3303,9 +3354,9 @@ function extractOrmSyncedTableContract({
3303
3354
  );
3304
3355
  }
3305
3356
  const primaryKey2 = table.primaryKey;
3306
- if (primaryKey2 === void 0 || primaryKey2.length === 0) {
3357
+ if (primaryKey2 === void 0) {
3307
3358
  throw new OrmDefinitionError(
3308
- `synced table "${table.table}" has no primary key \u2014 typed synced contracts require a primary key`
3359
+ `synced table "${table.table}" has no primary key contract \u2014 declare a key or explicit primaryKey: []`
3309
3360
  );
3310
3361
  }
3311
3362
  const pkSet = new Set(primaryKey2);
@@ -4110,9 +4161,9 @@ function buildActionParamsSchema(action) {
4110
4161
  for (const [name2, param] of entries) {
4111
4162
  shape[name2] = param.optional ? PARAM_SCHEMAS[param.type].nullish() : PARAM_SCHEMAS[param.type];
4112
4163
  }
4113
- return zod.z.strictObject(shape).superRefine((obj, ctx) => {
4164
+ return zod.z.strictObject(shape).superRefine((obj2, ctx) => {
4114
4165
  for (const [name2, param] of entries) {
4115
- if (!param.optional && param.type === "jsonb" && obj[name2] === void 0) {
4166
+ if (!param.optional && param.type === "jsonb" && obj2[name2] === void 0) {
4116
4167
  ctx.addIssue({ code: "custom", message: "Required", path: [name2] });
4117
4168
  }
4118
4169
  }
@@ -4770,6 +4821,17 @@ function syncedViewStatements({
4770
4821
  const viewName = `${schema}.${quoteIdentifier(table.table)}`;
4771
4822
  const physical2 = splitPhysicalRelation(backing.physicalRelation);
4772
4823
  const physicalTarget = `${quoteIdentifier(physical2.schema)}.${quoteIdentifier(physical2.table)}`;
4824
+ const declaredColumns = Object.keys(table.columns ?? {});
4825
+ const projection = declaredColumns.length > 0 ? declaredColumns.map(quoteIdentifier).join(", ") : "*";
4826
+ if (backing.selectColumns) {
4827
+ if (declaredColumns.length === 0) {
4828
+ throw new OrmDefinitionError(`synced backing for "${table.table}" requires declared columns`);
4829
+ }
4830
+ const approved = new Set(backing.selectColumns);
4831
+ if (declaredColumns.some((column) => !approved.has(column))) {
4832
+ throw new OrmDefinitionError(`synced backing for "${table.table}" projects outside its approved column ceiling`);
4833
+ }
4834
+ }
4773
4835
  const columnTypes = mergeSyncedViewColumnTypes({
4774
4836
  table,
4775
4837
  canonicalColumnTypes: columnTypesByTable?.get(table.table),
@@ -4778,23 +4840,26 @@ function syncedViewStatements({
4778
4840
  });
4779
4841
  const readPolicyIr = readPolicyIrByTable?.get(table.table);
4780
4842
  const readPolicy = table.policies?.read;
4781
- const whereClause = readPolicyIr ? ` WHERE ${compilePredIrToSql(readPolicyIr, {
4843
+ const policyClause = readPolicyIr ? ` WHERE ${compilePredIrToSql(readPolicyIr, {
4782
4844
  namespace: definition.namespace,
4783
4845
  columnTypes,
4784
4846
  columnNames: columnNamesByTable?.get(table.table)
4785
4847
  })}` : readPolicy ? ` WHERE ${compilePredicate(readPolicy.ast, narrowToV1ColumnTypes(columnTypes))}` : " WHERE false";
4786
4848
  if (backingPrivileges === "grant") {
4849
+ const selectPrivilege = backing.selectColumns ? `SELECT (${backing.selectColumns.map(quoteIdentifier).join(", ")})` : "SELECT";
4850
+ const hasSelectPrivilege = backing.selectColumns ? backing.selectColumns.map((column) => `pg_catalog.has_column_privilege(${quoteLiteral(executionOwnerRole)}, ${quoteLiteral(physicalTarget)}, ${quoteLiteral(column)}, 'SELECT')`).join("\n AND ") : `pg_catalog.has_table_privilege(${quoteLiteral(executionOwnerRole)}, ${quoteLiteral(physicalTarget)}, 'SELECT')`;
4787
4851
  statements.push(`GRANT USAGE ON SCHEMA ${quoteIdentifier(physical2.schema)} TO ${owner}`);
4788
- statements.push(`GRANT SELECT ON ${physicalTarget} TO ${owner}`);
4852
+ statements.push(`GRANT ${selectPrivilege} ON ${physicalTarget} TO ${owner}`);
4789
4853
  statements.push(`DO ${quoteLiteral(`BEGIN
4790
4854
  IF NOT (pg_catalog.has_schema_privilege(${quoteLiteral(executionOwnerRole)}, ${quoteLiteral(physical2.schema)}, 'USAGE')
4791
- AND pg_catalog.has_table_privilege(${quoteLiteral(executionOwnerRole)}, ${quoteLiteral(physicalTarget)}, 'SELECT')) THEN
4855
+ AND ${hasSelectPrivilege}) THEN
4792
4856
  RAISE EXCEPTION 'Synced backing owner lacks effective USAGE/SELECT; repair managed serving reader access before ORM apply' USING ERRCODE = '42501';
4793
4857
  END IF;
4794
4858
  END`)}`);
4795
4859
  }
4860
+ const whereClause = backing.readGuard ? ` WHERE (SELECT orm_native_sources.require_read_guard(${quoteLiteral(backing.readGuard)})) AND (${policyClause.slice(" WHERE ".length)})` : policyClause;
4796
4861
  statements.push(
4797
- `CREATE OR REPLACE VIEW ${viewName} AS SELECT * FROM ${physicalTarget}${whereClause}`
4862
+ `CREATE OR REPLACE VIEW ${viewName} AS SELECT ${projection} FROM ${physicalTarget}${whereClause}`
4798
4863
  );
4799
4864
  statements.push(`GRANT SELECT ON ${viewName} TO ${role}`);
4800
4865
  }
@@ -5065,10 +5130,142 @@ function snapshotTableNames(snapshot, schema) {
5065
5130
  return Object.values(tables).filter((entry) => entry.schema === schema).map((entry) => entry.name);
5066
5131
  }
5067
5132
 
5133
+ // src/sql/ast.ts
5134
+ function obj(value) {
5135
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("unsupported SQL node");
5136
+ return value;
5137
+ }
5138
+ function list(value) {
5139
+ return value === void 0 ? [] : Array.isArray(value) ? value : fail("expected SQL list");
5140
+ }
5141
+ function str(value) {
5142
+ return typeof value === "string" ? value : fail("expected SQL name");
5143
+ }
5144
+ function fail(message) {
5145
+ throw new Error(message);
5146
+ }
5147
+ function strings(value) {
5148
+ return list(value).map((v) => str(obj(obj(v).String).sval));
5149
+ }
5150
+
5151
+ // src/canonical/view-query.ts
5152
+ var parser;
5153
+ var functions = new Set(VIEW_QUERY_FUNCTION_ALLOWLIST);
5154
+ var scalarTypes = /* @__PURE__ */ new Set([
5155
+ "bool",
5156
+ "boolean",
5157
+ "int2",
5158
+ "int4",
5159
+ "int8",
5160
+ "integer",
5161
+ "bigint",
5162
+ "smallint",
5163
+ "numeric",
5164
+ "decimal",
5165
+ "float4",
5166
+ "float8",
5167
+ "real",
5168
+ "text",
5169
+ "varchar",
5170
+ "bpchar",
5171
+ "uuid",
5172
+ "date",
5173
+ "timestamp",
5174
+ "timestamptz",
5175
+ "time",
5176
+ "timetz",
5177
+ "interval",
5178
+ "json",
5179
+ "jsonb",
5180
+ "bytea"
5181
+ ]);
5182
+ var sqlValues = /* @__PURE__ */ new Set([
5183
+ "SVFOP_CURRENT_DATE",
5184
+ "SVFOP_CURRENT_TIME",
5185
+ "SVFOP_CURRENT_TIME_N",
5186
+ "SVFOP_CURRENT_TIMESTAMP",
5187
+ "SVFOP_CURRENT_TIMESTAMP_N",
5188
+ "SVFOP_LOCALTIME",
5189
+ "SVFOP_LOCALTIME_N",
5190
+ "SVFOP_LOCALTIMESTAMP",
5191
+ "SVFOP_LOCALTIMESTAMP_N",
5192
+ "SVFOP_CURRENT_ROLE",
5193
+ "SVFOP_CURRENT_USER",
5194
+ "SVFOP_USER",
5195
+ "SVFOP_SESSION_USER",
5196
+ "SVFOP_CURRENT_CATALOG",
5197
+ "SVFOP_CURRENT_SCHEMA"
5198
+ ]);
5199
+ async function validateManagedViewQueries({ canonical }) {
5200
+ for (const definition of [canonical, ...canonical.imports ?? []]) {
5201
+ for (const view of definition.views ?? []) {
5202
+ const fail2 = (message) => {
5203
+ throw new OrmDefinitionError(`managed view '${definition.namespace}/${view.view}': ${message}`);
5204
+ };
5205
+ parser ??= new pgParser.PgParser({ version: 17 });
5206
+ const parsed = await parser.parse(view.query);
5207
+ if (parsed.error || !parsed.tree) fail2("invalid SELECT query");
5208
+ const statements = list(obj(parsed.tree).stmts);
5209
+ if (statements.length !== 1 || !obj(obj(statements[0]).stmt).SelectStmt) fail2("one SELECT query is required");
5210
+ validateTree({ tree: parsed.tree, schema: ormSchemaName(definition.namespace), enums: definition.enums.map((entry) => entry.name), fail: fail2 });
5211
+ }
5212
+ }
5213
+ }
5214
+ function pgCatalogName(parts) {
5215
+ if (parts.length === 1) return parts[0];
5216
+ if (parts.length === 2 && parts[0] === "pg_catalog") return parts[1];
5217
+ return void 0;
5218
+ }
5219
+ function reviewedFunctionName({ parts, format }) {
5220
+ return format === "COERCE_SQL_SYNTAX" && parts.length === 2 && parts[0] === "pg_catalog" && parts[1] === "position" ? "strpos" : pgCatalogName(parts);
5221
+ }
5222
+ function validateTree({ tree, ...scope }) {
5223
+ const pending = [tree];
5224
+ while (pending.length) {
5225
+ const value = pending.pop();
5226
+ if (Array.isArray(value)) {
5227
+ pending.push(...value);
5228
+ continue;
5229
+ }
5230
+ if (!value || typeof value !== "object") continue;
5231
+ const node = obj(value);
5232
+ for (const [kind, child] of Object.entries(node)) {
5233
+ validateNode({ kind, child, scope });
5234
+ pending.push(child);
5235
+ }
5236
+ }
5237
+ }
5238
+ function validateNode({ kind, child, scope }) {
5239
+ const { fail: fail2 } = scope;
5240
+ if (kind.endsWith("Stmt") && kind !== "SelectStmt") fail2("only SELECT statements are permitted");
5241
+ if (kind === "FuncCall") {
5242
+ const call = obj(child);
5243
+ const parts = strings(call.funcname);
5244
+ const name2 = reviewedFunctionName({ parts, format: call.funcformat });
5245
+ if (!name2 || !functions.has(name2)) fail2(`only explicitly allowlisted pg_catalog functions are permitted (found '${parts.join(".")}')`);
5246
+ }
5247
+ if (kind === "typeName" || kind === "TypeName") validateType({ node: obj(child), ...scope });
5248
+ if (kind === "A_Expr" && pgCatalogName(strings(obj(child).name)) === void 0) fail2("only pg_catalog operators are permitted");
5249
+ if (kind === "SortBy" && list(obj(child).useOp).length && pgCatalogName(strings(obj(child).useOp)) === void 0) fail2("only pg_catalog sort operators are permitted");
5250
+ if (kind === "SQLValueFunction" && !sqlValues.has(String(obj(child).op))) fail2("unsupported SQL value expression");
5251
+ if (kind === "ParamRef") fail2("view queries cannot contain parameters");
5252
+ if (kind === "A_Indirection" && list(obj(child).indirection).some((entry) => !obj(entry).A_Indices)) {
5253
+ fail2("composite field notation is not permitted in managed views");
5254
+ }
5255
+ }
5256
+ function validateType({ node, schema, enums, fail: fail2 }) {
5257
+ const parts = strings(node.names);
5258
+ const builtin = pgCatalogName(parts);
5259
+ const managedEnum = parts.length === 1 && enums.includes(parts[0]) || parts.length === 2 && parts[0] === schema && enums.includes(parts[1]);
5260
+ if (node.setof || node.pct_type || !managedEnum && (!builtin || !scalarTypes.has(builtin))) {
5261
+ fail2(`casts may only use supported pg_catalog scalar types or declared enums (found '${parts.join(".")}')`);
5262
+ }
5263
+ }
5264
+
5068
5265
  // src/engine/materialized-refresh.ts
5069
5266
  function materializedRefreshFunctions(canonical) {
5070
5267
  const schema = ormSchemaName(canonical.namespace);
5071
- const functions = /* @__PURE__ */ new Map();
5268
+ const functions2 = /* @__PURE__ */ new Map();
5072
5269
  for (const view of canonical.views ?? []) {
5073
5270
  const materialized = view.resolved?.materialized;
5074
5271
  if (!materialized) continue;
@@ -5077,7 +5274,7 @@ function materializedRefreshFunctions(canonical) {
5077
5274
  if (!table || pk.length === 0) {
5078
5275
  throw new OrmDefinitionError(`materialized resolved table '${view.view}' needs its table and primary key`);
5079
5276
  }
5080
- functions.set(view.view, refreshFunctionSql({
5277
+ functions2.set(view.view, refreshFunctionSql({
5081
5278
  schema,
5082
5279
  name: view.view,
5083
5280
  table: materialized.table,
@@ -5086,13 +5283,13 @@ function materializedRefreshFunctions(canonical) {
5086
5283
  pk
5087
5284
  }));
5088
5285
  }
5089
- return functions;
5286
+ return functions2;
5090
5287
  }
5091
5288
  function refreshFunctionSql({ schema, name: name2, table, compute, columns, pk }) {
5092
5289
  const fn = `${quoteIdentifier(schema)}.${quoteIdentifier(materializedRefreshFunctionName(name2))}`;
5093
5290
  const target = `${quoteIdentifier(schema)}.${quoteIdentifier(table)}`;
5094
5291
  const source = `${quoteIdentifier(schema)}.${quoteIdentifier(compute)}`;
5095
- const list = columns.map(quoteIdentifier).join(", ");
5292
+ const list2 = columns.map(quoteIdentifier).join(", ");
5096
5293
  const others = columns.filter((column) => !pk.includes(column));
5097
5294
  const keys = pk.map(quoteIdentifier).join(", ");
5098
5295
  const matches = pk.map((column) => `c.${quoteIdentifier(column)} = m.${quoteIdentifier(column)}`).join(" AND ");
@@ -5118,8 +5315,8 @@ BEGIN
5118
5315
  PERFORM set_config('request.jwt.claims', ${quoteLiteral(MATERIALIZE_CLAIMS_JSON)}, true);
5119
5316
  RETURN QUERY
5120
5317
  WITH up AS (
5121
- INSERT INTO ${target} AS m (${list})
5122
- SELECT ${list} FROM ${source}
5318
+ INSERT INTO ${target} AS m (${list2})
5319
+ SELECT ${list2} FROM ${source}
5123
5320
  ON CONFLICT (${keys}) ${onConflict}
5124
5321
  RETURNING 1
5125
5322
  ), gone AS (
@@ -5730,6 +5927,7 @@ async function planWithViewRecreation({
5730
5927
  previousSnapshot,
5731
5928
  viewRecreation
5732
5929
  }) {
5930
+ await validateManagedViewQueries({ canonical });
5733
5931
  const schema = ormSchemaName(canonical.namespace);
5734
5932
  const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
5735
5933
  assertValidViewDependencies(canonical);
@@ -6428,7 +6626,7 @@ var limitIrSchema = zod.z.object({
6428
6626
  variable: zod.z.string().optional(),
6429
6627
  value: zod.z.number().int().positive().optional(),
6430
6628
  default: zod.z.number().int().positive(),
6431
- max: zod.z.number().int().positive()
6629
+ max: zod.z.number().int().positive().max(MAX_PERSISTED_READ_ROWS)
6432
6630
  });
6433
6631
  var selectionIrSchema = zod.z.lazy(
6434
6632
  () => zod.z.object({
@@ -8104,6 +8302,7 @@ exports.MATERIALIZE_CLAIMS_JSON = MATERIALIZE_CLAIMS_JSON;
8104
8302
  exports.MAX_DEFINITION_FILE_BYTES = MAX_DEFINITION_FILE_BYTES;
8105
8303
  exports.MAX_GENERATED_SDL_BYTES = MAX_GENERATED_SDL_BYTES;
8106
8304
  exports.MAX_IDENTIFIER_LENGTH = MAX_IDENTIFIER_LENGTH;
8305
+ exports.MAX_PERSISTED_READ_ROWS = MAX_PERSISTED_READ_ROWS;
8107
8306
  exports.MAX_TRACKED_COLUMN_NAME = MAX_TRACKED_COLUMN_NAME;
8108
8307
  exports.MIGRATIONS_DIRNAME = MIGRATIONS_DIRNAME;
8109
8308
  exports.NAMESPACE_CONFIG_BRAND = NAMESPACE_CONFIG_BRAND;