@malloy-publisher/server 0.2.4 → 0.2.5

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
@@ -251802,6 +251802,118 @@ var init_gate_registry_walk = __esm(() => {
251802
251802
  init_authorize();
251803
251803
  });
251804
251804
 
251805
+ // src/service/partition_annotation.ts
251806
+ import { payloadOf as payloadOf2, routeOf as routeOf2 } from "@malloydata/malloy";
251807
+ function noteRoute2(text) {
251808
+ return routeOf2({ value: text.trimStart() });
251809
+ }
251810
+ function containsPartitionAnnotationTag(texts) {
251811
+ return texts.some((text) => noteRoute2(text) === PARTITION_ROUTE);
251812
+ }
251813
+ function reachesPartitionTagBelow(node, seen = new WeakSet, depth = 0) {
251814
+ if (depth > MAX_PARTITION_IR_WALK_DEPTH) {
251815
+ throw new Error("partition-marker IR walk exceeded max depth");
251816
+ }
251817
+ if (node === null || typeof node !== "object")
251818
+ return false;
251819
+ if (seen.has(node))
251820
+ return false;
251821
+ seen.add(node);
251822
+ if (Array.isArray(node)) {
251823
+ return node.some((item) => reachesPartitionTagBelow(item, seen, depth + 1));
251824
+ }
251825
+ const record = node;
251826
+ if (depth > 0 && record.join !== undefined)
251827
+ return false;
251828
+ if (depth > 0) {
251829
+ for (const key of ["blockNotes", "notes"]) {
251830
+ const arr = record[key];
251831
+ if (!Array.isArray(arr))
251832
+ continue;
251833
+ const texts = arr.map((n) => typeof n === "string" ? n : n && typeof n === "object" && typeof n.text === "string" ? n.text : undefined).filter((text) => text !== undefined);
251834
+ if (containsPartitionAnnotationTag(texts))
251835
+ return true;
251836
+ }
251837
+ }
251838
+ return Object.entries(record).some(([key, value]) => depth === 0 && key === "annotations" ? false : reachesPartitionTagBelow(value, seen, depth + 1));
251839
+ }
251840
+ function notePayload2(text) {
251841
+ return payloadOf2({ value: text.trimStart() }) ?? "";
251842
+ }
251843
+ function rejectionMessage(sourceName, body, detail) {
251844
+ return `Source "${sourceName}" declares \`#(partition) ${body}\`: ${detail} ` + `#(partition) only accepts \`<column> = $GIVEN\`, where <column> is a ` + `single field or a dotted join path.`;
251845
+ }
251846
+ function reject(sourceName, body, cause, detail) {
251847
+ throw new PartitionAnnotationError(cause, rejectionMessage(sourceName, body, detail));
251848
+ }
251849
+ function parsePartitionAnnotation(sourceName, annotationText) {
251850
+ if (noteRoute2(annotationText) !== PARTITION_ROUTE)
251851
+ return null;
251852
+ const body = notePayload2(annotationText).trim();
251853
+ if (body.length === 0) {
251854
+ reject(sourceName, body, "empty_body", "the expression body is empty.");
251855
+ }
251856
+ if (COMPOUND_BOOLEAN_RE.test(body)) {
251857
+ reject(sourceName, body, "compound_boolean", "a compound boolean (`and`/`or`/`not`) is not allowed — declare one " + "`#(partition)` marker per column.");
251858
+ }
251859
+ if (IN_OPERATOR_RE.test(body)) {
251860
+ reject(sourceName, body, "in_operator", "the `in` operator is not allowed.");
251861
+ }
251862
+ if (NEGATED_OPERATOR_RE.test(body)) {
251863
+ reject(sourceName, body, "negated_operator", "`!=` is not allowed.");
251864
+ }
251865
+ if (COMPARISON_OPERATOR_RE.test(body)) {
251866
+ reject(sourceName, body, "comparison_operator", "only `=` is allowed, not `<`/`>`/`<=`/`>=`.");
251867
+ }
251868
+ const eq = body.indexOf("=");
251869
+ if (eq === -1) {
251870
+ reject(sourceName, body, "malformed_body", "no `=` was found.");
251871
+ }
251872
+ const left = body.slice(0, eq).trim();
251873
+ const right = body.slice(eq + 1).trim();
251874
+ if (!FIELD_PATH_RE.test(left)) {
251875
+ 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.");
251876
+ }
251877
+ const givenMatch = GIVEN_REF_RE.exec(right);
251878
+ if (!givenMatch) {
251879
+ reject(sourceName, body, "missing_given_reference", `\`${right}\` is not a given reference — the right side must be ` + "`$NAME`.");
251880
+ }
251881
+ return { column: left, given: givenMatch[1] };
251882
+ }
251883
+ function collectPartitionPairs(sourceName, annotationTexts2) {
251884
+ const pairs = [];
251885
+ const seenGivens = new Map;
251886
+ for (const text of annotationTexts2) {
251887
+ const pair = parsePartitionAnnotation(sourceName, text);
251888
+ if (pair === null)
251889
+ continue;
251890
+ const priorColumn = seenGivens.get(pair.given);
251891
+ if (priorColumn !== undefined) {
251892
+ 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.`);
251893
+ }
251894
+ seenGivens.set(pair.given, pair.column);
251895
+ pairs.push(pair);
251896
+ }
251897
+ return pairs;
251898
+ }
251899
+ 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;
251900
+ var init_partition_annotation = __esm(() => {
251901
+ init_errors();
251902
+ PartitionAnnotationError = class PartitionAnnotationError extends ModelCompilationError {
251903
+ rejectionCause;
251904
+ constructor(rejectionCause, message) {
251905
+ super({ message });
251906
+ this.rejectionCause = rejectionCause;
251907
+ }
251908
+ };
251909
+ FIELD_PATH_RE = new RegExp(`^${IDENT}(?:\\.${IDENT})*$`);
251910
+ GIVEN_REF_RE = new RegExp(`^\\$(${IDENT})$`);
251911
+ COMPOUND_BOOLEAN_RE = /\b(and|or|not)\b/i;
251912
+ IN_OPERATOR_RE = /\bin\b/i;
251913
+ NEGATED_OPERATOR_RE = /!=/;
251914
+ COMPARISON_OPERATOR_RE = /(>=|<=|>|<)/;
251915
+ });
251916
+
251805
251917
  // src/service/gate_classification.ts
251806
251918
  import {
251807
251919
  isSourceDef as isSourceDef4
@@ -252032,12 +252144,142 @@ function isBareFalseLiteral(expr) {
252032
252144
  function computeGivenDeclaredTypes(givens) {
252033
252145
  return new Map((givens ?? []).filter((g) => g.name != null && g.type != null).map((g) => [g.name, g.type]));
252034
252146
  }
252147
+ function resolveEntryPointPartitions(struct, modelDef, seen = new Set) {
252148
+ if (!struct || !modelDef || seen.has(struct))
252149
+ return [];
252150
+ seen.add(struct);
252151
+ const label = struct.as ?? struct.name;
252152
+ const own = collectPartitionPairs(label, ownLevelNotes(struct.annotations).map((note) => note.text));
252153
+ if (own.length > 0)
252154
+ return own;
252155
+ let inherited = struct.annotations?.inherits;
252156
+ for (let depth = 0;inherited && depth < ANCESTOR_WALK_MAX_DEPTH; depth++) {
252157
+ const pairs = collectPartitionPairs(label, ownLevelNoteTexts(inherited));
252158
+ if (pairs.length > 0)
252159
+ return pairs;
252160
+ inherited = inherited.inherits;
252161
+ }
252162
+ if (inherited)
252163
+ throw unresolvableAncestry(label);
252164
+ const declared = resolveDeclaredSource(struct, modelDef);
252165
+ if (declared.kind === "unresolvable")
252166
+ throw unresolvableAncestry(label);
252167
+ if (declared.kind === "resolved") {
252168
+ const pairs = resolveEntryPointPartitions(declared.source, modelDef, seen);
252169
+ if (pairs.length > 0)
252170
+ return pairs;
252171
+ }
252172
+ const queryBase = resolveQuerySourceBase(struct, modelDef);
252173
+ if (queryBase)
252174
+ return resolveEntryPointPartitions(queryBase, modelDef, seen);
252175
+ return [];
252176
+ }
252177
+ function unresolvableAncestry(label) {
252178
+ 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.`);
252179
+ }
252180
+ function assertPartitionAnnotationsValid(modelDef) {
252181
+ if (!modelDef)
252182
+ return;
252183
+ for (const [key, obj] of Object.entries(modelDef.contents)) {
252184
+ if (!isSourceDef4(obj))
252185
+ continue;
252186
+ const label = obj.as ?? obj.name ?? key;
252187
+ if (obj.type !== "composite") {
252188
+ let resolved = [];
252189
+ try {
252190
+ resolved = resolveEntryPointPartitions(obj, modelDef);
252191
+ } catch (err) {
252192
+ if (!(err instanceof PartitionAnnotationError) || err.rejectionCause !== "ancestry_unresolvable") {
252193
+ throw err;
252194
+ }
252195
+ continue;
252196
+ }
252197
+ assertNoUnreachableMarker(obj, label, resolved);
252198
+ continue;
252199
+ }
252200
+ if (resolveEntryPointPartitions(obj, modelDef).length > 0) {
252201
+ 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.`);
252202
+ }
252203
+ const marked = partitionedMemberLabel(obj, modelDef);
252204
+ if (marked !== undefined) {
252205
+ 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.`);
252206
+ }
252207
+ }
252208
+ }
252209
+ function assertNoUnreachableMarker(struct, label, resolved) {
252210
+ if (resolved.length > 0)
252211
+ return;
252212
+ let reachesBelow;
252213
+ try {
252214
+ reachesBelow = reachesPartitionTagBelow(struct);
252215
+ } catch {
252216
+ reachesBelow = true;
252217
+ }
252218
+ if (!reachesBelow)
252219
+ return;
252220
+ 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.`);
252221
+ }
252222
+ function partitionedMemberLabel(composite, modelDef, seen = new Set) {
252223
+ if (seen.has(composite))
252224
+ return;
252225
+ seen.add(composite);
252226
+ for (const member of composite.sources ?? []) {
252227
+ const label = member.as ?? member.name;
252228
+ if (resolveEntryPointPartitions(member, modelDef).length > 0) {
252229
+ return label;
252230
+ }
252231
+ if (member.type === "composite") {
252232
+ const nested = partitionedMemberLabel(member, modelDef, seen);
252233
+ if (nested !== undefined)
252234
+ return nested;
252235
+ }
252236
+ }
252237
+ return;
252238
+ }
252239
+ async function resolvePartitionGraftEntries(struct, originModelDef, graftScope, deps) {
252240
+ if (!struct || !originModelDef)
252241
+ return [];
252242
+ const pairs = resolveEntryPointPartitions(struct, originModelDef);
252243
+ if (pairs.length === 0)
252244
+ return [];
252245
+ const label = struct.as ?? struct.name;
252246
+ if (!graftScope) {
252247
+ logger.debug("Partition filter has no graft scope to attach to; denying", { modelPath: deps.modelPath, label });
252248
+ throw new Error(`partition on "${label}" has no graft scope`);
252249
+ }
252250
+ const graftTarget = resolveGraftTarget(struct, originModelDef, graftScope.modelDef);
252251
+ if (!graftTarget) {
252252
+ logger.debug("Partition filter resolved to no graft target; denying", {
252253
+ modelPath: deps.modelPath,
252254
+ label
252255
+ });
252256
+ throw new Error(`partition on "${label}" resolved to no graft target`);
252257
+ }
252258
+ const entries = [];
252259
+ for (const pair of pairs) {
252260
+ if (!deps.givenDeclaredTypes.has(pair.given)) {
252261
+ logger.warn("Partition references a given off the model surface; denying", { modelPath: deps.modelPath, graftTarget, givenName: pair.given });
252262
+ throw new Error(`partition on "${label}" references \`$${pair.given}\`, which is not on this model's given surface`);
252263
+ }
252264
+ const filterText = `${pair.column} = $${pair.given}`;
252265
+ const condition = await liftGateCondition(graftTarget, filterText, graftScope.materializer);
252266
+ entries.push({
252267
+ label,
252268
+ graftTarget,
252269
+ filterText,
252270
+ condition,
252271
+ givenNames: [pair.given]
252272
+ });
252273
+ }
252274
+ return entries;
252275
+ }
252035
252276
  var init_gate_classification = __esm(() => {
252036
252277
  init_logger();
252037
252278
  init_annotations();
252038
252279
  init_authorize();
252039
252280
  init_gate_dimension();
252040
252281
  init_gate_registry_walk();
252282
+ init_partition_annotation();
252041
252283
  });
