@sequenceholdings/orm 2.4.0 → 2.6.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 (58) hide show
  1. package/README.md +23 -0
  2. package/dist/authoring.cjs +53 -7
  3. package/dist/authoring.cjs.map +1 -1
  4. package/dist/authoring.d.cts +10 -6
  5. package/dist/authoring.d.ts +10 -6
  6. package/dist/authoring.js +53 -7
  7. package/dist/authoring.js.map +1 -1
  8. package/dist/canonical.cjs +48 -2
  9. package/dist/canonical.cjs.map +1 -1
  10. package/dist/canonical.d.cts +1 -1
  11. package/dist/canonical.d.ts +1 -1
  12. package/dist/canonical.js +48 -2
  13. package/dist/canonical.js.map +1 -1
  14. package/dist/cli.cjs +260 -31
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.js +260 -31
  17. package/dist/cli.js.map +1 -1
  18. package/dist/client.cjs +11 -6
  19. package/dist/client.cjs.map +1 -1
  20. package/dist/client.d.cts +38 -31
  21. package/dist/client.d.ts +38 -31
  22. package/dist/client.js +11 -7
  23. package/dist/client.js.map +1 -1
  24. package/dist/compiler.cjs +513 -23
  25. package/dist/compiler.cjs.map +1 -1
  26. package/dist/compiler.d.cts +39 -3
  27. package/dist/compiler.d.ts +39 -3
  28. package/dist/compiler.js +510 -24
  29. package/dist/compiler.js.map +1 -1
  30. package/dist/{constants-C6EG6Wq7.d.cts → constants-Dou1RkTo.d.cts} +7 -2
  31. package/dist/{constants-C6EG6Wq7.d.ts → constants-Dou1RkTo.d.ts} +7 -2
  32. package/dist/dynamic-input.d.cts +1 -1
  33. package/dist/dynamic-input.d.ts +1 -1
  34. package/dist/index.cjs +461 -140
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +34 -4
  37. package/dist/index.d.ts +34 -4
  38. package/dist/index.js +460 -141
  39. package/dist/index.js.map +1 -1
  40. package/dist/ir.cjs +55 -4
  41. package/dist/ir.cjs.map +1 -1
  42. package/dist/ir.d.cts +1 -1
  43. package/dist/ir.d.ts +1 -1
  44. package/dist/ir.js +55 -4
  45. package/dist/ir.js.map +1 -1
  46. package/dist/predicate-v2.cjs +55 -4
  47. package/dist/predicate-v2.cjs.map +1 -1
  48. package/dist/predicate-v2.d.cts +1 -1
  49. package/dist/predicate-v2.d.ts +1 -1
  50. package/dist/predicate-v2.js +55 -4
  51. package/dist/predicate-v2.js.map +1 -1
  52. package/dist/sdl.cjs +60 -4
  53. package/dist/sdl.cjs.map +1 -1
  54. package/dist/sdl.d.cts +3 -1
  55. package/dist/sdl.d.ts +3 -1
  56. package/dist/sdl.js +60 -4
  57. package/dist/sdl.js.map +1 -1
  58. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -56,18 +56,22 @@ var init_rehearsal_poll = __esm({
56
56
  });
57
57
 
58
58
  // src/constants.ts
59
- var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
59
+ var NAMESPACE_PATTERN = /^(?!(?:events|native_sources)$)[a-z][a-z0-9_]{0,40}$/;
60
60
  var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
61
61
  var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
62
62
  var MAX_IDENTIFIER_LENGTH = 63;
63
63
  var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
64
+ var MAX_PERSISTED_READ_ROWS = 50001;
64
65
  var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
65
66
  var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]{0,62}\.[a-z][a-z0-9_]{0,62}$/;
66
67
  var SYNCED_SOURCE_PATTERN = QUALIFIED_TABLE_PATTERN;
67
68
  var ORM_SCHEMA_PREFIX = "orm_";
68
69
  var CONSUMER_ROLE_SUFFIX = "_consumer";
