@malloy-publisher/server 0.2.4 → 0.2.6

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.
package/dist/server.mjs CHANGED
@@ -156558,6 +156558,8 @@ function internalErrorToHttpError(error) {
156558
156558
  return httpError(404, error.message);
156559
156559
  } else if (error instanceof MalloyError) {
156560
156560
  return httpError(400, error.message);
156561
+ } else if (error instanceof TableNotFoundError) {
156562
+ return httpError(404, error.message, "TABLE_NOT_FOUND");
156561
156563
  } else if (error instanceof ConnectionNotFoundError) {
156562
156564
  return httpError(404, error.message);
156563
156565
  } else if (error instanceof DestinationNotFoundError) {
@@ -156590,16 +156592,17 @@ function internalErrorToHttpError(error) {
156590
156592
  return httpError(500, error.message);
156591
156593
  }
156592
156594
  }
156593
- function httpError(code, message) {
156595
+ function httpError(code, message, reason) {
156594
156596
  return {
156595
156597
  status: code,
156596
156598
  json: {
156597
156599
  code,
156598
- message
156600
+ message,
156601
+ ...reason ? { reason } : {}
156599
156602
  }
156600
156603
  };
156601
156604
  }
156602
- var NotImplementedError, BadRequestError, InvalidArgumentError, EnvironmentNotFoundError, PackageNotFoundError, ModelNotFoundError, DashboardNotFoundError, ConnectionNotFoundError, ConnectionError, DestinationNotFoundError, ConnectionAuthError, UnsupportedCatalogFormatError, ModelCompilationError, MaterializationEligibilityError, PublisherConfigError, FrozenConfigError, AccessDeniedError, NotQueryableError, MaterializationNotFoundError, MaterializationConflictError, InvalidStateTransitionError, ServiceUnavailableError, PayloadTooLargeError, ResponseUnserializableError, QueryTimeoutError;
156605
+ var NotImplementedError, BadRequestError, InvalidArgumentError, EnvironmentNotFoundError, PackageNotFoundError, ModelNotFoundError, DashboardNotFoundError, ConnectionNotFoundError, TableNotFoundError, ConnectionError, DestinationNotFoundError, ConnectionAuthError, UnsupportedCatalogFormatError, ModelCompilationError, MaterializationEligibilityError, PublisherConfigError, FrozenConfigError, AccessDeniedError, NotQueryableError, MaterializationNotFoundError, MaterializationConflictError, InvalidStateTransitionError, ServiceUnavailableError, PayloadTooLargeError, ResponseUnserializableError, QueryTimeoutError;
156603
156606
  var init_errors = __esm(() => {
156604
156607
  init_constants();
156605
156608
  NotImplementedError = class NotImplementedError extends Error {
@@ -156639,6 +156642,11 @@ var init_errors = __esm(() => {
156639
156642
  super(message);
156640
156643
  }
156641
156644
  };
156645
+ TableNotFoundError = class TableNotFoundError extends Error {
156646
+ constructor(message) {
156647
+ super(message);
156648
+ }
156649
+ };
156642
156650
  ConnectionError = class ConnectionError extends Error {
156643
156651
  constructor(message) {
156644
156652
  super(message);
@@ -234644,14 +234652,14 @@ var init_connection = __esm(() => {
234644
234652
  });
234645
234653
  const result2 = await super.fetchTableSchema(tableKey, azureUrl);
234646
234654
  if (!result2) {
234647
- throw new Error(`Azure file not found: ${azureUrl}`);
234655
+ throw new TableNotFoundError(`Azure file not found: ${azureUrl}`);
234648
234656
  }
234649
234657
  return result2;
234650
234658
  }
234651
234659
  }
234652
234660
  const result = await super.fetchTableSchema(tableKey, tablePath);
234653
234661
  if (!result) {
234654
- throw new Error(`Table ${tablePath} not found`);
234662
+ throw new TableNotFoundError(`Table ${tablePath} not found`);
234655
234663
  }
234656
234664
  return result;
234657
234665
  }
@@ -234703,13 +234711,13 @@ var init_connection = __esm(() => {
234703
234711
  });
234704
234712
  const result2 = await super.fetchTableSchema(tableKey, prefixedPath);
234705
234713
  if (!result2) {
234706
- throw new Error(`Table ${prefixedPath} not found in connection ${this.connectionName}`);
234714
+ throw new TableNotFoundError(`Table ${prefixedPath} not found in connection ${this.connectionName}`);
234707
234715
  }
234708
234716
  return result2;
234709
234717
  }
234710
234718
  const result = await super.fetchTableSchema(tableKey, tablePath);
234711
234719
  if (!result) {
234712
- throw new Error(`Table ${tablePath} not found in connection ${this.connectionName}`);
234720
+ throw new TableNotFoundError(`Table ${tablePath} not found in connection ${this.connectionName}`);
234713
234721
  }
234714
234722
  return result;
234715
234723
  }
@@ -251802,6 +251810,118 @@ var init_gate_registry_walk = __esm(() => {
251802
251810
  init_authorize();
251803
251811
  });
251804
251812
 
251813
+ // src/service/partition_annotation.ts
251814
+ import { payloadOf as payloadOf2, routeOf as routeOf2 } from "@malloydata/malloy";
251815
+ function noteRoute2(text) {
251816
+ return routeOf2({ value: text.trimStart() });
251817
+ }
251818
+ function containsPartitionAnnotationTag(texts) {
251819
+ return texts.some((text) => noteRoute2(text) === PARTITION_ROUTE);
251820
+ }
251821
+ function reachesPartitionTagBelow(node, seen = new WeakSet, depth = 0) {
251822
+ if (depth > MAX_PARTITION_IR_WALK_DEPTH) {
251823
+ throw new Error("partition-marker IR walk exceeded max depth");
251824
+ }
251825
+ if (node === null || typeof node !== "object")
251826
+ return false;
251827
+ if (seen.has(node))
251828
+ return false;
251829
+ seen.add(node);
251830
+ if (Array.isArray(node)) {
251831
+ return node.some((item) => reachesPartitionTagBelow(item, seen, depth + 1));
251832
+ }
251833
+ const record = node;
251834
+ if (depth > 0 && record.join !== undefined)
251835
+ return false;
251836
+ if (depth > 0) {
251837
+ for (const key of ["blockNotes", "notes"]) {
251838
+ const arr = record[key];
251839
+ if (!Array.isArray(arr))
251840
+ continue;
251841
+ const texts = arr.map((n) => typeof n === "string" ? n : n && typeof n === "object" && typeof n.text === "string" ? n.text : undefined).filter((text) => text !== undefined);
251842
+ if (containsPartitionAnnotationTag(texts))
251843
+ return true;
251844
+ }
251845
+ }
251846
+ return Object.entries(record).some(([key, value]) => depth === 0 && key === "annotations" ? false : reachesPartitionTagBelow(value, seen, depth + 1));
251847
+ }
251848
+ function notePayload2(text) {
251849
+ return payloadOf2({ value: text.trimStart() }) ?? "";
251850
+ }
251851
+ function rejectionMessage(sourceName, body, detail) {
251852
+ return `Source "${sourceName}" declares \`#(partition) ${body}\`: ${detail} ` + `#(partition) only accepts \`<column> = $GIVEN\`, where <column> is a ` + `single field or a dotted join path.`;
251853
+ }
251854
+ function reject(sourceName, body, cause, detail) {
251855
+ throw new PartitionAnnotationError(cause, rejectionMessage(sourceName, body, detail));
251856
+ }
251857
+ function parsePartitionAnnotation(sourceName, annotationText) {
251858
+ if (noteRoute2(annotationText) !== PARTITION_ROUTE)
251859
+ return null;
251860
+ const body = notePayload2(annotationText).trim();
251861
+ if (body.length === 0) {
251862
+ reject(sourceName, body, "empty_body", "the expression body is empty.");
251863
+ }
251864
+ if (COMPOUND_BOOLEAN_RE.test(body)) {
251865
+ reject(sourceName, body, "compound_boolean", "a compound boolean (`and`/`or`/`not`) is not allowed — declare one " + "`#(partition)` marker per column.");
251866
+ }
251867
+ if (IN_OPERATOR_RE.test(body)) {
251868
+ reject(sourceName, body, "in_operator", "the `in` operator is not allowed.");
251869
+ }
251870
+ if (NEGATED_OPERATOR_RE.test(body)) {
251871
+ reject(sourceName, body, "negated_operator", "`!=` is not allowed.");
251872
+ }
251873
+ if (COMPARISON_OPERATOR_RE.test(body)) {
251874
+ reject(sourceName, body, "comparison_operator", "only `=` is allowed, not `<`/`>`/`<=`/`>=`.");
251875
+ }
251876
+ const eq = body.indexOf("=");
251877
+ if (eq === -1) {
251878
+ reject(sourceName, body, "malformed_body", "no `=` was found.");
251879
+ }
251880
+ const left = body.slice(0, eq).trim();
251881
+ const right = body.slice(eq + 1).trim();
251882
+ if (!FIELD_PATH_RE.test(left)) {
251883
+ reject(sourceName, body, "left_not_field_path", `\`${left}\` is not a field path — the left side must be a single ` + "column or a dotted join path, not an expression.");
251884
+ }
251885
+ const givenMatch = GIVEN_REF_RE.exec(right);
251886
+ if (!givenMatch) {
251887
+ reject(sourceName, body, "missing_given_reference", `\`${right}\` is not a given reference — the right side must be ` + "`$NAME`.");
251888
+ }
251889
+ return { column: left, given: givenMatch[1] };
251890
+ }
251891
+ function collectPartitionPairs(sourceName, annotationTexts2) {
251892
+ const pairs = [];
251893
+ const seenGivens = new Map;
251894
+ for (const text of annotationTexts2) {
251895
+ const pair = parsePartitionAnnotation(sourceName, text);
251896
+ if (pair === null)
251897
+ continue;
251898
+ const priorColumn = seenGivens.get(pair.given);
251899
+ if (priorColumn !== undefined) {
251900
+ throw new PartitionAnnotationError("duplicate_given", `Source "${sourceName}" declares \`#(partition)\` on both ` + `\`${priorColumn}\` and \`${pair.column}\` for the same given ` + `\`$${pair.given}\` — a given can back at most one partition ` + `column per source.`);
251901
+ }
251902
+ seenGivens.set(pair.given, pair.column);
251903
+ pairs.push(pair);
251904
+ }
251905
+ return pairs;
251906
+ }
251907
+ var PARTITION_ROUTE = "partition", MAX_PARTITION_IR_WALK_DEPTH = 200, PartitionAnnotationError, IDENT = "[A-Za-z_][A-Za-z0-9_]*", FIELD_PATH_RE, GIVEN_REF_RE, COMPOUND_BOOLEAN_RE, IN_OPERATOR_RE, NEGATED_OPERATOR_RE, COMPARISON_OPERATOR_RE;
251908
+ var init_partition_annotation = __esm(() => {
251909
+ init_errors();
251910
+ PartitionAnnotationError = class PartitionAnnotationError extends ModelCompilationError {
251911
+ rejectionCause;
251912
+ constructor(rejectionCause, message) {
251913
+ super({ message });
251914
+ this.rejectionCause = rejectionCause;
251915
+ }
251916
+ };
251917
+ FIELD_PATH_RE = new RegExp(`^${IDENT}(?:\\.${IDENT})*$`);
251918
+ GIVEN_REF_RE = new RegExp(`^\\$(${IDENT})$`);
251919
+ COMPOUND_BOOLEAN_RE = /\b(and|or|not)\b/i;
251920
+ IN_OPERATOR_RE = /\bin\b/i;
251921
+ NEGATED_OPERATOR_RE = /!=/;
251922
+ COMPARISON_OPERATOR_RE = /(>=|<=|>|<)/;
251923
+ });
251924
+
251805
251925
  // src/service/gate_classification.ts
251806
251926
  import {
251807
251927
  isSourceDef as isSourceDef4
@@ -252032,12 +252152,142 @@ function isBareFalseLiteral(expr) {
252032
252152
  function computeGivenDeclaredTypes(givens) {
252033
252153
  return new Map((givens ?? []).filter((g) => g.name != null && g.type != null).map((g) => [g.name, g.type]));
252034
252154
  }
252155
+ function resolveEntryPointPartitions(struct, modelDef, seen = new Set) {
252156
+ if (!struct || !modelDef || seen.has(struct))
252157
+ return [];
252158
+ seen.add(struct);
252159
+ const label = struct.as ?? struct.name;
252160
+ const own = collectPartitionPairs(label, ownLevelNotes(struct.annotations).map((note) => note.text));
252161
+ if (own.length > 0)
252162
+ return own;
252163
+ let inherited = struct.annotations?.inherits;
252164
+ for (let depth = 0;inherited && depth < ANCESTOR_WALK_MAX_DEPTH; depth++) {
252165
+ const pairs = collectPartitionPairs(label, ownLevelNoteTexts(inherited));
252166
+ if (pairs.length > 0)
252167
+ return pairs;
252168
+ inherited = inherited.inherits;
252169
+ }
252170
+ if (inherited)
252171
+ throw unresolvableAncestry(label);
252172
+ const declared = resolveDeclaredSource(struct, modelDef);
252173
+ if (declared.kind === "unresolvable")
252174
+ throw unresolvableAncestry(label);
252175
+ if (declared.kind === "resolved") {
252176
+ const pairs = resolveEntryPointPartitions(declared.source, modelDef, seen);
252177
+ if (pairs.length > 0)
252178
+ return pairs;
252179
+ }
252180
+ const queryBase = resolveQuerySourceBase(struct, modelDef);
252181
+ if (queryBase)
252182
+ return resolveEntryPointPartitions(queryBase, modelDef, seen);
252183
+ return [];
252184
+ }
252185
+ function unresolvableAncestry(label) {
252186
+ return new PartitionAnnotationError("ancestry_unresolvable", `Could not resolve whether source "${label}" carries a ` + `\`#(partition)\` marker: the derivation chain it inherits from ` + `could not be read. Denying rather than serving unfiltered.`);
252187
+ }
252188
+ function assertPartitionAnnotationsValid(modelDef) {
252189
+ if (!modelDef)
252190
+ return;
252191
+ for (const [key, obj] of Object.entries(modelDef.contents)) {
252192
+ if (!isSourceDef4(obj))
252193
+ continue;
252194
+ const label = obj.as ?? obj.name ?? key;
252195
+ if (obj.type !== "composite") {
252196
+ let resolved = [];
252197
+ try {
252198
+ resolved = resolveEntryPointPartitions(obj, modelDef);
252199
+ } catch (err) {
252200
+ if (!(err instanceof PartitionAnnotationError) || err.rejectionCause !== "ancestry_unresolvable") {
252201
+ throw err;
252202
+ }
252203
+ continue;
252204
+ }
252205
+ assertNoUnreachableMarker(obj, label, resolved);
252206
+ continue;
252207
+ }
252208
+ if (resolveEntryPointPartitions(obj, modelDef).length > 0) {
252209
+ throw new PartitionAnnotationError("partitioned_composite", `Source "${label}" is a composite source (\`compose(...)\`) and ` + `also declares \`#(partition)\` (on itself or an ancestor it ` + `derives from). A composite run target compiles each query ` + `against one resolved member branch, which a partition filter ` + `cannot attach to — declare \`#(partition)\` on a non-composite ` + `source instead.`);
252210
+ }
252211
+ const marked = partitionedMemberLabel(obj, modelDef);
252212
+ if (marked !== undefined) {
252213
+ throw new PartitionAnnotationError("partitioned_composite", `Source "${label}" is a composite source (\`compose(...)\`) with ` + `member "${marked}", which declares \`#(partition)\`. MEASURED: ` + `querying the composite reads EVERY partition of that member — ` + `the graft lands on the composite's own struct, not the ` + `resolved member branch, so no filter reaches the executed ` + `query. Remove the composite, or drop \`#(partition)\` from ` + `"${marked}" and expose it as its own source.`);
252214
+ }
252215
+ }
252216
+ }
252217
+ function assertNoUnreachableMarker(struct, label, resolved) {
252218
+ if (resolved.length > 0)
252219
+ return;
252220
+ let reachesBelow;
252221
+ try {
252222
+ reachesBelow = reachesPartitionTagBelow(struct);
252223
+ } catch {
252224
+ reachesBelow = true;
252225
+ }
252226
+ if (!reachesBelow)
252227
+ return;
252228
+ throw new PartitionAnnotationError("marker_unreachable", `Source "${label}" carries a \`#(partition)\` marker that is not ` + `declared on the source itself — it sits on a field, a view, or an ` + `inline \`compose(...)\` inside it. Nothing would filter this ` + `source: every caller would read every partition. Move the marker ` + `onto the \`source:\` declaration.`);
252229
+ }
252230
+ function partitionedMemberLabel(composite, modelDef, seen = new Set) {
252231
+ if (seen.has(composite))
252232
+ return;
252233
+ seen.add(composite);
252234
+ for (const member of composite.sources ?? []) {
252235
+ const label = member.as ?? member.name;
252236
+ if (resolveEntryPointPartitions(member, modelDef).length > 0) {
252237
+ return label;
252238
+ }
252239
+ if (member.type === "composite") {
252240
+ const nested = partitionedMemberLabel(member, modelDef, seen);
252241
+ if (nested !== undefined)
252242
+ return nested;
252243
+ }
252244
+ }
252245
+ return;
252246
+ }
252247
+ async function resolvePartitionGraftEntries(struct, originModelDef, graftScope, deps) {
252248
+ if (!struct || !originModelDef)
252249
+ return [];
252250
+ const pairs = resolveEntryPointPartitions(struct, originModelDef);
252251
+ if (pairs.length === 0)
252252
+ return [];
252253
+ const label = struct.as ?? struct.name;
252254
+ if (!graftScope) {
252255
+ logger.debug("Partition filter has no graft scope to attach to; denying", { modelPath: deps.modelPath, label });
252256
+ throw new Error(`partition on "${label}" has no graft scope`);
252257
+ }
252258
+ const graftTarget = resolveGraftTarget(struct, originModelDef, graftScope.modelDef);
252259
+ if (!graftTarget) {
252260
+ logger.debug("Partition filter resolved to no graft target; denying", {
252261
+ modelPath: deps.modelPath,
252262
+ label
252263
+ });
252264
+ throw new Error(`partition on "${label}" resolved to no graft target`);
252265
+ }
252266
+ const entries = [];
252267
+ for (const pair of pairs) {
252268
+ if (!deps.givenDeclaredTypes.has(pair.given)) {
252269
+ logger.warn("Partition references a given off the model surface; denying", { modelPath: deps.modelPath, graftTarget, givenName: pair.given });
252270
+ throw new Error(`partition on "${label}" references \`$${pair.given}\`, which is not on this model's given surface`);
252271
+ }
252272
+ const filterText = `${pair.column} = $${pair.given}`;
252273
+ const condition = await liftGateCondition(graftTarget, filterText, graftScope.materializer);
252274
+ entries.push({
252275
+ label,
252276
+ graftTarget,
252277
+ filterText,
252278
+ condition,
252279
+ givenNames: [pair.given]
252280
+ });
252281
+ }
252282
+ return entries;
252283
+ }
252035
252284
  var init_gate_classification = __esm(() => {
252036
252285
  init_logger();
252037
252286
  init_annotations();
252038
252287
  init_authorize();
252039
252288
  init_gate_dimension();
252040
252289
  init_gate_registry_walk();
252290
+ init_partition_annotation();
252041
252291
  });
252042
252292
 
252043
252293
  // src/service/incremental_declaration.ts
@@ -252234,8 +252484,24 @@ function assertMaterializationEligible(persistSource) {
252234
252484
  message: `Source '${sourceName}' cannot be materialized into a storage ` + `destination: it is protected by an #(authorize) gate (its own or a ` + `joined source's). An authorize expression is evaluated per request; ` + `a materialized-once table served frozen carries no gate, so it would ` + `be served to everyone, bypassing authorization. This is refused for ` + `safety. Serve this source live (drop 'storage=').`
252235
252485
  });
252236
252486
  }
252487
+ if (referencesPartition(persistSource)) {
252488
+ recordEligibilityRefused("partition");
252489
+ throw new MaterializationEligibilityError({
252490
+ reason: "partition",
252491
+ message: `Source '${sourceName}' cannot be materialized into a storage ` + `destination: it declares a #(partition) marker. A partition ` + `filter binds a given at query time (the same mechanism as ` + `row-level access control), so a materialized-once table served ` + `frozen would leak every partition's rows to every caller. This ` + `is refused for safety. Serve this source live (drop 'storage=').`
252492
+ });
252493
+ }
252237
252494
  }
