@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/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` });
@@ -1494,7 +1543,7 @@ var limitIrSchema = z.object({
1494
1543
  variable: z.string().optional(),
1495
1544
  value: z.number().int().positive().optional(),
1496
1545
  default: z.number().int().positive(),
1497
- max: z.number().int().positive()
1546
+ max: z.number().int().positive().max(MAX_PERSISTED_READ_ROWS)
1498
1547
  });
1499
1548
  var selectionIrSchema = z.lazy(
1500
1549
  () => z.object({
@@ -3118,6 +3167,7 @@ function buildCompileContext(rawCanonical) {
3118
3167
  const platformMeta = usesPlatformUsers ? platformUserMeta() : void 0;
3119
3168
  if (platformMeta) tables[PLATFORM_USER_TABLE] = platformMeta;
3120
3169
  const syncedTableNames = new Set(canonical.tables.filter((t2) => t2.source !== void 0).map((t2) => t2.table));
3170
+ const unkeyedSyncedTableNames = new Set(relations.filter((table) => table.source !== void 0 && table.primaryKey?.length === 0).map((table) => table.table));
3121
3171
  const viewDependencies = new Map((canonical.views ?? []).map((view) => [view.view, view.dependencies]));
3122
3172
  const claimsGatedMemo = /* @__PURE__ */ new Map();
3123
3173
  const relationIsClaimsGated = (name2) => {
@@ -3131,6 +3181,7 @@ function buildCompileContext(rawCanonical) {
3131
3181
  };
3132
3182
  for (const table of relations) {
3133
3183
  const isView = viewNames.has(table.table);
3184
+ const readOnly = isView || unkeyedSyncedTableNames.has(table.table);
3134
3185
  const owner = owners.get(table.table);
3135
3186
  const entries = resolveColumnEntries(table, enums, diagnostics);
3136
3187
  columnEntries.set(table.table, entries);
@@ -3178,13 +3229,13 @@ function buildCompileContext(rawCanonical) {
3178
3229
  // are claims-gated — see tableMetaIrSchema.claimsGated.
3179
3230
  claimsGated: relationIsClaimsGated(table.table),
3180
3231
  columns: entries.map(({ deprecated: _deprecated, onUpdateFn: _onUpdateFn, ...column }) => column),
3181
- primaryKey: isView ? primaryKey2 : primaryKey2.length > 0 ? primaryKey2 : ["id"],
3232
+ primaryKey: isView || unkeyedSyncedTableNames.has(table.table) ? primaryKey2 : primaryKey2.length > 0 ? primaryKey2 : ["id"],
3182
3233
  links: [],
3183
3234
  filterable: data.map((c) => c.name),
3184
3235
  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),
3236
+ insertable: resolved ? resolvedWritable(data) : readOnly ? [] : active.map((c) => c.name),
3237
+ settable: resolved ? resolvedWritable(data).filter((name2) => !primaryKey2.includes(name2)) : readOnly ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
3238
+ appendable: readOnly ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
3188
3239
  ...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
3189
3240
  ...cells ? { cells } : {},
3190
3241
  ...resolved ? { resolved: resolvedMeta({ resolved, pk: primaryKey2[0] }) } : {},
@@ -3208,6 +3259,16 @@ function buildCompileContext(rawCanonical) {
3208
3259
  });
3209
3260
  continue;
3210
3261
  }
3262
+ const identityTable = link.kind === "toOne" ? targetName : link.kind === "toMany" ? table.table : void 0;
3263
+ if (identityTable !== void 0 && unkeyedSyncedTableNames.has(identityTable)) {
3264
+ diagnostics.push({
3265
+ severity: "error",
3266
+ code: "unkeyed-link-identity",
3267
+ table: table.table,
3268
+ message: `link '${link.name}' cannot use unkeyed table '${identityTable}' as a row identity`
3269
+ });
3270
+ continue;
3271
+ }
3211
3272
  const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false, onUpdateFn: false })) : columnEntries.get(targetName);
3212
3273
  const onEntries = link.kind === "toMany" ? targetEntries : entries;
3213
3274
  const referencesEntries = link.kind === "toMany" ? entries : targetEntries;
@@ -3273,7 +3334,7 @@ function buildCompileContext(rawCanonical) {
3273
3334
  compilePolicyFor("write", writePolicies);
3274
3335
  compilePolicyFor("insert", insertPolicies);
3275
3336
  compilePolicyFor("update", updatePolicies);
3276
- if (isView && table.resolved === void 0) {
3337
+ if (isView && table.resolved === void 0 || unkeyedSyncedTableNames.has(table.table)) {
3277
3338
  writePolicies.set(table.table, CONST_FALSE);
3278
3339
  insertPolicies.set(table.table, CONST_FALSE);
3279
3340
  updatePolicies.set(table.table, CONST_FALSE);
@@ -3348,7 +3409,7 @@ function buildCompileContext(rawCanonical) {
3348
3409
  const queryRoots = /* @__PURE__ */ new Map();
3349
3410
  const mutationRoots = /* @__PURE__ */ new Map();
3350
3411
  for (const table of relations) {
3351
- if (table.internal === true) continue;
3412
+ if (table.internal === true || unkeyedSyncedTableNames.has(table.table)) continue;
3352
3413
  const roots = queryRootNames(table.table);
3353
3414
  queryRoots.set(roots.select, { kind: "select", table: table.table });
3354
3415
  if ((tables[table.table]?.primaryKey.length ?? 0) > 0) {
@@ -3728,8 +3789,10 @@ var OperationLowerer = class {
3728
3789
  constructor(ctx, restricted) {
3729
3790
  this.ctx = ctx;
3730
3791
  this.variables = new Map(restricted.variables.map((v) => [v.name, v]));
3792
+ this.maxRows = restricted.readBudget ?? MAX_LIMIT;
3731
3793
  }
3732
3794
  variables;
3795
+ maxRows;
3733
3796
  meta(table) {
3734
3797
  return this.ctx.tables[table];
3735
3798
  }
@@ -3933,13 +3996,13 @@ var OperationLowerer = class {
3933
3996
  });
3934
3997
  }
3935
3998
  lowerLimit(node) {
3936
- const base2 = { default: DEFAULT_LIMIT, max: MAX_LIMIT };
3999
+ const base2 = { default: DEFAULT_LIMIT, max: this.maxRows };
3937
4000
  if (!node) return base2;
3938
4001
  if (node.kind === Kind.VARIABLE) return { ...base2, variable: node.name.value };
3939
4002
  if (node.kind === Kind.INT) {
3940
4003
  const value = Number(node.value);
3941
4004
  if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
3942
- return { ...base2, value: Math.min(value, MAX_LIMIT) };
4005
+ return { ...base2, value: Math.min(value, this.maxRows) };
3943
4006
  }
3944
4007
  fail2("limit must be an integer literal or a variable", "invalid-limit");
3945
4008
  }
@@ -4738,7 +4801,13 @@ var OperationLowerer = class {
4738
4801
  };
4739
4802
  function lowerOperation({ restricted, ctx }) {
4740
4803
  const lowerer = new OperationLowerer(ctx, restricted);
4741
- return lowerer.lowerRoots(restricted.definition, restricted.kind);
4804
+ const roots = lowerer.lowerRoots(restricted.definition, restricted.kind);
4805
+ if (restricted.readBudget === void 0) return roots;
4806
+ const root = roots[0];
4807
+ 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) {
4808
+ fail2("@readBudget requires one flat select root without related selections, aggregates, cells, or prefetch", "invalid-read-budget");
4809
+ }
4810
+ return roots;
4742
4811
  }
4743
4812
  function isCellColumn(meta, name2) {
4744
4813
  if (!meta.cells) return false;
@@ -4797,6 +4866,19 @@ function parseDocuments({
4797
4866
  }
4798
4867
  return { documents: parsed, diagnostics };
4799
4868
  }
4869
+ function parseReadBudget(operation) {
4870
+ const directives = operation.directives?.filter((directive) => directive.name.value === "readBudget") ?? [];
4871
+ if (directives.length === 0) return void 0;
4872
+ const value = directives[0]?.arguments?.find((argument) => argument.name.value === "maxRows")?.value;
4873
+ if (operation.operation !== "query" || directives.length !== 1 || value?.kind !== Kind.INT) {
4874
+ throw new DocumentCompileError("@readBudget requires a literal maxRows on a query", "invalid-read-budget");
4875
+ }
4876
+ const maxRows = Number(value.value);
4877
+ if (!Number.isSafeInteger(maxRows) || maxRows < 1 || maxRows > MAX_PERSISTED_READ_ROWS) {
4878
+ throw new DocumentCompileError(`@readBudget(maxRows:) must be between 1 and ${MAX_PERSISTED_READ_ROWS}`, "invalid-read-budget");
4879
+ }
4880
+ return maxRows;
4881
+ }
4800
4882
  function unwrapType(node, variable) {
4801
4883
  let nullable = true;
4802
4884
  let list2 = false;
@@ -4957,6 +5039,7 @@ function restrictOperation({
4957
5039
  name: name2,
4958
5040
  kind: operation.operation === "mutation" ? "mutation" : "query",
4959
5041
  requires: parseRequires(operation),
5042
+ readBudget: parseReadBudget(operation),
4960
5043
  variables,
4961
5044
  definition: operation
4962
5045
  };
@@ -5614,6 +5697,118 @@ function bindNamedParameters({ sql: sql4, names }) {
5614
5697
  }
5615
5698
  return result;
5616
5699
  }
5700
+ var parser2;
5701
+ var functions = new Set(VIEW_QUERY_FUNCTION_ALLOWLIST);
5702
+ var scalarTypes = /* @__PURE__ */ new Set([
5703
+ "bool",
5704
+ "boolean",
5705
+ "int2",
5706
+ "int4",
5707
+ "int8",
5708
+ "integer",
5709
+ "bigint",
5710
+ "smallint",
5711
+ "numeric",
5712
+ "decimal",
5713
+ "float4",
5714
+ "float8",
5715
+ "real",
5716
+ "text",
5717
+ "varchar",
5718
+ "bpchar",
5719
+ "uuid",
5720
+ "date",
5721
+ "timestamp",
5722
+ "timestamptz",
5723
+ "time",
5724
+ "timetz",
5725
+ "interval",
5726
+ "json",
5727
+ "jsonb",
5728
+ "bytea"
5729
+ ]);
5730
+ var sqlValues = /* @__PURE__ */ new Set([
5731
+ "SVFOP_CURRENT_DATE",
5732
+ "SVFOP_CURRENT_TIME",
5733
+ "SVFOP_CURRENT_TIME_N",
5734
+ "SVFOP_CURRENT_TIMESTAMP",
5735
+ "SVFOP_CURRENT_TIMESTAMP_N",
5736
+ "SVFOP_LOCALTIME",
5737
+ "SVFOP_LOCALTIME_N",
5738
+ "SVFOP_LOCALTIMESTAMP",
5739
+ "SVFOP_LOCALTIMESTAMP_N",
5740
+ "SVFOP_CURRENT_ROLE",
5741
+ "SVFOP_CURRENT_USER",
5742
+ "SVFOP_USER",
5743
+ "SVFOP_SESSION_USER",
5744
+ "SVFOP_CURRENT_CATALOG",
5745
+ "SVFOP_CURRENT_SCHEMA"
5746
+ ]);
5747
+ async function validateManagedViewQueries({ canonical }) {
5748
+ for (const definition of [canonical, ...canonical.imports ?? []]) {
5749
+ for (const view of definition.views ?? []) {
5750
+ const fail4 = (message) => {
5751
+ throw new OrmDefinitionError(`managed view '${definition.namespace}/${view.view}': ${message}`);
5752
+ };
5753
+ parser2 ??= new PgParser({ version: 17 });
5754
+ const parsed = await parser2.parse(view.query);
5755
+ if (parsed.error || !parsed.tree) fail4("invalid SELECT query");
5756
+ const statements = list(obj(parsed.tree).stmts);
5757
+ if (statements.length !== 1 || !obj(obj(statements[0]).stmt).SelectStmt) fail4("one SELECT query is required");
5758
+ validateTree({ tree: parsed.tree, schema: ormSchemaName(definition.namespace), enums: definition.enums.map((entry) => entry.name), fail: fail4 });
5759
+ }
5760
+ }
5761
+ }
5762
+ function pgCatalogName(parts) {
5763
+ if (parts.length === 1) return parts[0];
5764
+ if (parts.length === 2 && parts[0] === "pg_catalog") return parts[1];
5765
+ return void 0;
5766
+ }
5767
+ function reviewedFunctionName({ parts, format }) {
5768
+ return format === "COERCE_SQL_SYNTAX" && parts.length === 2 && parts[0] === "pg_catalog" && parts[1] === "position" ? "strpos" : pgCatalogName(parts);
5769
+ }
5770
+ function validateTree({ tree, ...scope }) {
5771
+ const pending = [tree];
5772
+ while (pending.length) {
5773
+ const value = pending.pop();
5774
+ if (Array.isArray(value)) {
5775
+ pending.push(...value);
5776
+ continue;
5777
+ }
5778
+ if (!value || typeof value !== "object") continue;
5779
+ const node = obj(value);
5780
+ for (const [kind, child] of Object.entries(node)) {
5781
+ validateNode({ kind, child, scope });
5782
+ pending.push(child);
5783
+ }
5784
+ }
5785
+ }
5786
+ function validateNode({ kind, child, scope }) {
5787
+ const { fail: fail4 } = scope;
5788
+ if (kind.endsWith("Stmt") && kind !== "SelectStmt") fail4("only SELECT statements are permitted");
5789
+ if (kind === "FuncCall") {
5790
+ const call = obj(child);
5791
+ const parts = strings(call.funcname);
5792
+ const name2 = reviewedFunctionName({ parts, format: call.funcformat });
5793
+ if (!name2 || !functions.has(name2)) fail4(`only explicitly allowlisted pg_catalog functions are permitted (found '${parts.join(".")}')`);
5794
+ }
5795
+ if (kind === "typeName" || kind === "TypeName") validateType({ node: obj(child), ...scope });
5796
+ if (kind === "A_Expr" && pgCatalogName(strings(obj(child).name)) === void 0) fail4("only pg_catalog operators are permitted");
5797
+ if (kind === "SortBy" && list(obj(child).useOp).length && pgCatalogName(strings(obj(child).useOp)) === void 0) fail4("only pg_catalog sort operators are permitted");
5798
+ if (kind === "SQLValueFunction" && !sqlValues.has(String(obj(child).op))) fail4("unsupported SQL value expression");
5799
+ if (kind === "ParamRef") fail4("view queries cannot contain parameters");
5800
+ if (kind === "A_Indirection" && list(obj(child).indirection).some((entry) => !obj(entry).A_Indices)) {
5801
+ fail4("composite field notation is not permitted in managed views");
5802
+ }
5803
+ }
5804
+ function validateType({ node, schema, enums, fail: fail4 }) {
5805
+ const parts = strings(node.names);
5806
+ const builtin = pgCatalogName(parts);
5807
+ const managedEnum = parts.length === 1 && enums.includes(parts[0]) || parts.length === 2 && parts[0] === schema && enums.includes(parts[1]);
5808
+ if (node.setof || node.pct_type || !managedEnum && (!builtin || !scalarTypes.has(builtin))) {
5809
+ fail4(`casts may only use supported pg_catalog scalar types or declared enums (found '${parts.join(".")}')`);
5810
+ }
5811
+ }
5617
5812
 
5618
5813
  // src/sql/operations.ts
5619
5814
  function isSqlOperationDocument(path, contents) {
@@ -5629,7 +5824,13 @@ function isSqlOperationDocument(path, contents) {
5629
5824
  async function compileOperationDocuments(input) {
5630
5825
  const sqlDocuments = input.documents.filter((d) => isSqlOperationDocument(d.path, d.contents));
5631
5826
  const result = compileNamespace({ ...input, documents: input.documents.filter((d) => !isSqlOperationDocument(d.path, d.contents)) });
5632
- if (!result.ok || !sqlDocuments.length) return result;
5827
+ if (!result.ok) return result;
5828
+ const viewDiagnostics = await managedViewDiagnostics(input.canonical);
5829
+ if (viewDiagnostics.length) return { ok: false, diagnostics: [...result.diagnostics, ...viewDiagnostics] };
5830
+ if (!sqlDocuments.length) return result;
5831
+ return compileSqlDocuments({ input, result, sqlDocuments });
5832
+ }
5833
+ async function compileSqlDocuments({ input, result, sqlDocuments }) {
5633
5834
  for (const document of sqlDocuments) {
5634
5835
  try {
5635
5836
  const definition = defineOperation(JSON.parse(document.contents));
@@ -5723,6 +5924,18 @@ async function compileOperationDocuments(input) {
5723
5924
  result.manifest.opsHash = result.artifact.opsHash;
5724
5925
  return { ...result, artifact: normalizeIr(result.artifact) };
5725
5926
  }
5927
+ async function managedViewDiagnostics(canonical) {
5928
+ try {
5929
+ await validateManagedViewQueries({ canonical });
5930
+ return [];
5931
+ } catch (error) {
5932
+ return [{
5933
+ severity: "error",
5934
+ code: "invalid-view-query",
5935
+ message: error instanceof Error ? error.message : "invalid managed view query"
5936
+ }];
5937
+ }
5938
+ }
5726
5939
 
5727
5940
  // src/sql/codegen.ts
5728
5941
  var typeOf = (type) => ["integer", "double"].includes(type) ? "number" : type === "boolean" ? "boolean" : "string";
@@ -7818,7 +8031,7 @@ function snapshotTableNames(snapshot, schema) {
7818
8031
  // src/engine/materialized-refresh.ts
7819
8032
  function materializedRefreshFunctions(canonical) {
7820
8033
  const schema = ormSchemaName(canonical.namespace);
7821
- const functions = /* @__PURE__ */ new Map();
8034
+ const functions2 = /* @__PURE__ */ new Map();
7822
8035
  for (const view of canonical.views ?? []) {
7823
8036
  const materialized = view.resolved?.materialized;
7824
8037
  if (!materialized) continue;
@@ -7827,7 +8040,7 @@ function materializedRefreshFunctions(canonical) {
7827
8040
  if (!table || pk.length === 0) {
7828
8041
  throw new OrmDefinitionError(`materialized resolved table '${view.view}' needs its table and primary key`);
7829
8042
  }
7830
- functions.set(view.view, refreshFunctionSql({
8043
+ functions2.set(view.view, refreshFunctionSql({
7831
8044
  schema,
7832
8045
  name: view.view,
7833
8046
  table: materialized.table,
@@ -7836,7 +8049,7 @@ function materializedRefreshFunctions(canonical) {
7836
8049
  pk
7837
8050
  }));
7838
8051
  }
7839
- return functions;
8052
+ return functions2;
7840
8053
  }
7841
8054
  function refreshFunctionSql({ schema, name: name2, table, compute, columns, pk }) {
7842
8055
  const fn = `${quoteIdentifier(schema)}.${quoteIdentifier(materializedRefreshFunctionName(name2))}`;
@@ -8480,6 +8693,7 @@ async function planWithViewRecreation({
8480
8693
  previousSnapshot,
8481
8694
  viewRecreation
8482
8695
  }) {
8696
+ await validateManagedViewQueries({ canonical });
8483
8697
  const schema = ormSchemaName(canonical.namespace);
8484
8698
  const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
8485
8699
  assertValidViewDependencies(canonical);
@@ -9770,6 +9984,9 @@ function generateSdl(canonical, options = {}) {
9770
9984
  blocks.push(
9771
9985
  '"""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
9986
  );
9987
+ if (!options.omitReadBudgetDirective) blocks.push(
9988
+ '"""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'
9989
+ );
9773
9990
  blocks.push("directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT");
9774
9991
  blocks.push(
9775
9992
  '"""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 +12191,7 @@ function emitSyncedTable(table) {
11974
12191
  lines.push(" },", " {");
11975
12192
  lines.push(` source: ${quote(table.source ?? "")},`);
11976
12193
  if (table.description !== void 0) lines.push(` description: ${quote(table.description)},`);
11977
- if (singlePrimaryKey === void 0 && table.primaryKey && table.primaryKey.length > 0) {
12194
+ if (singlePrimaryKey === void 0 && table.primaryKey !== void 0) {
11978
12195
  lines.push(` primaryKey: ${jsonLiteral(table.primaryKey, 4)},`);
11979
12196
  }
11980
12197
  if (table.links.length > 0) {