69
- var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
70
- var consumerRoleName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}${CONSUMER_ROLE_SUFFIX}`;
70
+ var ormSchemaName = (namespace) => {
71
+ if (!NAMESPACE_PATTERN.test(namespace)) throw new Error(`invalid or reserved ORM namespace '${namespace}'`);
72
+ return `${ORM_SCHEMA_PREFIX}${namespace}`;
73
+ };
74
+ var consumerRoleName = (namespace) => `${ormSchemaName(namespace)}${CONSUMER_ROLE_SUFFIX}`;
71
75
  var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
72
76
  var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
73
77
  var HASH_DOMAIN = "sequence-orm-definitions\0";
@@ -535,6 +539,7 @@ function validateResolvedView({ view, tables, views = [] }) {
535
539
  function validateSource({ scope, source, table }) {
536
540
  const { fail: fail4 } = scope;
537
541
  if (source.kind === "etl" && table.source === void 0) fail4(`etl source '${source.name}' must reference a synced table`);
542
+ if (source.kind === "etl" && table.primaryKey?.length === 0) fail4(`etl source '${source.name}' cannot use an unkeyed synced table for resolved row identity`);
538
543
  if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
539
544
  fail4(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
540
545
  }
@@ -983,7 +988,7 @@ var canonicalTableSchema = z.object({
983
988
  /** Synced/reflected read-only tables (`source: schema.table`) — v1 semantics. */
984
989
  source: z.string().regex(QUALIFIED_TABLE_PATTERN).optional(),
985
990
  columns: z.array(canonicalColumnSchema).default([]),
986
- primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
991
+ primaryKey: z.array(columnIdentifier).max(8).optional(),
987
992
  links: z.array(canonicalLinkSchema).default([]),
988
993
  indexes: z.array(canonicalIndexSchema).default([]),
989
994
  /** Omitted by old definitions, preserving their existing content hashes. */
@@ -1019,6 +1024,29 @@ var canonicalTableSchema = z.object({
1019
1024
  * data plane. Omitted otherwise.
1020
1025
  */
1021
1026
  materializes: identifier2.optional()
1027
+ }).superRefine((table, ctx) => {
1028
+ if (table.primaryKey?.length !== 0) return;
1029
+ const fail4 = (message, path = ["primaryKey"]) => ctx.addIssue({ code: "custom", path, message: `unkeyed table '${table.table}' ${message}` });
1030
+ if (table.source === void 0 || table.columns.length === 0) {
1031
+ fail4("requires a typed synced source");
1032
+ }
1033
+ if ([table.policies.write, table.policies.insert, table.policies.update].some((policy) => policy !== void 0)) {
1034
+ fail4("is read-only and cannot declare write policies", ["policies"]);
1035
+ }
1036
+ const ownedCollections = [table.indexes, table.actions, table.checks, table.foreignKeys, table.cowMutations];
1037
+ const ownedMetadata = [table.cellMetadata, table.internal, table.materializes, table.renamedFrom];
1038
+ if (ownedCollections.some((items) => items !== void 0 && items.length > 0) || ownedMetadata.some((value) => value !== void 0)) {
1039
+ fail4("cannot declare owned storage, constraints or identity operations");
1040
+ }
1041
+ for (const [index3, column] of table.columns.entries()) {
1042
+ const ownedColumnMetadata = [column.default, column.defaultFn, column.onUpdateFn, column.renamedFrom, column.cast, column.cell];
1043
+ if (column.unique || ownedColumnMetadata.some((value) => value !== void 0)) {
1044
+ fail4("cannot declare column uniqueness, mutation defaults or migration metadata", ["columns", index3]);
1045
+ }
1046
+ }
1047
+ for (const [index3, link] of table.links.entries()) {
1048
+ if (link.enforce || link.kind === "toMany") fail4("cannot provide an identity for this link", ["links", index3]);
1049
+ }
1022
1050
  });
1023
1051
  var MAX_VIEW_QUERY = 1e5;
1024
1052
  var MAX_RESOLVED_VIEW_QUERY = 5e6;
@@ -1075,6 +1103,26 @@ var canonicalViewSchema = z.object({
1075
1103
  }
1076
1104
  }
1077
1105
  });
1106
+ function validateUnkeyedLinkIdentities({ definition, ctx }) {
1107
+ const tables = new Map(definition.tables.map((table) => [table.table, table]));
1108
+ const relations = [
1109
+ ...definition.tables.map((table, index3) => ({ kind: "tables", index: index3, links: table.links })),
1110
+ ...(definition.views ?? []).map((view, index3) => ({ kind: "views", index: index3, links: view.links }))
1111
+ ];
1112
+ for (const relation of relations) {
1113
+ for (const [linkIndex, link] of relation.links.entries()) {
1114
+ if (link.kind !== "toOne" || link.namespace !== void 0 && link.namespace !== definition.namespace) continue;
1115
+ const target = link.target === void 0 ? void 0 : tables.get(link.target);
1116
+ if (target?.source !== void 0 && target.primaryKey?.length === 0) {
1117
+ ctx.addIssue({
1118
+ code: "custom",
1119
+ path: [relation.kind, relation.index, "links", linkIndex],
1120
+ message: `link '${link.name}' cannot use unkeyed table '${target.table}' as a to-one identity`
1121
+ });
1122
+ }
1123
+ }
1124
+ }
1125
+ }
1078
1126
  var CELL_PART_TYPES = {
1079
1127
  at: "timestamptz",
1080
1128
  by: "text",
@@ -1138,7 +1186,7 @@ var exportGateSchema = z.union([z.literal(true), z.object({ capability: z.string
1138
1186
  var relationExportSchema = z.object({ read: exportGateSchema, write: exportGateSchema.optional() }).strict();
1139
1187
  var localDefinitionSchema = z.object({
1140
1188
  definition_format: z.literal(CANONICAL_FORMAT),
1141
- namespace: identifier2.max(41),
1189
+ namespace: identifier2.max(41).regex(NAMESPACE_PATTERN, "invalid or reserved ORM namespace"),
1142
1190
  description: z.string().max(2e3).optional(),
1143
1191
  enums: z.array(canonicalEnumSchema).default([]),
1144
1192
  tables: z.array(canonicalTableSchema).min(0).max(500),
@@ -1172,6 +1220,7 @@ var localDefinitionSchema = z.object({
1172
1220
  }
1173
1221
  }
1174
1222
  const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
1223
+ validateUnkeyedLinkIdentities({ definition, ctx });
1175
1224
  const relations = /* @__PURE__ */ new Set([...fkTables.keys(), ...(definition.views ?? []).map((view) => view.view)]);
1176
1225
  for (const name2 of Object.keys(definition.exports ?? {})) {
1177
1226
  if (!relations.has(name2)) ctx.addIssue({ code: "custom", path: ["exports", name2], message: `export '${name2}' is not a local object` });
@@ -1406,6 +1455,9 @@ function normalizeCanonical(definition) {
1406
1455
  return normalized;
1407
1456
  }
1408
1457
 
1458
+ // src/client/sql.ts
1459
+ var BUILTIN_SQL_READ_HASH = "sha256:a50c477c952a5e0fe68ed158a4530e4aab573c09bcbb1a8ac454dc88a39a6258";
1460
+
1409
1461
  // src/ir/index.ts
1410
1462
  var IR_VERSION = 5;
1411
1463
  var LEGACY_IR_VERSION = 1;
@@ -1494,7 +1546,7 @@ var limitIrSchema = z.object({
1494
1546
  variable: z.string().optional(),
1495
1547
  value: z.number().int().positive().optional(),
1496
1548
  default: z.number().int().positive(),
1497
- max: z.number().int().positive()
1549
+ max: z.number().int().positive().max(MAX_PERSISTED_READ_ROWS)
1498
1550
  });
1499
1551
  var selectionIrSchema = z.lazy(
1500
1552
  () => z.object({
@@ -1739,7 +1791,8 @@ var compiledOperationSchema = z.object({
1739
1791
  }).superRefine((operation, ctx) => {
1740
1792
  if (operation.language === "sql") {
1741
1793
  const validPlan = operation.sqlQuery ? !operation.sql && operation.irVersion === RUNTIME_SQL_IR_VERSION : !!operation.sql && operation.irVersion === SQL_IR_VERSION;
1742
- if (!validPlan || operation.kind !== "query" || operation.roots.length !== 0 || !operation.requires?.capability) {
1794
+ const builtin = operation.name === "BuiltinSqlRead" && operation.opHash === BUILTIN_SQL_READ_HASH && operation.requires === null;
1795
+ if (!validPlan || operation.kind !== "query" || operation.roots.length !== 0 || !builtin && !operation.requires?.capability) {
1743
1796
  ctx.addIssue({ code: "custom", message: "SQL operations require a query-only v4 plan or v5 runtime contract, capability, and no GraphQL roots" });
1744
1797
  }
1745
1798
  } else if (operation.sql || operation.sqlQuery || operation.roots.length === 0) {
@@ -3118,6 +3171,7 @@ function buildCompileContext(rawCanonical) {
3118
3171
  const platformMeta = usesPlatformUsers ? platformUserMeta() : void 0;
3119
3172
  if (platformMeta) tables[PLATFORM_USER_TABLE] = platformMeta;
3120
3173
  const syncedTableNames = new Set(canonical.tables.filter((t2) => t2.source !== void 0).map((t2) => t2.table));
3174
+ const unkeyedSyncedTableNames = new Set(relations.filter((table) => table.source !== void 0 && table.primaryKey?.length === 0).map((table) => table.table));
3121
3175
  const viewDependencies = new Map((canonical.views ?? []).map((view) => [view.view, view.dependencies]));
3122
3176
  const claimsGatedMemo = /* @__PURE__ */ new Map();
3123
3177
  const relationIsClaimsGated = (name2) => {
@@ -3131,6 +3185,7 @@ function buildCompileContext(rawCanonical) {
3131
3185
  };
3132
3186
  for (const table of relations) {
3133
3187
  const isView = viewNames.has(table.table);
3188
+ const readOnly = isView || unkeyedSyncedTableNames.has(table.table);
3134
3189
  const owner = owners.get(table.table);
3135
3190
  const entries = resolveColumnEntries(table, enums, diagnostics);
3136
3191
  columnEntries.set(table.table, entries);
@@ -3178,13 +3233,13 @@ function buildCompileContext(rawCanonical) {
3178
3233
  // are claims-gated — see tableMetaIrSchema.claimsGated.
3179
3234
  claimsGated: relationIsClaimsGated(table.table),
3180
3235
  columns: entries.map(({ deprecated: _deprecated, onUpdateFn: _onUpdateFn, ...column }) => column),
3181
- primaryKey: isView ? primaryKey2 : primaryKey2.length > 0 ? primaryKey2 : ["id"],
3236
+ primaryKey: isView || unkeyedSyncedTableNames.has(table.table) ? primaryKey2 : primaryKey2.length > 0 ? primaryKey2 : ["id"],
3182
3237
  links: [],
3183
3238
  filterable: data.map((c) => c.name),
3184
3239
  orderable: data.filter((c) => c.type !== "jsonb").map((c) => c.name),
3185
- insertable: resolved ? resolvedWritable(data) : isView ? [] : active.map((c) => c.name),
3186
- settable: resolved ? resolvedWritable(data).filter((name2) => !primaryKey2.includes(name2)) : isView ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
3187
- appendable: isView ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
3240
+ insertable: resolved ? resolvedWritable(data) : readOnly ? [] : active.map((c) => c.name),
3241
+ settable: resolved ? resolvedWritable(data).filter((name2) => !primaryKey2.includes(name2)) : readOnly ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
3242
+ appendable: readOnly ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
3188
3243
  ...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
3189
3244
  ...cells ? { cells } : {},
3190
3245
  ...resolved ? { resolved: resolvedMeta({ resolved, pk: primaryKey2[0] }) } : {},
@@ -3208,6 +3263,16 @@ function buildCompileContext(rawCanonical) {
3208
3263
  });
3209
3264
  continue;
3210
3265
  }
3266
+ const identityTable = link.kind === "toOne" ? targetName : link.kind === "toMany" ? table.table : void 0;
3267
+ if (identityTable !== void 0 && unkeyedSyncedTableNames.has(identityTable)) {
3268
+ diagnostics.push({
3269
+ severity: "error",
3270
+ code: "unkeyed-link-identity",
3271
+ table: table.table,
3272
+ message: `link '${link.name}' cannot use unkeyed table '${identityTable}' as a row identity`
3273
+ });
3274
+ continue;
3275
+ }
3211
3276
  const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false, onUpdateFn: false })) : columnEntries.get(targetName);
3212
3277
  const onEntries = link.kind === "toMany" ? targetEntries : entries;
3213
3278
  const referencesEntries = link.kind === "toMany" ? entries : targetEntries;
@@ -3273,7 +3338,7 @@ function buildCompileContext(rawCanonical) {
3273
3338
  compilePolicyFor("write", writePolicies);
3274
3339
  compilePolicyFor("insert", insertPolicies);
3275
3340
  compilePolicyFor("update", updatePolicies);
3276
- if (isView && table.resolved === void 0) {
3341
+ if (isView && table.resolved === void 0 || unkeyedSyncedTableNames.has(table.table)) {
3277
3342
  writePolicies.set(table.table, CONST_FALSE);
3278
3343
  insertPolicies.set(table.table, CONST_FALSE);
3279
3344
  updatePolicies.set(table.table, CONST_FALSE);
@@ -3348,7 +3413,7 @@ function buildCompileContext(rawCanonical) {
3348
3413
  const queryRoots = /* @__PURE__ */ new Map();
3349
3414
  const mutationRoots = /* @__PURE__ */ new Map();
3350
3415
  for (const table of relations) {
3351
- if (table.internal === true) continue;
3416
+ if (table.internal === true || unkeyedSyncedTableNames.has(table.table)) continue;
3352
3417
  const roots = queryRootNames(table.table);
3353
3418
  queryRoots.set(roots.select, { kind: "select", table: table.table });
3354
3419
  if ((tables[table.table]?.primaryKey.length ?? 0) > 0) {
@@ -3728,8 +3793,10 @@ var OperationLowerer = class {
3728
3793
  constructor(ctx, restricted) {
3729
3794
  this.ctx = ctx;
3730
3795
  this.variables = new Map(restricted.variables.map((v) => [v.name, v]));
3796
+ this.maxRows = restricted.readBudget ?? MAX_LIMIT;
3731
3797
  }
3732
3798
  variables;
3799
+ maxRows;
3733
3800
  meta(table) {
3734
3801
  return this.ctx.tables[table];
3735
3802
  }
@@ -3933,13 +4000,13 @@ var OperationLowerer = class {
3933
4000
  });
3934
4001
  }
3935
4002
  lowerLimit(node) {
3936
- const base2 = { default: DEFAULT_LIMIT, max: MAX_LIMIT };
4003
+ const base2 = { default: DEFAULT_LIMIT, max: this.maxRows };
3937
4004
  if (!node) return base2;
3938
4005
  if (node.kind === Kind.VARIABLE) return { ...base2, variable: node.name.value };
3939
4006
  if (node.kind === Kind.INT) {
3940
4007
  const value = Number(node.value);
3941
4008
  if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
3942
- return { ...base2, value: Math.min(value, MAX_LIMIT) };
4009
+ return { ...base2, value: Math.min(value, this.maxRows) };
3943
4010
  }
3944
4011
  fail2("limit must be an integer literal or a variable", "invalid-limit");
3945
4012
  }
@@ -4738,7 +4805,13 @@ var OperationLowerer = class {
4738
4805
  };
4739
4806
  function lowerOperation({ restricted, ctx }) {
4740
4807
  const lowerer = new OperationLowerer(ctx, restricted);
4741
- return lowerer.lowerRoots(restricted.definition, restricted.kind);
4808
+ const roots = lowerer.lowerRoots(restricted.definition, restricted.kind);
4809
+ if (restricted.readBudget === void 0) return roots;
4810
+ const root = roots[0];
4811
+ if (restricted.kind !== "query" || restricted.definition.selectionSet.selections.length !== 1 || roots.length !== 1 || root?.kind !== "select" || root.selection.relations.length > 0 || root.selection.aggregates.length > 0 || root.selection.cells !== void 0 || root.prefetch?.length) {
4812
+ fail2("@readBudget requires one flat select root without related selections, aggregates, cells, or prefetch", "invalid-read-budget");
4813
+ }
4814
+ return roots;
4742
4815
  }
4743
4816
  function isCellColumn(meta, name2) {
4744
4817
  if (!meta.cells) return false;
@@ -4797,6 +4870,19 @@ function parseDocuments({
4797
4870
  }
4798
4871
  return { documents: parsed, diagnostics };
4799
4872
  }
4873
+ function parseReadBudget(operation) {
4874
+ const directives = operation.directives?.filter((directive) => directive.name.value === "readBudget") ?? [];
4875
+ if (directives.length === 0) return void 0;
4876
+ const value = directives[0]?.arguments?.find((argument) => argument.name.value === "maxRows")?.value;
4877
+ if (operation.operation !== "query" || directives.length !== 1 || value?.kind !== Kind.INT) {
4878
+ throw new DocumentCompileError("@readBudget requires a literal maxRows on a query", "invalid-read-budget");
4879
+ }
4880
+ const maxRows = Number(value.value);
4881
+ if (!Number.isSafeInteger(maxRows) || maxRows < 1 || maxRows > MAX_PERSISTED_READ_ROWS) {
4882
+ throw new DocumentCompileError(`@readBudget(maxRows:) must be between 1 and ${MAX_PERSISTED_READ_ROWS}`, "invalid-read-budget");
4883
+ }
4884
+ return maxRows;
4885
+ }
4800
4886
  function unwrapType(node, variable) {
4801
4887
  let nullable = true;
4802
4888
  let list2 = false;
@@ -4957,6 +5043,7 @@ function restrictOperation({
4957
5043
  name: name2,
4958
5044
  kind: operation.operation === "mutation" ? "mutation" : "query",
4959
5045
  requires: parseRequires(operation),
5046
+ readBudget: parseReadBudget(operation),
4960
5047
  variables,
4961
5048
  definition: operation
4962
5049
  };
@@ -5087,9 +5174,9 @@ function compilePredIrToSql(pred, options = {}) {
5087
5174
  case "cmp":
5088
5175
  return compileCmp(pred.op, pred.column, pred.value, options);
5089
5176
  case "capability":
5090
- return `(${capabilityExpression(pred.key, pred.namespace ?? options.namespace)})`;
5177
+ return `(${options.statementClaimChecks ? "SELECT " : ""}${capabilityExpression(pred.key, pred.namespace ?? options.namespace)})`;
5091
5178
  case "role":
5092
- return `(${roleExpression(pred.key, pred.namespace ?? options.namespace)})`;
5179
+ return `(${options.statementClaimChecks ? "SELECT " : ""}${roleExpression(pred.key, pred.namespace ?? options.namespace)})`;
5093
5180
  case "exists":
5094
5181
  if (options.exists) return options.exists(pred);
5095
5182
  throw new OrmDefinitionError("synced view read policy cannot use 'exists' without a relationship resolver");
@@ -5197,7 +5284,8 @@ function createSqlCatalog({ canonical, publishedObjects, executionTarget }) {
5197
5284
  const { ctx, diagnostics } = buildCompileContext(canonical);
5198
5285
  if (diagnostics.some((d) => d.severity === "error")) throw new Error(diagnostics.map((d) => d.message).join("; "));
5199
5286
  const identity = sqlHash(`ontology-sql-v1\0${computeSchemaHash(canonical)}\0${executionTarget}`);
5200
- const refOf = (meta) => `${meta.namespace ?? canonical.namespace}/${meta.table}`;
5287
+ const logicalNames = new Map(Object.entries(ctx.tables).map(([key, meta]) => [meta, key]));
5288
+ const refOf = (meta) => `${meta.namespace ?? canonical.namespace}/${logicalNames.get(meta)}`;
5201
5289
  const definitions = [canonical, ...canonical.imports ?? []];
5202
5290
  const internal = new Set(definitions.flatMap((owner) => [
5203
5291
  ...owner.tables.filter((t2) => t2.internal).map((t2) => `${owner.namespace}/${t2.table}`),
@@ -5273,15 +5361,22 @@ function only(node, keys) {
5273
5361
  var parser;
5274
5362
  var SQL_TYPES = ["text", "boolean", "integer", "bigint", "numeric", "double", "uuid", "date", "timestamptz"];
5275
5363
  var pgType = (type) => type === "double" ? "double precision" : SQL_TYPES.some((t2) => t2 === type) ? type : fail3("unsupported SQL type");
5276
- async function compileOntologySql({ catalog, sql: sql4, parameterTypes = [], maxRows = 1e3 }) {
5277
- if (!Number.isInteger(maxRows) || maxRows < 1 || maxRows > 1e4 || Buffer.byteLength(sql4) > 65536) fail3("SQL limit exceeded");
5364
+ async function parseSelect(sql4) {
5365
+ if (Buffer.byteLength(sql4) > 65536) fail3("SQL limit exceeded");
5278
5366
  parser ??= new PgParser({ version: 17 });
5279
5367
  const parsed = await parser.parse(sql4);
5280
5368
  if (parsed.error || !parsed.tree) fail3("invalid SQL");
5281
5369
  const statements = list(obj(parsed.tree).stmts);
5282
5370
  if (statements.length !== 1) fail3("one SQL SELECT is required");
5371
+ const statement = obj(obj(statements[0]).stmt);
5372
+ if (!statement.SelectStmt) fail3("one SQL SELECT is required");
5373
+ return statement;
5374
+ }
5375
+ async function compileOntologySql({ catalog, sql: sql4, parameterTypes = [], maxRows = 1e3 }) {
5376
+ if (!Number.isInteger(maxRows) || maxRows < 1 || maxRows > 1e4 || Buffer.byteLength(sql4) > 65536) fail3("SQL limit exceeded");
5377
+ const statement = await parseSelect(sql4);
5283
5378
  const emitter = new SqlEmitter({ catalog, parameterTypes });
5284
- const result = emitter.select(obj(obj(statements[0]).stmt), { relations: [], ctes: /* @__PURE__ */ new Map() });
5379
+ const result = emitter.select(statement, { relations: [], ctes: /* @__PURE__ */ new Map() });
5285
5380
  if (emitter.used.size !== parameterTypes.length) fail3("SQL parameters must be used exactly as declared");
5286
5381
  if (!emitter.read.size) fail3("SQL operation must read an ontology object");
5287
5382
  return {
@@ -5614,6 +5709,118 @@ function bindNamedParameters({ sql: sql4, names }) {
5614
5709
  }
5615
5710
  return result;
5616
5711
  }
5712
+ var parser2;
5713
+ var functions = new Set(VIEW_QUERY_FUNCTION_ALLOWLIST);
5714
+ var scalarTypes = /* @__PURE__ */ new Set([
5715
+ "bool",
5716
+ "boolean",
5717
+ "int2",
5718
+ "int4",
5719
+ "int8",
5720
+ "integer",
5721
+ "bigint",
5722
+ "smallint",
5723
+ "numeric",
5724
+ "decimal",
5725
+ "float4",
5726
+ "float8",
5727
+ "real",
5728
+ "text",
5729
+ "varchar",
5730
+ "bpchar",
5731
+ "uuid",
5732
+ "date",
5733
+ "timestamp",
5734
+ "timestamptz",
5735
+ "time",
5736
+ "timetz",
5737
+ "interval",
5738
+ "json",
5739
+ "jsonb",
5740
+ "bytea"
5741
+ ]);
5742
+ var sqlValues = /* @__PURE__ */ new Set([
5743
+ "SVFOP_CURRENT_DATE",
5744
+ "SVFOP_CURRENT_TIME",
5745
+ "SVFOP_CURRENT_TIME_N",
5746
+ "SVFOP_CURRENT_TIMESTAMP",
5747
+ "SVFOP_CURRENT_TIMESTAMP_N",
5748
+ "SVFOP_LOCALTIME",
5749
+ "SVFOP_LOCALTIME_N",
5750
+ "SVFOP_LOCALTIMESTAMP",
5751
+ "SVFOP_LOCALTIMESTAMP_N",
5752
+ "SVFOP_CURRENT_ROLE",
5753
+ "SVFOP_CURRENT_USER",
5754
+ "SVFOP_USER",
5755
+ "SVFOP_SESSION_USER",
5756
+ "SVFOP_CURRENT_CATALOG",
5757
+ "SVFOP_CURRENT_SCHEMA"
5758
+ ]);
5759
+ async function validateManagedViewQueries({ canonical }) {
5760
+ for (const definition of [canonical, ...canonical.imports ?? []]) {
5761
+ for (const view of definition.views ?? []) {
5762
+ const fail4 = (message) => {
5763
+ throw new OrmDefinitionError(`managed view '${definition.namespace}/${view.view}': ${message}`);
5764
+ };
5765
+ parser2 ??= new PgParser({ version: 17 });
5766
+ const parsed = await parser2.parse(view.query);
5767
+ if (parsed.error || !parsed.tree) fail4("invalid SELECT query");
5768
+ const statements = list(obj(parsed.tree).stmts);
5769
+ if (statements.length !== 1 || !obj(obj(statements[0]).stmt).SelectStmt) fail4("one SELECT query is required");
5770
+ validateTree({ tree: parsed.tree, schema: ormSchemaName(definition.namespace), enums: definition.enums.map((entry) => entry.name), fail: fail4 });
5771
+ }
5772
+ }
5773
+ }
5774
+ function pgCatalogName(parts) {
5775
+ if (parts.length === 1) return parts[0];
5776
+ if (parts.length === 2 && parts[0] === "pg_catalog") return parts[1];
5777
+ return void 0;
5778
+ }
5779
+ function reviewedFunctionName({ parts, format }) {
5780
+ return format === "COERCE_SQL_SYNTAX" && parts.length === 2 && parts[0] === "pg_catalog" && parts[1] === "position" ? "strpos" : pgCatalogName(parts);
5781
+ }
5782
+ function validateTree({ tree, ...scope }) {
5783
+ const pending = [tree];
5784
+ while (pending.length) {
5785
+ const value = pending.pop();
5786
+ if (Array.isArray(value)) {
5787
+ pending.push(...value);
5788
+ continue;
5789
+ }
5790
+ if (!value || typeof value !== "object") continue;
5791
+ const node = obj(value);
5792
+ for (const [kind, child] of Object.entries(node)) {
5793
+ validateNode({ kind, child, scope });
5794
+ pending.push(child);
5795
+ }
5796
+ }
5797
+ }
5798
+ function validateNode({ kind, child, scope }) {
5799
+ const { fail: fail4 } = scope;
5800
+ if (kind.endsWith("Stmt") && kind !== "SelectStmt") fail4("only SELECT statements are permitted");
5801
+ if (kind === "FuncCall") {
5802
+ const call = obj(child);
5803
+ const parts = strings(call.funcname);
5804
+ const name2 = reviewedFunctionName({ parts, format: call.funcformat });
5805
+ if (!name2 || !functions.has(name2)) fail4(`only explicitly allowlisted pg_catalog functions are permitted (found '${parts.join(".")}')`);
5806
+ }
5807
+ if (kind === "typeName" || kind === "TypeName") validateType({ node: obj(child), ...scope });
5808
+ if (kind === "A_Expr" && pgCatalogName(strings(obj(child).name)) === void 0) fail4("only pg_catalog operators are permitted");
5809
+ if (kind === "SortBy" && list(obj(child).useOp).length && pgCatalogName(strings(obj(child).useOp)) === void 0) fail4("only pg_catalog sort operators are permitted");
5810
+ if (kind === "SQLValueFunction" && !sqlValues.has(String(obj(child).op))) fail4("unsupported SQL value expression");
5811
+ if (kind === "ParamRef") fail4("view queries cannot contain parameters");
5812
+ if (kind === "A_Indirection" && list(obj(child).indirection).some((entry) => !obj(entry).A_Indices)) {
5813
+ fail4("composite field notation is not permitted in managed views");
5814
+ }
5815
+ }
5816
+ function validateType({ node, schema, enums, fail: fail4 }) {
5817
+ const parts = strings(node.names);
5818
+ const builtin = pgCatalogName(parts);
5819
+ const managedEnum = parts.length === 1 && enums.includes(parts[0]) || parts.length === 2 && parts[0] === schema && enums.includes(parts[1]);
5820
+ if (node.setof || node.pct_type || !managedEnum && (!builtin || !scalarTypes.has(builtin))) {
5821
+ fail4(`casts may only use supported pg_catalog scalar types or declared enums (found '${parts.join(".")}')`);
5822
+ }
5823
+ }
5617
5824
 
5618
5825
  // src/sql/operations.ts
5619
5826
  function isSqlOperationDocument(path, contents) {
@@ -5629,7 +5836,13 @@ function isSqlOperationDocument(path, contents) {
5629
5836
  async function compileOperationDocuments(input) {
5630
5837
  const sqlDocuments = input.documents.filter((d) => isSqlOperationDocument(d.path, d.contents));
5631
5838
  const result = compileNamespace({ ...input, documents: input.documents.filter((d) => !isSqlOperationDocument(d.path, d.contents)) });
5632
- if (!result.ok || !sqlDocuments.length) return result;
5839
+ if (!result.ok) return result;
5840
+ const viewDiagnostics = await managedViewDiagnostics(input.canonical);
5841
+ if (viewDiagnostics.length) return { ok: false, diagnostics: [...result.diagnostics, ...viewDiagnostics] };
5842
+ if (!sqlDocuments.length) return result;
5843
+ return compileSqlDocuments({ input, result, sqlDocuments });
5844
+ }
5845
+ async function compileSqlDocuments({ input, result, sqlDocuments }) {
5633
5846
  for (const document of sqlDocuments) {
5634
5847
  try {
5635
5848
  const definition = defineOperation(JSON.parse(document.contents));
@@ -5723,6 +5936,18 @@ async function compileOperationDocuments(input) {
5723
5936
  result.manifest.opsHash = result.artifact.opsHash;
5724
5937
  return { ...result, artifact: normalizeIr(result.artifact) };
5725
5938
  }
5939
+ async function managedViewDiagnostics(canonical) {
5940
+ try {
5941
+ await validateManagedViewQueries({ canonical });
5942
+ return [];
5943
+ } catch (error) {
5944
+ return [{
5945
+ severity: "error",
5946
+ code: "invalid-view-query",
5947
+ message: error instanceof Error ? error.message : "invalid managed view query"
5948
+ }];
5949
+ }
5950
+ }
5726
5951
 
5727
5952
  // src/sql/codegen.ts
5728
5953
  var typeOf = (type) => ["integer", "double"].includes(type) ? "number" : type === "boolean" ? "boolean" : "string";
@@ -7570,19 +7795,19 @@ function compileComparison(op, left, right, columnTypes) {
7570
7795
  }
7571
7796
  return `${compileOperand(left, right, columnTypes)} ${SQL_COMPARE_OPS[op]} ${compileOperand(right, left, columnTypes)}`;
7572
7797
  }
7573
- function compilePredicate(predicate, columnTypes) {
7798
+ function compilePredicate(predicate, columnTypes, options = {}) {
7574
7799
  switch (predicate.kind) {
7575
7800
  case "and":
7576
7801
  case "or": {
7577
- const joined = predicate.operands.map((operand) => compilePredicate(operand, columnTypes)).join(` ${predicate.kind.toUpperCase()} `);
7802
+ const joined = predicate.operands.map((operand) => compilePredicate(operand, columnTypes, options)).join(` ${predicate.kind.toUpperCase()} `);
7578
7803
  return `(${joined})`;
7579
7804
  }
7580
7805
  case "not":
7581
- return `(NOT ${compilePredicate(predicate.operand, columnTypes)})`;
7806
+ return `(NOT ${compilePredicate(predicate.operand, columnTypes, options)})`;
7582
7807
  case "compare":
7583
7808
  return `(${compileComparison(predicate.op, predicate.left, predicate.right, columnTypes)})`;
7584
7809
  case "capability":
7585
- return `(${capabilityExpression(predicate.key)})`;
7810
+ return `(${options.statementClaimChecks ? "SELECT " : ""}${capabilityExpression(predicate.key)})`;
7586
7811
  }
7587
7812
  }
7588
7813
 
@@ -7818,7 +8043,7 @@ function snapshotTableNames(snapshot, schema) {
7818
8043
  // src/engine/materialized-refresh.ts
7819
8044
  function materializedRefreshFunctions(canonical) {
7820
8045
  const schema = ormSchemaName(canonical.namespace);
7821
- const functions = /* @__PURE__ */ new Map();
8046
+ const functions2 = /* @__PURE__ */ new Map();
7822
8047
  for (const view of canonical.views ?? []) {
7823
8048
  const materialized = view.resolved?.materialized;
7824
8049
  if (!materialized) continue;
@@ -7827,7 +8052,7 @@ function materializedRefreshFunctions(canonical) {
7827
8052
  if (!table || pk.length === 0) {
7828
8053
  throw new OrmDefinitionError(`materialized resolved table '${view.view}' needs its table and primary key`);
7829
8054
  }
7830
- functions.set(view.view, refreshFunctionSql({
8055
+ functions2.set(view.view, refreshFunctionSql({
7831
8056
  schema,
7832
8057
  name: view.view,
7833
8058
  table: materialized.table,
@@ -7836,7 +8061,7 @@ function materializedRefreshFunctions(canonical) {
7836
8061
  pk
7837
8062
  }));
7838
8063
  }
7839
- return functions;
8064
+ return functions2;
7840
8065
  }
7841
8066
  function refreshFunctionSql({ schema, name: name2, table, compute, columns, pk }) {
7842
8067
  const fn = `${quoteIdentifier(schema)}.${quoteIdentifier(materializedRefreshFunctionName(name2))}`;
@@ -8480,6 +8705,7 @@ async function planWithViewRecreation({
8480
8705
  previousSnapshot,
8481
8706
  viewRecreation
8482
8707
  }) {
8708
+ await validateManagedViewQueries({ canonical });
8483
8709
  const schema = ormSchemaName(canonical.namespace);
8484
8710
  const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
8485
8711
  assertValidViewDependencies(canonical);
@@ -9770,6 +9996,9 @@ function generateSdl(canonical, options = {}) {
9770
9996
  blocks.push(
9771
9997
  '"""Gates an operation behind a platform capability or custom role \u2014 checked by the data plane before execution."""\ndirective @requires(capability: String, role: String) on QUERY | MUTATION'
9772
9998
  );
9999
+ if (!options.omitReadBudgetDirective) blocks.push(
10000
+ '"""Sets a document-authored ceiling of 1\u201350001 rows for one flat select root. Runtime variables cannot raise it."""\ndirective @readBudget(maxRows: Int!) on QUERY'
10001
+ );
9773
10002
  blocks.push("directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT");
9774
10003
  blocks.push(
9775
10004
  '"""Abort the entire mutation unless this root affects exactly the document-authored number of rows. Mutation roots only; cannot be conditional."""\ndirective @assertAffectedRows(exactly: Int!) on FIELD'
@@ -11974,7 +12203,7 @@ function emitSyncedTable(table) {
11974
12203
  lines.push(" },", " {");
11975
12204
  lines.push(` source: ${quote(table.source ?? "")},`);
11976
12205
  if (table.description !== void 0) lines.push(` description: ${quote(table.description)},`);
11977
- if (singlePrimaryKey === void 0 && table.primaryKey && table.primaryKey.length > 0) {
12206
+ if (singlePrimaryKey === void 0 && table.primaryKey !== void 0) {
11978
12207
  lines.push(` primaryKey: ${jsonLiteral(table.primaryKey, 4)},`);
11979
12208
  }
11980
12209
  if (table.links.length > 0) {