252042
252284
 
252043
252285
  // src/service/incremental_declaration.ts
@@ -252234,8 +252476,24 @@ function assertMaterializationEligible(persistSource) {
252234
252476
  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
252477
  });
252236
252478
  }
252479
+ if (referencesPartition(persistSource)) {
252480
+ recordEligibilityRefused("partition");
252481
+ throw new MaterializationEligibilityError({
252482
+ reason: "partition",
252483
+ 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=').`
252484
+ });
252485
+ }
252237
252486
  }
252238
252487
  function assertColocatedPersistNotAuthorizeGated(persistSource, sourceName = persistSource.name, origin2 = "persist", gateOutcome) {
252488
+ if (referencesPartition(persistSource)) {
252489
+ recordEligibilityRefused("partition");
252490
+ const what2 = origin2 === "preaggregate" ? `Pre-aggregation rollup '${sourceName}'` : `Source '${sourceName}'`;
252491
+ const gated2 = origin2 === "preaggregate" ? `the source '${sourceName}' rolls up declares` : `it declares`;
252492
+ throw new MaterializationEligibilityError({
252493
+ reason: "partition",
252494
+ 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.`
252495
+ });
252496
+ }
252239
252497
  if (!referencesAuthorize(persistSource))
252240
252498
  return;
252241
252499
  if (origin2 === "persist" && gateOutcome?.classification === "row_level" && gateOutcome.attributed) {
@@ -252311,6 +252569,44 @@ function walkForGiven(node, seen, depth) {
252311
252569
  }
252312
252570
  return false;
252313
252571
  }
252572
+ function referencesPartition(persistSource) {
252573
+ try {
252574
+ return walkForPartition(persistSource._sourceDef, new WeakSet, 0);
252575
+ } catch {
252576
+ return true;
252577
+ }
252578
+ }
252579
+ function walkForPartition(node, seen, depth) {
252580
+ if (depth > MAX_GIVEN_WALK_DEPTH) {
252581
+ throw new Error("partition-usage walk exceeded max depth");
252582
+ }
252583
+ if (node === null || typeof node !== "object")
252584
+ return false;
252585
+ if (seen.has(node))
252586
+ return false;
252587
+ seen.add(node);
252588
+ if (Array.isArray(node)) {
252589
+ for (const item of node) {
252590
+ if (walkForPartition(item, seen, depth + 1))
252591
+ return true;
252592
+ }
252593
+ return false;
252594
+ }
252595
+ const record = node;
252596
+ for (const key of ["blockNotes", "notes"]) {
252597
+ const arr = record[key];
252598
+ if (!Array.isArray(arr))
252599
+ continue;
252600
+ const texts = arr.map((n) => typeof n === "string" ? n : n && typeof n === "object" && typeof n.text === "string" ? n.text : undefined).filter((text) => text !== undefined);
252601
+ if (containsPartitionAnnotationTag(texts))
252602
+ return true;
252603
+ }
252604
+ for (const value of Object.values(record)) {
252605
+ if (walkForPartition(value, seen, depth + 1))
252606
+ return true;
252607
+ }
252608
+ return false;
252609
+ }
252314
252610
  function referencesAuthorize(persistSource) {
252315
252611
  try {
252316
252612
  return walkForAuthorize(persistSource._sourceDef, new WeakSet, 0);
@@ -252411,6 +252707,7 @@ var init_materialization_eligibility = __esm(() => {
252411
252707
  init_errors();
252412
252708
  init_materialization_metrics();
252413
252709
  init_authorize();
252710
+ init_partition_annotation();
252414
252711
  });
252415
252712
 
252416
252713
  // src/service/preaggregation_compile.ts
@@ -253865,6 +254162,7 @@ var init_model = __esm(() => {
253865
254162
  init_query_metadata();
253866
254163
  init_preaggregation_validation();
253867
254164
  init_gate_registry_walk();
254165
+ init_partition_annotation();
253868
254166
  init_gate_classification();
253869
254167
  init_source_extraction();
253870
254168
  init_authorize_metrics();
@@ -254060,20 +254358,7 @@ var init_model = __esm(() => {
254060
254358
  if (!modelDef)
254061
254359
  return false;
254062
254360
  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) {
254361
+ for (const struct of this.reachableStructsForNoteSweep(modelDef)) {
254077
254362
  if (containsAuthorizeAnnotationTag(annotationTexts(struct.annotations) ?? [])) {
254078
254363
  return true;
254079
254364
  }
@@ -254090,6 +254375,43 @@ var init_model = __esm(() => {
254090
254375
  })();
254091
254376
  return this.anyAuthorizeNote;
254092
254377
  }
254378
+ reachableStructsForNoteSweep(modelDef) {
254379
+ const structs = [];
254380
+ for (const obj of Object.values(modelDef.contents)) {
254381
+ if (isSourceDef6(obj))
254382
+ structs.push(obj);
254383
+ }
254384
+ for (const value of Object.values(modelDef.sourceRegistry ?? {})) {
254385
+ const entry = value.entry;
254386
+ if (entry.type === "source_registry_reference")
254387
+ continue;
254388
+ if (isSourceDef6(entry))
254389
+ structs.push(entry);
254390
+ }
254391
+ structs.push(...derivedStructsReachable(structs, modelDef));
254392
+ return structs;
254393
+ }
254394
+ anyPartitionNote;
254395
+ hasAnyPartitionNote() {
254396
+ if (this.anyPartitionNote !== undefined)
254397
+ return this.anyPartitionNote;
254398
+ this.anyPartitionNote = (() => {
254399
+ const modelDef = this.modelDef;
254400
+ if (!modelDef)
254401
+ return false;
254402
+ try {
254403
+ for (const struct of this.reachableStructsForNoteSweep(modelDef)) {
254404
+ if (containsPartitionAnnotationTag(annotationTexts(struct.annotations) ?? [])) {
254405
+ return true;
254406
+ }
254407
+ }
254408
+ return false;
254409
+ } catch {
254410
+ return true;
254411
+ }
254412
+ })();
254413
+ return this.anyPartitionNote;
254414
+ }
254093
254415
  async assertAuthorized(sourceName, _givens, bypassAuthorize = false, graftScope = this.defaultGraftScope()) {
254094
254416
  if (bypassAuthorize) {
254095
254417
  this.noteAuthorizeBypass("source", sourceName);
@@ -254146,7 +254468,7 @@ var init_model = __esm(() => {
254146
254468
  entryPointGates = Array.from(byKey.values());
254147
254469
  }
254148
254470
  }
254149
- return { entryPointGates, modelDef };
254471
+ return { entryPointGates, modelDef, struct };
254150
254472
  }
254151
254473
  async assertAuthorizedFromCompiledRunnable(runnable, givens) {
254152
254474
  await this.authorizeAndBindRunnable(runnable, givens, {
@@ -254158,7 +254480,7 @@ var init_model = __esm(() => {
254158
254480
  return this.rowLevelFilteredRunnables.has(runnable);
254159
254481
  }
254160
254482
  async probeEntryPointGates(runnable, givens, graftScope, skipOwnSourceGate = false) {
254161
- const { entryPointGates, modelDef } = await this.collectAuthorizeEntryPointGates(runnable, givens, graftScope, skipOwnSourceGate);
254483
+ const { entryPointGates, modelDef, struct } = await this.collectAuthorizeEntryPointGates(runnable, givens, graftScope, skipOwnSourceGate);
254162
254484
  const rowLevel = [];
254163
254485
  for (const entry of entryPointGates) {
254164
254486
  const resolution = modelDef ? await this.resolveGateShape(entry, modelDef, graftScope) : { shape: "rejected", cause: undefined };
@@ -254177,6 +254499,16 @@ var init_model = __esm(() => {
254177
254499
  recordRowLevelGateRejected(resolution.cause);
254178
254500
  throw new AccessDeniedError(`Access denied for source "${entry.label}".`);
254179
254501
  }
254502
+ try {
254503
+ rowLevel.push(...await resolvePartitionGraftEntries(struct, modelDef, graftScope, this.gateClassificationDeps()));
254504
+ } catch (err) {
254505
+ recordRowLevelGateDecision("denied_by_gate");
254506
+ logger.debug("Partition filter could not be resolved; denying", {
254507
+ modelPath: this.modelPath,
254508
+ error: err instanceof Error ? err.message : String(err)
254509
+ });
254510
+ throw new AccessDeniedError(`Access denied for source "${struct?.as ?? struct?.name ?? "unknown"}".`);
254511
+ }
254180
254512
  return rowLevel;
254181
254513
  }
254182
254514
  async queryEntryPointHasRowLevelGate(runnable) {
@@ -254189,7 +254521,7 @@ var init_model = __esm(() => {
254189
254521
  if (compositeResolvedSourceDef) {
254190
254522
  gates.push(...this.collectEntryPointGates(compositeResolvedSourceDef, modelDef, seen, true, undefined, struct ? ownLevelNotes(struct.annotations) : []));
254191
254523
  }
254192
- return gates.length > 0;
254524
+ return gates.length > 0 || resolveEntryPointPartitions(struct, modelDef).length > 0;
254193
254525
  } catch {
254194
254526
  return true;
254195
254527
  }
@@ -254520,6 +254852,7 @@ var init_model = __esm(() => {
254520
254852
  filterMap = sourceResult.filterMap;
254521
254853
  const queryResult = Model.getQueries(modelDef);
254522
254854
  queries = queryResult.queries;
254855
+ assertPartitionAnnotationsValid(modelDef);
254523
254856
  assertNoMisplacedAuthorizeAnnotations([
254524
254857
  ...sourceResult.misplacedAuthorize,
254525
254858
  ...queryResult.misplacedAuthorize
@@ -255147,7 +255480,7 @@ run: ${sourceName ? `${quoteMalloyIdentifier2(sourceName)} -> ` : ""}${quoteMall
255147
255480
  liveRunnable = runnable;
255148
255481
  const storageRoutingPossible = getPersistStorageMode() === "on" && this.serveBindings.length > 0 && !!this.serveDestinationConfig;
255149
255482
  const preaggServeMaterializer = this.preaggregateServeMaterializer;
255150
- const routingBlockedByRowLevelGate = (storageRoutingPossible || !!preaggServeMaterializer) && !bypassAuthorize && this.hasAnyAuthorizeNote() && await this.queryEntryPointHasRowLevelGate(runnable);
255483
+ const routingBlockedByRowLevelGate = (storageRoutingPossible || !!preaggServeMaterializer) && !bypassAuthorize && (this.hasAnyAuthorizeNote() || this.hasAnyPartitionNote()) && await this.queryEntryPointHasRowLevelGate(runnable);
255151
255484
  if (routingBlockedByRowLevelGate) {
255152
255485
  recordStorageServeRouting("blocked_by_row_level_gate");
255153
255486
  }
@@ -256631,10 +256964,10 @@ var require_recursive_readdir = __commonJS((exports, module) => {
256631
256964
  ignores = [];
256632
256965
  }
256633
256966
  if (!callback) {
256634
- return new Promise(function(resolve4, reject) {
256967
+ return new Promise(function(resolve4, reject2) {
256635
256968
  readdir3(path9, ignores || [], function(err, data) {
256636
256969
  if (err) {
256637
- reject(err);
256970
+ reject2(err);
256638
256971
  } else {
256639
256972
  resolve4(data);
256640
256973
  }
@@ -288124,19 +288457,34 @@ init_model();
288124
288457
  init_errors();
288125
288458
  var PERSIST_LINE_PATTERN = /^\s*#@\s+persist\b/;
288126
288459
  var UNQUOTED_NAME_PATTERN = /(?<![.\w])name\s*=\s*(?!["'])/;
288460
+ var QUOTED_NAME_VALUE_PATTERN = /(?<![.\w])name\s*=\s*(["'])(.*?)\1/g;
288461
+ var SAFE_NAME_PATH = /^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$/;
288127
288462
  function assertPersistNamesQuoted(modelSource, modelPath) {
288128
- const offenders = [];
288463
+ const unquoted = [];
288464
+ const unsafe = [];
288129
288465
  for (const rawLine of modelSource.split(`
288130
288466
  `)) {
288131
288467
  if (!PERSIST_LINE_PATTERN.test(rawLine))
288132
288468
  continue;
288133
288469
  if (UNQUOTED_NAME_PATTERN.test(rawLine)) {
288134
- offenders.push(rawLine.trim());
288470
+ unquoted.push(rawLine.trim());
288471
+ continue;
288135
288472
  }
288473
+ for (const match of rawLine.matchAll(QUOTED_NAME_VALUE_PATTERN)) {
288474
+ if (!SAFE_NAME_PATH.test(match[2])) {
288475
+ unsafe.push(rawLine.trim());
288476
+ break;
288477
+ }
288478
+ }
288479
+ }
288480
+ if (unquoted.length > 0) {
288481
+ throw new ModelCompilationError({
288482
+ 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("; ")}.`
288483
+ });
288136
288484
  }
288137
- if (offenders.length > 0) {
288485
+ if (unsafe.length > 0) {
288138
288486
  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("; ")}.`
288487
+ 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
288488
  });
288141
288489
  }
288142
288490
  }
@@ -290288,8 +290636,8 @@ ${source}` : source ?? "";
290288
290636
  }
290289
290637
  async fetchManifestEntriesWithTimeout(manifestLocation) {
290290
290638
  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);
290639
+ const timeout = new Promise((_, reject2) => {
290640
+ timer = setTimeout(() => reject2(new Error(`Timed out after ${MANIFEST_FETCH_TIMEOUT_MS}ms fetching manifest ${manifestLocation}`)), MANIFEST_FETCH_TIMEOUT_MS);
290293
290641
  });
290294
290642
  try {
290295
290643
  return await Promise.race([
@@ -291851,8 +292199,8 @@ class EnvironmentStore {
291851
292199
  }
291852
292200
  const file = fs9.createWriteStream(zipFilePath);
291853
292201
  item.Body.transformToWebStream().pipeTo(Writable.toWeb(file));
291854
- await new Promise((resolve5, reject) => {
291855
- file.on("error", reject);
292202
+ await new Promise((resolve5, reject2) => {
292203
+ file.on("error", reject2);
291856
292204
  file.on("finish", resolve5);
291857
292205
  });
291858
292206
  await this.unzipEnvironment(zipFilePath);
@@ -291891,8 +292239,8 @@ class EnvironmentStore {
291891
292239
  }
291892
292240
  const file = fs9.createWriteStream(absoluteFilePath);
291893
292241
  item.Body.transformToWebStream().pipeTo(Writable.toWeb(file));
291894
- await new Promise((resolve5, reject) => {
291895
- file.on("error", reject);
292242
+ await new Promise((resolve5, reject2) => {
292243
+ file.on("error", reject2);
291896
292244
  file.on("finish", resolve5);
291897
292245
  });
291898
292246
  }));
@@ -291913,7 +292261,7 @@ class EnvironmentStore {
291913
292261
  await fs9.promises.mkdir(absoluteDirPath, { recursive: true });
291914
292262
  const repoUrl = `https://github.com/${owner}/${repoName}`;
291915
292263
  const reporter = new CloneProgressReporter(cloneProgressLabel(`${owner}/${repoName}`, progressContext));
291916
- await new Promise((resolve5, reject) => {
292264
+ await new Promise((resolve5, reject2) => {
291917
292265
  esm_default2({
291918
292266
  progress: (event) => reporter.onProgress(event)
291919
292267
  }).clone(repoUrl, absoluteDirPath, GIT_CLONE_OPTIONS, (err) => {
@@ -291925,7 +292273,7 @@ class EnvironmentStore {
291925
292273
  }
291926
292274
  const errorData = this.extractErrorDataFromError(err);
291927
292275
  logger.error(`Failed to clone GitHub repository "${repoUrl}"`, errorData);
291928
- reject(err);
292276
+ reject2(err);
291929
292277
  return;
291930
292278
  }
291931
292279
  resolve5();
@@ -292708,10 +293056,10 @@ class Protocol {
292708
293056
  }
292709
293057
  request(request, resultSchema, options) {
292710
293058
  const { relatedRequestId, resumptionToken, onresumptiontoken } = options !== null && options !== undefined ? options : {};
292711
- return new Promise((resolve5, reject) => {
293059
+ return new Promise((resolve5, reject2) => {
292712
293060
  var _a2, _b, _c, _d, _e, _f;
292713
293061
  if (!this._transport) {
292714
- reject(new Error("Not connected"));
293062
+ reject2(new Error("Not connected"));
292715
293063
  return;
292716
293064
  }
292717
293065
  if (((_a2 = this._options) === null || _a2 === undefined ? undefined : _a2.enforceStrictCapabilities) === true) {
@@ -292747,7 +293095,7 @@ class Protocol {
292747
293095
  reason: String(reason)
292748
293096
  }
292749
293097
  }, { relatedRequestId, resumptionToken, onresumptiontoken }).catch((error) => this._onerror(new Error(`Failed to send cancellation: ${error}`)));
292750
- reject(reason);
293098
+ reject2(reason);
292751
293099
  };
292752
293100
  this._responseHandlers.set(messageId, (response) => {
292753
293101
  var _a3;
@@ -292755,13 +293103,13 @@ class Protocol {
292755
293103
  return;
292756
293104
  }
292757
293105
  if (response instanceof Error) {
292758
- return reject(response);
293106
+ return reject2(response);
292759
293107
  }
292760
293108
  try {
292761
293109
  const result = resultSchema.parse(response.result);
292762
293110
  resolve5(result);
292763
293111
  } catch (error) {
292764
- reject(error);
293112
+ reject2(error);
292765
293113
  }
292766
293114
  });
292767
293115
  (_d = options === null || options === undefined ? undefined : options.signal) === null || _d === undefined || _d.addEventListener("abort", () => {
@@ -292773,7 +293121,7 @@ class Protocol {
292773
293121
  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
293122
  this._transport.send(jsonrpcRequest, { relatedRequestId, resumptionToken, onresumptiontoken }).catch((error) => {
292775
293123
  this._cleanupTimeout(messageId);
292776
- reject(error);
293124
+ reject2(error);
292777
293125
  });
292778
293126
  });
292779
293127
  }
@@ -306222,10 +306570,10 @@ var promisifyStore = (passedStore) => {
306222
306570
 
306223
306571
  class PromisifiedStore {
306224
306572
  async increment(key) {
306225
- return new Promise((resolve6, reject) => {
306573
+ return new Promise((resolve6, reject2) => {
306226
306574
  legacyStore.incr(key, (error, totalHits, resetTime) => {
306227
306575
  if (error)
306228
- reject(error);
306576
+ reject2(error);
306229
306577
  resolve6({ totalHits, resetTime });
306230
306578
  });
306231
306579
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@malloy-publisher/server",
3
3
  "description": "Malloy Publisher Server",
4
- "version": "0.2.4",
4
+ "version": "0.2.5",
5
5
  "main": "dist/server.mjs",
6
6
  "bin": {
7
7
  "malloy-publisher": "dist/server.mjs"