252238
252495
  function assertColocatedPersistNotAuthorizeGated(persistSource, sourceName = persistSource.name, origin2 = "persist", gateOutcome) {
252496
+ if (referencesPartition(persistSource)) {
252497
+ recordEligibilityRefused("partition");
252498
+ const what2 = origin2 === "preaggregate" ? `Pre-aggregation rollup '${sourceName}'` : `Source '${sourceName}'`;
252499
+ const gated2 = origin2 === "preaggregate" ? `the source '${sourceName}' rolls up declares` : `it declares`;
252500
+ throw new MaterializationEligibilityError({
252501
+ reason: "partition",
252502
+ message: `${what2} cannot be materialized (colocated '#@ persist'): ` + `${gated2} a #(partition) marker. A partition filter binds a ` + `given at query time; this pass cannot prove the persisted ` + `artifact's read path still applies it, so this is refused for ` + `safety. Move the marker to a source that is not materialized, ` + `or stop persisting this one.`
252503
+ });
252504
+ }
252239
252505
  if (!referencesAuthorize(persistSource))
252240
252506
  return;
252241
252507
  if (origin2 === "persist" && gateOutcome?.classification === "row_level" && gateOutcome.attributed) {
@@ -252311,6 +252577,44 @@ function walkForGiven(node, seen, depth) {
252311
252577
  }
252312
252578
  return false;
252313
252579
  }
252580
+ function referencesPartition(persistSource) {
252581
+ try {
252582
+ return walkForPartition(persistSource._sourceDef, new WeakSet, 0);
252583
+ } catch {
252584
+ return true;
252585
+ }
252586
+ }
252587
+ function walkForPartition(node, seen, depth) {
252588
+ if (depth > MAX_GIVEN_WALK_DEPTH) {
252589
+ throw new Error("partition-usage walk exceeded max depth");
252590
+ }
252591
+ if (node === null || typeof node !== "object")
252592
+ return false;
252593
+ if (seen.has(node))
252594
+ return false;
252595
+ seen.add(node);
252596
+ if (Array.isArray(node)) {
252597
+ for (const item of node) {
252598
+ if (walkForPartition(item, seen, depth + 1))
252599
+ return true;
252600
+ }
252601
+ return false;
252602
+ }
252603
+ const record = node;
252604
+ for (const key of ["blockNotes", "notes"]) {
252605
+ const arr = record[key];
252606
+ if (!Array.isArray(arr))
252607
+ continue;
252608
+ const texts = arr.map((n) => typeof n === "string" ? n : n && typeof n === "object" && typeof n.text === "string" ? n.text : undefined).filter((text) => text !== undefined);
252609
+ if (containsPartitionAnnotationTag(texts))
252610
+ return true;
252611
+ }
252612
+ for (const value of Object.values(record)) {
252613
+ if (walkForPartition(value, seen, depth + 1))
252614
+ return true;
252615
+ }
252616
+ return false;
252617
+ }
252314
252618
  function referencesAuthorize(persistSource) {
252315
252619
  try {
252316
252620
  return walkForAuthorize(persistSource._sourceDef, new WeakSet, 0);
@@ -252411,6 +252715,7 @@ var init_materialization_eligibility = __esm(() => {
252411
252715
  init_errors();
252412
252716
  init_materialization_metrics();
252413
252717
  init_authorize();
252718
+ init_partition_annotation();
252414
252719
  });
252415
252720
 
252416
252721
  // src/service/preaggregation_compile.ts
@@ -253865,6 +254170,7 @@ var init_model = __esm(() => {
253865
254170
  init_query_metadata();
253866
254171
  init_preaggregation_validation();
253867
254172
  init_gate_registry_walk();
254173
+ init_partition_annotation();
253868
254174
  init_gate_classification();
253869
254175
  init_source_extraction();
253870
254176
  init_authorize_metrics();
@@ -254060,20 +254366,7 @@ var init_model = __esm(() => {
254060
254366
  if (!modelDef)
254061
254367
  return false;
254062
254368
  try {
254063
- const structs = [];
254064
- for (const obj of Object.values(modelDef.contents)) {
254065
- if (isSourceDef6(obj))
254066
- structs.push(obj);
254067
- }
254068
- for (const value of Object.values(modelDef.sourceRegistry ?? {})) {
254069
- const entry = value.entry;
254070
- if (entry.type === "source_registry_reference")
254071
- continue;
254072
- if (isSourceDef6(entry))
254073
- structs.push(entry);
254074
- }
254075
- structs.push(...derivedStructsReachable(structs, modelDef));
254076
- for (const struct of structs) {
254369
+ for (const struct of this.reachableStructsForNoteSweep(modelDef)) {
254077
254370
  if (containsAuthorizeAnnotationTag(annotationTexts(struct.annotations) ?? [])) {
254078
254371
  return true;
254079
254372
  }
@@ -254090,6 +254383,43 @@ var init_model = __esm(() => {
254090
254383
  })();
254091
254384
  return this.anyAuthorizeNote;
254092
254385
  }
254386
+ reachableStructsForNoteSweep(modelDef) {
254387
+ const structs = [];
254388
+ for (const obj of Object.values(modelDef.contents)) {
254389
+ if (isSourceDef6(obj))
254390
+ structs.push(obj);
254391
+ }
254392
+ for (const value of Object.values(modelDef.sourceRegistry ?? {})) {
254393
+ const entry = value.entry;
254394
+ if (entry.type === "source_registry_reference")
254395
+ continue;
254396
+ if (isSourceDef6(entry))
254397
+ structs.push(entry);
254398
+ }
254399
+ structs.push(...derivedStructsReachable(structs, modelDef));
254400
+ return structs;
254401
+ }
254402
+ anyPartitionNote;
254403
+ hasAnyPartitionNote() {
254404
+ if (this.anyPartitionNote !== undefined)
254405
+ return this.anyPartitionNote;
254406
+ this.anyPartitionNote = (() => {
254407
+ const modelDef = this.modelDef;
254408
+ if (!modelDef)
254409
+ return false;
254410
+ try {
254411
+ for (const struct of this.reachableStructsForNoteSweep(modelDef)) {
254412
+ if (containsPartitionAnnotationTag(annotationTexts(struct.annotations) ?? [])) {
254413
+ return true;
254414
+ }
254415
+ }
254416
+ return false;
254417
+ } catch {
254418
+ return true;
254419
+ }
254420
+ })();
254421
+ return this.anyPartitionNote;
254422
+ }
254093
254423
  async assertAuthorized(sourceName, _givens, bypassAuthorize = false, graftScope = this.defaultGraftScope()) {
254094
254424
  if (bypassAuthorize) {
254095
254425
  this.noteAuthorizeBypass("source", sourceName);
@@ -254146,7 +254476,7 @@ var init_model = __esm(() => {
254146
254476
  entryPointGates = Array.from(byKey.values());
254147
254477
  }
254148
254478
  }
254149
- return { entryPointGates, modelDef };
254479
+ return { entryPointGates, modelDef, struct };
254150
254480
  }
254151
254481
  async assertAuthorizedFromCompiledRunnable(runnable, givens) {
254152
254482
  await this.authorizeAndBindRunnable(runnable, givens, {
@@ -254158,7 +254488,7 @@ var init_model = __esm(() => {
254158
254488
  return this.rowLevelFilteredRunnables.has(runnable);
254159
254489
  }
254160
254490
  async probeEntryPointGates(runnable, givens, graftScope, skipOwnSourceGate = false) {
254161
- const { entryPointGates, modelDef } = await this.collectAuthorizeEntryPointGates(runnable, givens, graftScope, skipOwnSourceGate);
254491
+ const { entryPointGates, modelDef, struct } = await this.collectAuthorizeEntryPointGates(runnable, givens, graftScope, skipOwnSourceGate);
254162
254492
  const rowLevel = [];
254163
254493
  for (const entry of entryPointGates) {
254164
254494
  const resolution = modelDef ? await this.resolveGateShape(entry, modelDef, graftScope) : { shape: "rejected", cause: undefined };
@@ -254177,6 +254507,16 @@ var init_model = __esm(() => {
254177
254507
  recordRowLevelGateRejected(resolution.cause);
254178
254508
  throw new AccessDeniedError(`Access denied for source "${entry.label}".`);
254179
254509
  }
254510
+ try {
254511
+ rowLevel.push(...await resolvePartitionGraftEntries(struct, modelDef, graftScope, this.gateClassificationDeps()));
254512
+ } catch (err) {
254513
+ recordRowLevelGateDecision("denied_by_gate");
254514
+ logger.debug("Partition filter could not be resolved; denying", {
254515
+ modelPath: this.modelPath,
254516
+ error: err instanceof Error ? err.message : String(err)
254517
+ });
254518
+ throw new AccessDeniedError(`Access denied for source "${struct?.as ?? struct?.name ?? "unknown"}".`);
254519
+ }
254180
254520
  return rowLevel;
254181
254521
  }
254182
254522
  async queryEntryPointHasRowLevelGate(runnable) {
@@ -254189,7 +254529,7 @@ var init_model = __esm(() => {
254189
254529
  if (compositeResolvedSourceDef) {
254190
254530
  gates.push(...this.collectEntryPointGates(compositeResolvedSourceDef, modelDef, seen, true, undefined, struct ? ownLevelNotes(struct.annotations) : []));
254191
254531
  }
254192
- return gates.length > 0;
254532
+ return gates.length > 0 || resolveEntryPointPartitions(struct, modelDef).length > 0;
254193
254533
  } catch {
254194
254534
  return true;
254195
254535
  }
@@ -254520,6 +254860,7 @@ var init_model = __esm(() => {
254520
254860
  filterMap = sourceResult.filterMap;
254521
254861
  const queryResult = Model.getQueries(modelDef);
254522
254862
  queries = queryResult.queries;
254863
+ assertPartitionAnnotationsValid(modelDef);
254523
254864
  assertNoMisplacedAuthorizeAnnotations([
254524
254865
  ...sourceResult.misplacedAuthorize,
254525
254866
  ...queryResult.misplacedAuthorize
@@ -255147,7 +255488,7 @@ run: ${sourceName ? `${quoteMalloyIdentifier2(sourceName)} -> ` : ""}${quoteMall
255147
255488
  liveRunnable = runnable;
255148
255489
  const storageRoutingPossible = getPersistStorageMode() === "on" && this.serveBindings.length > 0 && !!this.serveDestinationConfig;
255149
255490
  const preaggServeMaterializer = this.preaggregateServeMaterializer;
255150
- const routingBlockedByRowLevelGate = (storageRoutingPossible || !!preaggServeMaterializer) && !bypassAuthorize && this.hasAnyAuthorizeNote() && await this.queryEntryPointHasRowLevelGate(runnable);
255491
+ const routingBlockedByRowLevelGate = (storageRoutingPossible || !!preaggServeMaterializer) && !bypassAuthorize && (this.hasAnyAuthorizeNote() || this.hasAnyPartitionNote()) && await this.queryEntryPointHasRowLevelGate(runnable);
255151
255492
  if (routingBlockedByRowLevelGate) {
255152
255493
  recordStorageServeRouting("blocked_by_row_level_gate");
255153
255494
  }
@@ -256631,10 +256972,10 @@ var require_recursive_readdir = __commonJS((exports, module) => {
256631
256972
  ignores = [];
256632
256973
  }
256633
256974
  if (!callback) {
256634
- return new Promise(function(resolve4, reject) {
256975
+ return new Promise(function(resolve4, reject2) {
256635
256976
  readdir3(path9, ignores || [], function(err, data) {
256636
256977
  if (err) {
256637
- reject(err);
256978
+ reject2(err);
256638
256979
  } else {
256639
256980
  resolve4(data);
256640
256981
  }
@@ -277216,6 +277557,26 @@ function validateAdminAuthoredConnection(connectionName, connectionConfig) {
277216
277557
  }
277217
277558
  }
277218
277559
  }
277560
+ var BIGQUERY_NOT_FOUND = /^Not found: (Table|Dataset)\b/;
277561
+ var BIGQUERY_IMPROPER_PATH = /^Improper table path\b/;
277562
+ var DUCKDB_TABLE_NOT_FOUND = /^Catalog Error: Table with name .+ does not exist/;
277563
+ var DUCKDB_CATALOG_NOT_FOUND = /^Binder Error: Catalog .+ does not exist/;
277564
+ function driverErrorToPublisherError(message) {
277565
+ if (BIGQUERY_NOT_FOUND.test(message) || DUCKDB_TABLE_NOT_FOUND.test(message) || DUCKDB_CATALOG_NOT_FOUND.test(message)) {
277566
+ return new TableNotFoundError(message);
277567
+ }
277568
+ if (BIGQUERY_IMPROPER_PATH.test(message)) {
277569
+ return new InvalidArgumentError(message);
277570
+ }
277571
+ return new ConnectionError(message);
277572
+ }
277573
+ function classifyDriverFailure(error) {
277574
+ if (error instanceof TableNotFoundError || error instanceof InvalidArgumentError) {
277575
+ return error;
277576
+ }
277577
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : JSON.stringify(error);
277578
+ return driverErrorToPublisherError(message);
277579
+ }
277219
277580
 
277220
277581
  class ConnectionController {
277221
277582
  environmentStore;
@@ -277282,10 +277643,10 @@ class ConnectionController {
277282
277643
  try {
277283
277644
  const source = await malloyConnection.fetchTableSchema(tableKey, tablePath);
277284
277645
  if (!source) {
277285
- throw new ConnectionError(`Table ${tablePath} not found`);
277646
+ throw new TableNotFoundError(`Table ${tablePath} not found`);
277286
277647
  }
277287
277648
  if (typeof source === "string") {
277288
- throw new ConnectionError(source);
277649
+ throw driverErrorToPublisherError(source);
277289
277650
  }
277290
277651
  return {
277291
277652
  source: JSON.stringify(source),
@@ -277296,13 +277657,21 @@ class ConnectionController {
277296
277657
  }))
277297
277658
  };
277298
277659
  } catch (error) {
277299
- const errorMessage = error instanceof Error ? error.message : typeof error === "string" ? error : JSON.stringify(error);
277660
+ const classified = classifyDriverFailure(error);
277661
+ if (!(classified instanceof ConnectionError)) {
277662
+ logger.warn("table not resolvable", {
277663
+ tableKey,
277664
+ tablePath,
277665
+ reason: classified.constructor.name
277666
+ });
277667
+ throw classified;
277668
+ }
277300
277669
  logger.error("fetchTableSchema error", {
277301
277670
  error,
277302
277671
  tableKey,
277303
277672
  tablePath
277304
277673
  });
277305
- throw new ConnectionError(errorMessage);
277674
+ throw classified;
277306
277675
  }
277307
277676
  }
277308
277677
  async getConnection(environmentName, connectionName) {
@@ -277336,13 +277705,13 @@ class ConnectionController {
277336
277705
  selectStr: sqlStatement
277337
277706
  });
277338
277707
  if (typeof schema === "string") {
277339
- throw new ConnectionError(schema);
277708
+ throw driverErrorToPublisherError(schema);
277340
277709
  }
277341
277710
  return {
277342
277711
  source: JSON.stringify(schema)
277343
277712
  };
277344
277713
  } catch (error) {
277345
- throw new ConnectionError(error.message);
277714
+ throw classifyDriverFailure(error);
277346
277715
  }
277347
277716
  }
277348
277717
  async getTable(environmentName, connectionName, schemaName, tablePath, packageName) {
@@ -288124,19 +288493,34 @@ init_model();
288124
288493
  init_errors();
288125
288494
  var PERSIST_LINE_PATTERN = /^\s*#@\s+persist\b/;
288126
288495
  var UNQUOTED_NAME_PATTERN = /(?<![.\w])name\s*=\s*(?!["'])/;
288496
+ var QUOTED_NAME_VALUE_PATTERN = /(?<![.\w])name\s*=\s*(["'])(.*?)\1/g;
288497
+ var SAFE_NAME_PATH = /^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$/;
288127
288498
  function assertPersistNamesQuoted(modelSource, modelPath) {
288128
- const offenders = [];
288499
+ const unquoted = [];
288500
+ const unsafe = [];
288129
288501
  for (const rawLine of modelSource.split(`
288130
288502
  `)) {
288131
288503
  if (!PERSIST_LINE_PATTERN.test(rawLine))
288132
288504
  continue;
288133
288505
  if (UNQUOTED_NAME_PATTERN.test(rawLine)) {
288134
- offenders.push(rawLine.trim());
288506
+ unquoted.push(rawLine.trim());
288507
+ continue;
288508
+ }
288509
+ for (const match of rawLine.matchAll(QUOTED_NAME_VALUE_PATTERN)) {
288510
+ if (!SAFE_NAME_PATH.test(match[2])) {
288511
+ unsafe.push(rawLine.trim());
288512
+ break;
288513
+ }
288135
288514
  }
288136
288515
  }
288137
- if (offenders.length > 0) {
288516
+ if (unquoted.length > 0) {
288138
288517
  throw new ModelCompilationError({
288139
- message: `${modelPath}: persist annotation name must be quoted. Write a quoted ` + `value like name="engaged_events" (or a dialect table path such as ` + `name="my_dataset.engaged_events"), not a bare value an unquoted ` + `persist name is dropped from the build plan, so the source would ` + `publish but never materialize. Offending annotation(s): ` + `${offenders.join("; ")}.`
288518
+ message: `${modelPath}: persist annotation name must be quoted. Write a quoted ` + `value like name="engaged_events" (or a dialect table path such as ` + `name="my_dataset.engaged_events"), not a bare value -- an unquoted ` + `persist name is dropped from the build plan, so the source would ` + `publish but never materialize. Offending annotation(s): ` + `${unquoted.join("; ")}.`
288519
+ });
288520
+ }
288521
+ if (unsafe.length > 0) {
288522
+ throw new ModelCompilationError({
288523
+ message: `${modelPath}: persist annotation name must be a plain identifier path -- ` + `dot-separated segments of letters, digits, underscores, and hyphens ` + `(e.g. name="engaged_events", name="my_dataset.engaged_events", or a ` + `hyphenated container path name="my-proj.mydataset.engaged_events"). ` + `The name is inlined into the materialization table DDL, so a value ` + `containing a quote, backtick, semicolon, or space is rejected. ` + `Offending annotation(s): ${unsafe.join("; ")}.`
288140
288524
  });
288141
288525
  }
288142
288526
  }
@@ -290288,8 +290672,8 @@ ${source}` : source ?? "";
290288
290672
  }
290289
290673
  async fetchManifestEntriesWithTimeout(manifestLocation) {
290290
290674
  let timer;
290291
- const timeout = new Promise((_, reject) => {
290292
- timer = setTimeout(() => reject(new Error(`Timed out after ${MANIFEST_FETCH_TIMEOUT_MS}ms fetching manifest ${manifestLocation}`)), MANIFEST_FETCH_TIMEOUT_MS);
290675
+ const timeout = new Promise((_, reject2) => {
290676
+ timer = setTimeout(() => reject2(new Error(`Timed out after ${MANIFEST_FETCH_TIMEOUT_MS}ms fetching manifest ${manifestLocation}`)), MANIFEST_FETCH_TIMEOUT_MS);
290293
290677
  });
290294
290678
  try {
290295
290679
  return await Promise.race([
@@ -291851,8 +292235,8 @@ class EnvironmentStore {
291851
292235
  }
291852
292236
  const file = fs9.createWriteStream(zipFilePath);
291853
292237
  item.Body.transformToWebStream().pipeTo(Writable.toWeb(file));
291854
- await new Promise((resolve5, reject) => {
291855
- file.on("error", reject);
292238
+ await new Promise((resolve5, reject2) => {
292239
+ file.on("error", reject2);
291856
292240
  file.on("finish", resolve5);
291857
292241
  });
291858
292242
  await this.unzipEnvironment(zipFilePath);
@@ -291891,8 +292275,8 @@ class EnvironmentStore {
291891
292275
  }
291892
292276
  const file = fs9.createWriteStream(absoluteFilePath);
291893
292277
  item.Body.transformToWebStream().pipeTo(Writable.toWeb(file));
291894
- await new Promise((resolve5, reject) => {
291895
- file.on("error", reject);
292278
+ await new Promise((resolve5, reject2) => {
292279
+ file.on("error", reject2);
291896
292280
  file.on("finish", resolve5);
291897
292281
  });
291898
292282
  }));
@@ -291913,7 +292297,7 @@ class EnvironmentStore {
291913
292297
  await fs9.promises.mkdir(absoluteDirPath, { recursive: true });
291914
292298
  const repoUrl = `https://github.com/${owner}/${repoName}`;
291915
292299
  const reporter = new CloneProgressReporter(cloneProgressLabel(`${owner}/${repoName}`, progressContext));
291916
- await new Promise((resolve5, reject) => {
292300
+ await new Promise((resolve5, reject2) => {
291917
292301
  esm_default2({
291918
292302
  progress: (event) => reporter.onProgress(event)
291919
292303
  }).clone(repoUrl, absoluteDirPath, GIT_CLONE_OPTIONS, (err) => {
@@ -291925,7 +292309,7 @@ class EnvironmentStore {
291925
292309
  }
291926
292310
  const errorData = this.extractErrorDataFromError(err);
291927
292311
  logger.error(`Failed to clone GitHub repository "${repoUrl}"`, errorData);
291928
- reject(err);
292312
+ reject2(err);
291929
292313
  return;
291930
292314
  }
291931
292315
  resolve5();
@@ -292708,10 +293092,10 @@ class Protocol {
292708
293092
  }
292709
293093
  request(request, resultSchema, options) {
292710
293094
  const { relatedRequestId, resumptionToken, onresumptiontoken } = options !== null && options !== undefined ? options : {};
292711
- return new Promise((resolve5, reject) => {
293095
+ return new Promise((resolve5, reject2) => {
292712
293096
  var _a2, _b, _c, _d, _e, _f;
292713
293097
  if (!this._transport) {
292714
- reject(new Error("Not connected"));
293098
+ reject2(new Error("Not connected"));
292715
293099
  return;
292716
293100
  }
292717
293101
  if (((_a2 = this._options) === null || _a2 === undefined ? undefined : _a2.enforceStrictCapabilities) === true) {
@@ -292747,7 +293131,7 @@ class Protocol {
292747
293131
  reason: String(reason)
292748
293132
  }
292749
293133
  }, { relatedRequestId, resumptionToken, onresumptiontoken }).catch((error) => this._onerror(new Error(`Failed to send cancellation: ${error}`)));
292750
- reject(reason);
293134
+ reject2(reason);
292751
293135
  };
292752
293136
  this._responseHandlers.set(messageId, (response) => {
292753
293137
  var _a3;
@@ -292755,13 +293139,13 @@ class Protocol {
292755
293139
  return;
292756
293140
  }
292757
293141
  if (response instanceof Error) {
292758
- return reject(response);
293142
+ return reject2(response);
292759
293143
  }
292760
293144
  try {
292761
293145
  const result = resultSchema.parse(response.result);
292762
293146
  resolve5(result);
292763
293147
  } catch (error) {
292764
- reject(error);
293148
+ reject2(error);
292765
293149
  }
292766
293150
  });
292767
293151
  (_d = options === null || options === undefined ? undefined : options.signal) === null || _d === undefined || _d.addEventListener("abort", () => {
@@ -292773,7 +293157,7 @@ class Protocol {
292773
293157
  this._setupTimeout(messageId, timeout, options === null || options === undefined ? undefined : options.maxTotalTimeout, timeoutHandler, (_f = options === null || options === undefined ? undefined : options.resetTimeoutOnProgress) !== null && _f !== undefined ? _f : false);
292774
293158
  this._transport.send(jsonrpcRequest, { relatedRequestId, resumptionToken, onresumptiontoken }).catch((error) => {
292775
293159
  this._cleanupTimeout(messageId);
292776
- reject(error);
293160
+ reject2(error);
292777
293161
  });
292778
293162
  });
292779
293163
  }
@@ -306222,10 +306606,10 @@ var promisifyStore = (passedStore) => {
306222
306606
 
306223
306607
  class PromisifiedStore {
306224
306608
  async increment(key) {
306225
- return new Promise((resolve6, reject) => {
306609
+ return new Promise((resolve6, reject2) => {
306226
306610
  legacyStore.incr(key, (error, totalHits, resetTime) => {
306227
306611
  if (error)
306228
- reject(error);
306612
+ reject2(error);
306229
306613
  resolve6({ totalHits, resetTime });
306230
306614
  });
306231
306615
  });