@malloy-publisher/server 0.0.209 → 0.0.211

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 (34) hide show
  1. package/dist/app/api-doc.yaml +20 -3
  2. package/dist/app/assets/{EnvironmentPage-BRMCY9d8.js → EnvironmentPage-CuO6sty5.js} +1 -1
  3. package/dist/app/assets/HomePage-u3vxROIF.js +1 -0
  4. package/dist/app/assets/{MainPage-sZdUjUcu.js → MainPage-Bt2sYLbr.js} +2 -2
  5. package/dist/app/assets/{MaterializationsPage-C_jQQUCJ.js → MaterializationsPage-B1rj61zs.js} +1 -1
  6. package/dist/app/assets/{ModelPage-Bh62OIEq.js → ModelPage-BpvONvSR.js} +1 -1
  7. package/dist/app/assets/{PackagePage-DJW4xjLp.js → PackagePage-DHNcwboW.js} +1 -1
  8. package/dist/app/assets/{RouteError-Vi5yGs_F.js → RouteError-D1vhLJvr.js} +1 -1
  9. package/dist/app/assets/{WorkbookPage-BvJMi21d.js → WorkbookPage-CZmud-yI.js} +1 -1
  10. package/dist/app/assets/{core-BbW0t3RQ.es-L-mZcOk3.js → core-XtBSnW2U.es-DE88sVsZ.js} +1 -1
  11. package/dist/app/assets/{index-CCcHdeew.js → index-BQdOB6m9.js} +1 -1
  12. package/dist/app/assets/{index-DuqTjxM_.js → index-BlnQtDZj.js} +137 -137
  13. package/dist/app/assets/{index-CNFX-CGL.js → index-CYf2akGH.js} +1 -1
  14. package/dist/app/assets/{index.umd-GgEb4WfT.js → index.umd-BdQ0R4hx.js} +1 -1
  15. package/dist/app/index.html +1 -1
  16. package/dist/package_load_worker.mjs +3 -0
  17. package/dist/server.mjs +314 -306
  18. package/package.json +1 -1
  19. package/src/materialization_metrics.spec.ts +84 -0
  20. package/src/materialization_metrics.ts +78 -119
  21. package/src/service/build_plan.spec.ts +84 -0
  22. package/src/service/build_plan.ts +53 -2
  23. package/src/service/materialization_service.spec.ts +126 -8
  24. package/src/service/materialization_service.ts +96 -83
  25. package/src/service/materialization_test_fixtures.ts +2 -0
  26. package/src/service/model.ts +25 -25
  27. package/src/service/package.ts +19 -7
  28. package/src/service/package_worker_path.spec.ts +65 -55
  29. package/src/service/persist_annotation_validation.spec.ts +77 -0
  30. package/src/service/persist_annotation_validation.ts +54 -0
  31. package/src/service/quoting.spec.ts +50 -0
  32. package/src/service/quoting.ts +37 -16
  33. package/src/utils.ts +5 -0
  34. package/dist/app/assets/HomePage-DQzgkiMI.js +0 -1
package/dist/server.mjs CHANGED
@@ -237699,97 +237699,52 @@ import { pathToFileURL as pathToFileURL2 } from "url";
237699
237699
 
237700
237700
  // src/materialization_metrics.ts
237701
237701
  var import_api4 = __toESM(require_src(), 1);
237702
- var runCounter = null;
237703
- var runDuration = null;
237704
- var sourcesCounter = null;
237705
- var buildPlanComputeDuration = null;
237706
- var autoLoadCounter = null;
237707
- var connectionDigestSkipCounter = null;
237708
- var manifestBindCounter = null;
237709
- var sourceBuildDuration = null;
237710
- var dropTablesCounter = null;
237711
- function ensureTelemetry() {
237712
- if (runCounter && runDuration) {
237713
- return { counter: runCounter, duration: runDuration };
237714
- }
237715
- const meter2 = import_api4.metrics.getMeter("publisher");
237716
- if (!runCounter) {
237717
- runCounter = meter2.createCounter("publisher_materialization_runs_total", {
237718
- description: "Materialization builds completed. Labels: mode ('auto'|'orchestrated'), outcome ('success'|'failed'|'cancelled')."
237719
- });
237720
- }
237721
- if (!runDuration) {
237722
- runDuration = meter2.createHistogram("publisher_materialization_run_duration_ms", {
237723
- description: "Wall-clock duration of a materialization build. Label: mode ('auto'|'orchestrated').",
237724
- unit: "ms"
237725
- });
237726
- }
237727
- return { counter: runCounter, duration: runDuration };
237728
- }
237702
+ var resetHooks = [];
237703
+ function lazyCounter(name, description) {
237704
+ let instrument2 = null;
237705
+ resetHooks.push(() => instrument2 = null);
237706
+ return () => instrument2 ??= import_api4.metrics.getMeter("publisher").createCounter(name, { description });
237707
+ }
237708
+ function lazyHistogram(name, description, unit) {
237709
+ let instrument2 = null;
237710
+ resetHooks.push(() => instrument2 = null);
237711
+ return () => instrument2 ??= import_api4.metrics.getMeter("publisher").createHistogram(name, { description, unit });
237712
+ }
237713
+ var runCounter = lazyCounter("publisher_materialization_runs_total", "Materialization builds completed. Labels: mode ('auto'|'orchestrated'), outcome ('success'|'failed'|'cancelled').");
237714
+ var runDuration = lazyHistogram("publisher_materialization_run_duration_ms", "Wall-clock duration of a materialization build. Label: mode ('auto'|'orchestrated').", "ms");
237715
+ var sourcesCounter = lazyCounter("publisher_materialization_sources_total", "Persist sources processed by a materialization run. Label: outcome ('built'|'reused').");
237716
+ var buildPlanComputeDuration = lazyHistogram("publisher_materialization_build_plan_compute_duration_ms", "Wall-clock duration of compiling a package's build plan (Package.buildPlan).", "ms");
237717
+ var autoLoadCounter = lazyCounter("publisher_materialization_auto_load_total", "Auto-run manifest auto-load attempts. Label: outcome ('success'|'failure').");
237718
+ var connectionDigestSkipCounter = lazyCounter("publisher_materialization_connection_digest_skipped_total", "Connection digests skipped during build-plan compile because the connection did not resolve.");
237719
+ var manifestBindCounter = lazyCounter("publisher_materialization_manifest_bind_total", "Manifest bind attempts. Label: outcome ('success'|'failure'|'timeout').");
237720
+ var sourceBuildDuration = lazyHistogram("publisher_materialization_source_build_duration_ms", "Wall-clock duration of building a single persist source.", "ms");
237721
+ var dropTablesCounter = lazyCounter("publisher_materialization_drop_tables_total", "Physical tables dropped on delete. Label: outcome ('success'|'failure').");
237729
237722
  function recordMaterializationRun(mode, outcome, durationMs) {
237730
- const { counter, duration } = ensureTelemetry();
237731
- counter.add(1, { mode, outcome });
237732
- duration.record(durationMs, { mode });
237723
+ runCounter().add(1, { mode, outcome });
237724
+ runDuration().record(durationMs, { mode });
237733
237725
  }
237734
237726
  function recordSourcesOutcome(outcome, count) {
237735
237727
  if (count <= 0)
237736
237728
  return;
237737
- if (!sourcesCounter) {
237738
- sourcesCounter = import_api4.metrics.getMeter("publisher").createCounter("publisher_materialization_sources_total", {
237739
- description: "Persist sources processed by a materialization run. Label: outcome ('built'|'reused')."
237740
- });
237741
- }
237742
- sourcesCounter.add(count, { outcome });
237729
+ sourcesCounter().add(count, { outcome });
237743
237730
  }
237744
237731
  function recordBuildPlanComputeDuration(durationMs) {
237745
- if (!buildPlanComputeDuration) {
237746
- buildPlanComputeDuration = import_api4.metrics.getMeter("publisher").createHistogram("publisher_materialization_build_plan_compute_duration_ms", {
237747
- description: "Wall-clock duration of compiling a package's build plan (Package.buildPlan).",
237748
- unit: "ms"
237749
- });
237750
- }
237751
- buildPlanComputeDuration.record(durationMs);
237732
+ buildPlanComputeDuration().record(durationMs);
237752
237733
  }
237753
237734
  function recordAutoLoadOutcome(outcome) {
237754
- if (!autoLoadCounter) {
237755
- autoLoadCounter = import_api4.metrics.getMeter("publisher").createCounter("publisher_materialization_auto_load_total", {
237756
- description: "Auto-run manifest auto-load attempts. Label: outcome ('success'|'failure')."
237757
- });
237758
- }
237759
- autoLoadCounter.add(1, { outcome });
237735
+ autoLoadCounter().add(1, { outcome });
237760
237736
  }
237761
237737
  function recordConnectionDigestSkipped() {
237762
- if (!connectionDigestSkipCounter) {
237763
- connectionDigestSkipCounter = import_api4.metrics.getMeter("publisher").createCounter("publisher_materialization_connection_digest_skipped_total", {
237764
- description: "Connection digests skipped during build-plan compile because the connection did not resolve."
237765
- });
237766
- }
237767
- connectionDigestSkipCounter.add(1);
237738
+ connectionDigestSkipCounter().add(1);
237768
237739
  }
237769
237740
  function recordManifestBind(outcome) {
237770
- if (!manifestBindCounter) {
237771
- manifestBindCounter = import_api4.metrics.getMeter("publisher").createCounter("publisher_materialization_manifest_bind_total", {
237772
- description: "Manifest bind attempts. Label: outcome ('success'|'failure'|'timeout')."
237773
- });
237774
- }
237775
- manifestBindCounter.add(1, { outcome });
237741
+ manifestBindCounter().add(1, { outcome });
237776
237742
  }
237777
237743
  function recordSourceBuildDuration(durationMs) {
237778
- if (!sourceBuildDuration) {
237779
- sourceBuildDuration = import_api4.metrics.getMeter("publisher").createHistogram("publisher_materialization_source_build_duration_ms", {
237780
- description: "Wall-clock duration of building a single persist source.",
237781
- unit: "ms"
237782
- });
237783
- }
237784
- sourceBuildDuration.record(durationMs);
237744
+ sourceBuildDuration().record(durationMs);
237785
237745
  }
237786
237746
  function recordDropTables(outcome) {
237787
- if (!dropTablesCounter) {
237788
- dropTablesCounter = import_api4.metrics.getMeter("publisher").createCounter("publisher_materialization_drop_tables_total", {
237789
- description: "Physical tables dropped on delete. Label: outcome ('success'|'failure')."
237790
- });
237791
- }
237792
- dropTablesCounter.add(1, { outcome });
237747
+ dropTablesCounter().add(1, { outcome });
237793
237748
  }
237794
237749
 
237795
237750
  // src/utils.ts
@@ -237808,6 +237763,9 @@ var URL_READER = {
237808
237763
  function ignoreDotfiles(file) {
237809
237764
  return path5.basename(file).startsWith(".");
237810
237765
  }
237766
+ function errMessage(err) {
237767
+ return err instanceof Error ? err.message : String(err);
237768
+ }
237811
237769
 
237812
237770
  // src/service/manifest_loader.ts
237813
237771
  init_logger();
@@ -237889,10 +237847,10 @@ import {
237889
237847
  } from "@malloydata/malloy";
237890
237848
 
237891
237849
  // src/service/build_plan.ts
237850
+ init_constants();
237892
237851
  init_logger();
237893
237852
 
237894
237853
  // src/service/model.ts
237895
- init_package_load_pool();
237896
237854
  var import_api5 = __toESM(require_src(), 1);
237897
237855
  import {
237898
237856
  Annotations as Annotations2,
@@ -237963,6 +237921,150 @@ class HackyDataStylesAccumulator {
237963
237921
  // src/service/model.ts
237964
237922
  init_errors();
237965
237923
  init_logger();
237924
+ init_package_load_pool();
237925
+
237926
+ // src/service/annotations.ts
237927
+ import { Annotations } from "@malloydata/malloy";
237928
+ function isReservedRoute(route) {
237929
+ return route === "" || !/[\p{L}\p{N}]/u.test(route);
237930
+ }
237931
+ function modelAnnotations(modelDef) {
237932
+ const registry = modelDef.modelAnnotations ?? {};
237933
+ const visited = new Set;
237934
+ const order = [];
237935
+ const visit = (id) => {
237936
+ if (visited.has(id))
237937
+ return;
237938
+ visited.add(id);
237939
+ const entry = registry[id];
237940
+ if (!entry)
237941
+ return;
237942
+ for (const dep of entry.inheritsFrom)
237943
+ visit(dep);
237944
+ order.push(id);
237945
+ };
237946
+ visit(modelDef.modelID);
237947
+ let folded;
237948
+ for (const id of order) {
237949
+ const own = registry[id].ownNotes;
237950
+ if (!own.notes?.length && !own.blockNotes?.length)
237951
+ continue;
237952
+ folded = {
237953
+ notes: own.notes,
237954
+ blockNotes: own.blockNotes,
237955
+ inherits: folded
237956
+ };
237957
+ }
237958
+ return folded ?? {};
237959
+ }
237960
+ function annotationTexts(annote) {
237961
+ const texts = new Annotations(annote).texts();
237962
+ return texts.length > 0 ? texts : undefined;
237963
+ }
237964
+
237965
+ // src/service/authorize.ts
237966
+ init_errors();
237967
+ var SOURCE_PREFIX = "#(authorize)";
237968
+ var FILE_PREFIX = "##(authorize)";
237969
+ function buildAuthorizeProbe(exprs) {
237970
+ const selects = exprs.map((expr, i) => `__auth_${i} is (${expr})`).join(`
237971
+ `);
237972
+ return `run: duckdb.sql("SELECT 1 AS __authorize_probe_row") -> {
237973
+ select:
237974
+ ${selects}
237975
+ limit: 1
237976
+ }`;
237977
+ }
237978
+ function isProbeTrue(cell) {
237979
+ return cell === true || cell === 1 || cell === "true";
237980
+ }
237981
+ async function evaluateAuthorize(executor, exprs, givens) {
237982
+ for (const expr of exprs) {
237983
+ try {
237984
+ const result = await executor.loadQuery(buildAuthorizeProbe([expr])).run({ rowLimit: 1, givens });
237985
+ const row = result?.data?.value?.[0];
237986
+ if (row && isProbeTrue(row.__auth_0)) {
237987
+ return true;
237988
+ }
237989
+ } catch {
237990
+ continue;
237991
+ }
237992
+ }
237993
+ return false;
237994
+ }
237995
+ async function validateAuthorizeProbes(compiler, sources) {
237996
+ for (const source of sources) {
237997
+ const exprs = source.authorize;
237998
+ if (!exprs || exprs.length === 0)
237999
+ continue;
238000
+ try {
238001
+ await compiler.loadQuery(buildAuthorizeProbe(exprs)).getPreparedQuery();
238002
+ } catch (err) {
238003
+ const detail = err instanceof Error ? err.message : String(err);
238004
+ throw new ModelCompilationError({
238005
+ message: `Invalid #(authorize) annotation on source "${source.name ?? "(unnamed)"}" [${exprs.join(" | ")}]: ${detail}`
238006
+ });
238007
+ }
238008
+ }
238009
+ }
238010
+ function parseAuthorizeAnnotation(annotation) {
238011
+ const trimmed2 = annotation.trim();
238012
+ let body;
238013
+ if (trimmed2.startsWith(FILE_PREFIX)) {
238014
+ body = trimmed2.slice(FILE_PREFIX.length).trim();
238015
+ } else if (trimmed2.startsWith(SOURCE_PREFIX)) {
238016
+ body = trimmed2.slice(SOURCE_PREFIX.length).trim();
238017
+ } else {
238018
+ return null;
238019
+ }
238020
+ return unwrapQuotedExpression(body);
238021
+ }
238022
+ function collectAuthorizeExprs(annotations) {
238023
+ const exprs = [];
238024
+ for (const annotation of annotations) {
238025
+ const expr = parseAuthorizeAnnotation(annotation);
238026
+ if (expr !== null) {
238027
+ exprs.push(expr);
238028
+ }
238029
+ }
238030
+ return exprs;
238031
+ }
238032
+ function unwrapQuotedExpression(body) {
238033
+ if (body.length < 2 || body[0] !== '"') {
238034
+ throw new Error(`authorize annotation expression must be a double-quoted string, got: ${body || "(empty)"}`);
238035
+ }
238036
+ let expr = "";
238037
+ let i = 1;
238038
+ let closed = false;
238039
+ for (;i < body.length; i++) {
238040
+ const ch = body[i];
238041
+ if (ch === "\\" && i + 1 < body.length) {
238042
+ const next = body[i + 1];
238043
+ if (next === '"' || next === "\\") {
238044
+ expr += next;
238045
+ i++;
238046
+ continue;
238047
+ }
238048
+ }
238049
+ if (ch === '"') {
238050
+ closed = true;
238051
+ i++;
238052
+ break;
238053
+ }
238054
+ expr += ch;
238055
+ }
238056
+ if (!closed) {
238057
+ throw new Error(`authorize annotation has mismatched quotes: ${body}`);
238058
+ }
238059
+ const rest = body.slice(i).trim();
238060
+ if (rest.length > 0) {
238061
+ throw new Error(`authorize annotation has unexpected content after the expression: ${rest}`);
238062
+ }
238063
+ if (expr.trim().length === 0) {
238064
+ throw new Error("authorize annotation has an empty expression body");
238065
+ }
238066
+ return expr;
238067
+ }
237966
238068
 
237967
238069
  // src/service/filter.ts
237968
238070
  var VALID_FILTER_TYPES = new Set([
@@ -238155,149 +238257,6 @@ function tokenize(input) {
238155
238257
  return tokens;
238156
238258
  }
238157
238259
 
238158
- // src/service/authorize.ts
238159
- init_errors();
238160
- var SOURCE_PREFIX = "#(authorize)";
238161
- var FILE_PREFIX = "##(authorize)";
238162
- function buildAuthorizeProbe(exprs) {
238163
- const selects = exprs.map((expr, i) => `__auth_${i} is (${expr})`).join(`
238164
- `);
238165
- return `run: duckdb.sql("SELECT 1 AS __authorize_probe_row") -> {
238166
- select:
238167
- ${selects}
238168
- limit: 1
238169
- }`;
238170
- }
238171
- function isProbeTrue(cell) {
238172
- return cell === true || cell === 1 || cell === "true";
238173
- }
238174
- async function evaluateAuthorize(executor, exprs, givens) {
238175
- for (const expr of exprs) {
238176
- try {
238177
- const result = await executor.loadQuery(buildAuthorizeProbe([expr])).run({ rowLimit: 1, givens });
238178
- const row = result?.data?.value?.[0];
238179
- if (row && isProbeTrue(row.__auth_0)) {
238180
- return true;
238181
- }
238182
- } catch {
238183
- continue;
238184
- }
238185
- }
238186
- return false;
238187
- }
238188
- async function validateAuthorizeProbes(compiler, sources) {
238189
- for (const source of sources) {
238190
- const exprs = source.authorize;
238191
- if (!exprs || exprs.length === 0)
238192
- continue;
238193
- try {
238194
- await compiler.loadQuery(buildAuthorizeProbe(exprs)).getPreparedQuery();
238195
- } catch (err) {
238196
- const detail = err instanceof Error ? err.message : String(err);
238197
- throw new ModelCompilationError({
238198
- message: `Invalid #(authorize) annotation on source "${source.name ?? "(unnamed)"}" [${exprs.join(" | ")}]: ${detail}`
238199
- });
238200
- }
238201
- }
238202
- }
238203
- function parseAuthorizeAnnotation(annotation) {
238204
- const trimmed2 = annotation.trim();
238205
- let body;
238206
- if (trimmed2.startsWith(FILE_PREFIX)) {
238207
- body = trimmed2.slice(FILE_PREFIX.length).trim();
238208
- } else if (trimmed2.startsWith(SOURCE_PREFIX)) {
238209
- body = trimmed2.slice(SOURCE_PREFIX.length).trim();
238210
- } else {
238211
- return null;
238212
- }
238213
- return unwrapQuotedExpression(body);
238214
- }
238215
- function collectAuthorizeExprs(annotations) {
238216
- const exprs = [];
238217
- for (const annotation of annotations) {
238218
- const expr = parseAuthorizeAnnotation(annotation);
238219
- if (expr !== null) {
238220
- exprs.push(expr);
238221
- }
238222
- }
238223
- return exprs;
238224
- }
238225
- function unwrapQuotedExpression(body) {
238226
- if (body.length < 2 || body[0] !== '"') {
238227
- throw new Error(`authorize annotation expression must be a double-quoted string, got: ${body || "(empty)"}`);
238228
- }
238229
- let expr = "";
238230
- let i = 1;
238231
- let closed = false;
238232
- for (;i < body.length; i++) {
238233
- const ch = body[i];
238234
- if (ch === "\\" && i + 1 < body.length) {
238235
- const next = body[i + 1];
238236
- if (next === '"' || next === "\\") {
238237
- expr += next;
238238
- i++;
238239
- continue;
238240
- }
238241
- }
238242
- if (ch === '"') {
238243
- closed = true;
238244
- i++;
238245
- break;
238246
- }
238247
- expr += ch;
238248
- }
238249
- if (!closed) {
238250
- throw new Error(`authorize annotation has mismatched quotes: ${body}`);
238251
- }
238252
- const rest = body.slice(i).trim();
238253
- if (rest.length > 0) {
238254
- throw new Error(`authorize annotation has unexpected content after the expression: ${rest}`);
238255
- }
238256
- if (expr.trim().length === 0) {
238257
- throw new Error("authorize annotation has an empty expression body");
238258
- }
238259
- return expr;
238260
- }
238261
-
238262
- // src/service/annotations.ts
238263
- import { Annotations } from "@malloydata/malloy";
238264
- function isReservedRoute(route) {
238265
- return route === "" || !/[\p{L}\p{N}]/u.test(route);
238266
- }
238267
- function modelAnnotations(modelDef) {
238268
- const registry = modelDef.modelAnnotations ?? {};
238269
- const visited = new Set;
238270
- const order = [];
238271
- const visit = (id) => {
238272
- if (visited.has(id))
238273
- return;
238274
- visited.add(id);
238275
- const entry = registry[id];
238276
- if (!entry)
238277
- return;
238278
- for (const dep of entry.inheritsFrom)
238279
- visit(dep);
238280
- order.push(id);
238281
- };
238282
- visit(modelDef.modelID);
238283
- let folded;
238284
- for (const id of order) {
238285
- const own = registry[id].ownNotes;
238286
- if (!own.notes?.length && !own.blockNotes?.length)
238287
- continue;
238288
- folded = {
238289
- notes: own.notes,
238290
- blockNotes: own.blockNotes,
238291
- inherits: folded
238292
- };
238293
- }
238294
- return folded ?? {};
238295
- }
238296
- function annotationTexts(annote) {
238297
- const texts = new Annotations(annote).texts();
238298
- return texts.length > 0 ? texts : undefined;
238299
- }
238300
-
238301
238260
  // src/service/given.ts
238302
238261
  function malloyGivenToApi(given) {
238303
238262
  const type = given.type;
@@ -238310,6 +238269,25 @@ function malloyGivenToApi(given) {
238310
238269
  };
238311
238270
  }
238312
238271
 
238272
+ // src/service/model_limits.ts
238273
+ init_errors();
238274
+ function resolveModelQueryRowLimit(userLimit, { defaultLimit, maxRows }) {
238275
+ const requested = userLimit && userLimit > 0 ? userLimit : defaultLimit;
238276
+ if (maxRows <= 0)
238277
+ return requested;
238278
+ return Math.min(requested, maxRows + 1);
238279
+ }
238280
+ function assertWithinModelResponseLimits(rowCount, serializedBytes, { maxRows, maxBytes }, source) {
238281
+ if (maxRows > 0 && rowCount > maxRows) {
238282
+ recordQueryCapExceeded("rows", source);
238283
+ throw new PayloadTooLargeError(`Query returned more than ${maxRows} rows. Refine the query (add a LIMIT or more selective WHERE) or raise PUBLISHER_MAX_QUERY_ROWS.`);
238284
+ }
238285
+ if (maxBytes > 0 && serializedBytes > maxBytes) {
238286
+ recordQueryCapExceeded("bytes", source);
238287
+ throw new PayloadTooLargeError(`Query response exceeded ${maxBytes} bytes (was ${serializedBytes}). Project fewer columns, add a LIMIT, or raise PUBLISHER_MAX_RESPONSE_BYTES.`);
238288
+ }
238289
+ }
238290
+
238313
238291
  // src/service/source_extraction.ts
238314
238292
  import {
238315
238293
  isSourceDef
@@ -238388,25 +238366,6 @@ function extractQueriesFromModelDef(modelDef) {
238388
238366
  }));
238389
238367
  }
238390
238368
 
238391
- // src/service/model_limits.ts
238392
- init_errors();
238393
- function resolveModelQueryRowLimit(userLimit, { defaultLimit, maxRows }) {
238394
- const requested = userLimit && userLimit > 0 ? userLimit : defaultLimit;
238395
- if (maxRows <= 0)
238396
- return requested;
238397
- return Math.min(requested, maxRows + 1);
238398
- }
238399
- function assertWithinModelResponseLimits(rowCount, serializedBytes, { maxRows, maxBytes }, source) {
238400
- if (maxRows > 0 && rowCount > maxRows) {
238401
- recordQueryCapExceeded("rows", source);
238402
- throw new PayloadTooLargeError(`Query returned more than ${maxRows} rows. Refine the query (add a LIMIT or more selective WHERE) or raise PUBLISHER_MAX_QUERY_ROWS.`);
238403
- }
238404
- if (maxBytes > 0 && serializedBytes > maxBytes) {
238405
- recordQueryCapExceeded("bytes", source);
238406
- throw new PayloadTooLargeError(`Query response exceeded ${maxBytes} bytes (was ${serializedBytes}). Project fewer columns, add a LIMIT, or raise PUBLISHER_MAX_RESPONSE_BYTES.`);
238407
- }
238408
- }
238409
-
238410
238369
  // src/service/model.ts
238411
238370
  var MALLOY_VERSION = createRequire2(import.meta.url)("@malloydata/malloy/package.json").version;
238412
238371
  function quoteMalloyIdentifier(name) {
@@ -238788,9 +238747,7 @@ class Model {
238788
238747
  }
238789
238748
  const errors2 = validateRenderTags2(result).filter((log) => log.severity === "error");
238790
238749
  if (errors2.length > 0) {
238791
- throw new ModelCompilationError({
238792
- message: `Invalid renderer configuration on '${target.label}': ${errors2.map((e) => e.message).join("; ")}`
238793
- });
238750
+ logger.warn(`Invalid renderer configuration on '${target.label}': ${errors2.map((e) => e.message).join("; ")}`);
238794
238751
  }
238795
238752
  }
238796
238753
  }
@@ -239309,14 +239266,35 @@ function deriveColumns(persistSource) {
239309
239266
  } catch (err) {
239310
239267
  logger.warn("Failed to derive columns for persist source", {
239311
239268
  sourceID: persistSource.sourceID,
239312
- error: err instanceof Error ? err.message : String(err)
239269
+ error: errMessage(err)
239313
239270
  });
239314
239271
  return [];
239315
239272
  }
239316
239273
  }
239274
+ function deriveAnnotationFields(persistSource) {
239275
+ const out = {};
239276
+ try {
239277
+ const tag = persistSource.annotations.parseAsTag("@").tag;
239278
+ for (const [key, value] of tag.entries()) {
239279
+ const text = value.text();
239280
+ if (text !== undefined)
239281
+ out[key] = text;
239282
+ }
239283
+ } catch {}
239284
+ return out;
239285
+ }
239317
239286
  function flattenDependsOn(node) {
239318
239287
  return node.dependsOn.map((d) => d.sourceID);
239319
239288
  }
239289
+ function* iterGraphSources(graph, sources) {
239290
+ for (const level of graph.nodes) {
239291
+ for (const node of level) {
239292
+ const source = sources[node.sourceID];
239293
+ if (source)
239294
+ yield source;
239295
+ }
239296
+ }
239297
+ }
239320
239298
  function computeBuildId(source, connectionDigests) {
239321
239299
  return source.makeBuildId(connectionDigests[source.connectionName], source.getSQL());
239322
239300
  }
@@ -239331,7 +239309,7 @@ async function resolvePackageConnections(pkg, names) {
239331
239309
  map.set(name, await pkg.getMalloyConnection(name));
239332
239310
  } catch (err) {
239333
239311
  logger.warn(`Failed to resolve connection ${name}`, {
239334
- error: err instanceof Error ? err.message : String(err)
239312
+ error: errMessage(err)
239335
239313
  });
239336
239314
  }
239337
239315
  }
@@ -239341,6 +239319,8 @@ async function compilePackageBuildPlan(pkg, signal) {
239341
239319
  const allGraphs = [];
239342
239320
  const allSources = {};
239343
239321
  for (const modelPath of pkg.getModelPaths()) {
239322
+ if (!modelPath.endsWith(MODEL_FILE_SUFFIX))
239323
+ continue;
239344
239324
  if (signal?.aborted)
239345
239325
  throw new Error("Build cancelled");
239346
239326
  const { runtime, modelURL, importBaseURL } = await Model.getModelRuntime(pkg.getPackagePath(), modelPath, pkg.getMalloyConfig());
@@ -239402,7 +239382,8 @@ function deriveBuildPlan(graphs, sources, connectionDigests, sourceNames) {
239402
239382
  dialect: source.dialectName,
239403
239383
  buildId: computeBuildId(source, connectionDigests),
239404
239384
  sql: source.getSQL(),
239405
- columns: deriveColumns(source)
239385
+ columns: deriveColumns(source),
239386
+ annotationFields: deriveAnnotationFields(source)
239406
239387
  };
239407
239388
  }
239408
239389
  return { graphs: wireGraphs, sources: wireSources };
@@ -239415,6 +239396,27 @@ async function computePackageBuildPlan(pkg, signal) {
239415
239396
  return deriveBuildPlan(compiled.graphs, compiled.sources, compiled.connectionDigests);
239416
239397
  }
239417
239398
 
239399
+ // src/service/persist_annotation_validation.ts
239400
+ init_errors();
239401
+ var PERSIST_LINE_PATTERN = /^\s*#@\s+persist\b/;
239402
+ var UNQUOTED_NAME_PATTERN = /\bname\s*=\s*(?!["'])/;
239403
+ function assertPersistNamesQuoted(modelSource, modelPath) {
239404
+ const offenders = [];
239405
+ for (const rawLine of modelSource.split(`
239406
+ `)) {
239407
+ if (!PERSIST_LINE_PATTERN.test(rawLine))
239408
+ continue;
239409
+ if (UNQUOTED_NAME_PATTERN.test(rawLine)) {
239410
+ offenders.push(rawLine.trim());
239411
+ }
239412
+ }
239413
+ if (offenders.length > 0) {
239414
+ throw new ModelCompilationError({
239415
+ 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("; ")}.`
239416
+ });
239417
+ }
239418
+ }
239419
+
239418
239420
  // src/service/package.ts
239419
239421
  class Package {
239420
239422
  environmentName;
@@ -239558,6 +239560,10 @@ class Package {
239558
239560
  }
239559
239561
  const model = Model.fromSerialized(packageName, packagePath, malloyConfig, sm);
239560
239562
  await model.validateRenderTags();
239563
+ if (sm.modelPath.endsWith(MODEL_FILE_SUFFIX)) {
239564
+ const modelSource = await fs6.readFile(path7.join(packagePath, sm.modelPath), "utf-8");
239565
+ assertPersistNamesQuoted(modelSource, sm.modelPath);
239566
+ }
239561
239567
  models.set(sm.modelPath, model);
239562
239568
  }
239563
239569
  const endTime = performance.now();
@@ -239578,7 +239584,7 @@ class Package {
239578
239584
  } catch (err) {
239579
239585
  logger.warn(`Failed to compute build plan for package ${packageName}`, {
239580
239586
  packageName,
239581
- error: err instanceof Error ? err.message : String(err)
239587
+ error: errMessage(err)
239582
239588
  });
239583
239589
  }
239584
239590
  const invalidMsg = pkg.formatInvalidExplores();
@@ -248571,15 +248577,19 @@ init_logger();
248571
248577
  import { Manifest } from "@malloydata/malloy";
248572
248578
 
248573
248579
  // src/service/quoting.ts
248574
- function splitTablePath(tableName) {
248580
+ function bareTableName(tableName) {
248575
248581
  const lastDot = tableName.lastIndexOf(".");
248576
- if (lastDot >= 0) {
248577
- return {
248578
- schemaPrefix: tableName.substring(0, lastDot + 1),
248579
- bareName: tableName.substring(lastDot + 1)
248580
- };
248582
+ return lastDot >= 0 ? tableName.substring(lastDot + 1) : tableName;
248583
+ }
248584
+ var BACKTICK_DIALECTS = new Set(["standardsql", "mysql", "databricks"]);
248585
+ function quoteIdentifier(identifier, dialect) {
248586
+ if (BACKTICK_DIALECTS.has(dialect)) {
248587
+ return "`" + identifier.replace(/`/g, "``") + "`";
248581
248588
  }
248582
- return { schemaPrefix: "", bareName: tableName };
248589
+ return '"' + identifier.replace(/"/g, '""') + '"';
248590
+ }
248591
+ function quoteTablePath(tableName, dialect) {
248592
+ return tableName.split(".").map((segment) => quoteIdentifier(segment, dialect)).join(".");
248583
248593
  }
248584
248594
 
248585
248595
  // src/service/resolve_environment.ts
@@ -248598,11 +248608,7 @@ function stagingSuffix(buildId) {
248598
248608
  return `_${buildId.substring(0, STAGING_BUILD_ID_LEN)}`;
248599
248609
  }
248600
248610
  function selfAssignTableName(persistSource) {
248601
- try {
248602
- return persistSource.annotations.parseAsTag("@").tag.text("name") || persistSource.name;
248603
- } catch {
248604
- return persistSource.name;
248605
- }
248611
+ return deriveAnnotationFields(persistSource).name || persistSource.name;
248606
248612
  }
248607
248613
  function outcomeFor(_err, signal) {
248608
248614
  return signal?.aborted ? "cancelled" : "failed";
@@ -248670,7 +248676,7 @@ class MaterializationService {
248670
248676
  }
248671
248677
  const active2 = await this.repository.getActiveMaterialization(environmentId, packageName);
248672
248678
  if (active2) {
248673
- throw new MaterializationConflictError(`Package ${packageName} already has an active materialization (${active2.id})`);
248679
+ throw this.activeConflict(packageName, active2.id);
248674
248680
  }
248675
248681
  const forceRefresh = options.forceRefresh ?? false;
248676
248682
  const metadata = {
@@ -248684,7 +248690,7 @@ class MaterializationService {
248684
248690
  } catch (err) {
248685
248691
  if (err instanceof DuplicateActiveMaterializationError) {
248686
248692
  const winner = await this.repository.getActiveMaterialization(environmentId, packageName);
248687
- throw new MaterializationConflictError(winner ? `Package ${packageName} already has an active materialization (${winner.id})` : `Package ${packageName} already has an active materialization`);
248693
+ throw this.activeConflict(packageName, winner?.id);
248688
248694
  }
248689
248695
  throw err;
248690
248696
  }
@@ -248705,6 +248711,9 @@ class MaterializationService {
248705
248711
  });
248706
248712
  const startedAt = Date.now();
248707
248713
  try {
248714
+ await this.repository.updateMaterialization(id, {
248715
+ startedAt: new Date(startedAt)
248716
+ });
248708
248717
  const environmentId = await this.resolveEnvironmentId(environmentName);
248709
248718
  const environment = await this.environmentStore.getEnvironment(environmentName, false);
248710
248719
  const pkg = await environment.getPackage(packageName, false);
@@ -248755,29 +248764,24 @@ class MaterializationService {
248755
248764
  const carried = {};
248756
248765
  const seen = new Set;
248757
248766
  for (const graph of compiled.graphs) {
248758
- for (const level of graph.nodes) {
248759
- for (const node of level) {
248760
- const persistSource = compiled.sources[node.sourceID];
248761
- if (!persistSource)
248762
- continue;
248763
- if (include && !include.has(persistSource.name))
248764
- continue;
248765
- const buildId = computeBuildId(persistSource, compiled.connectionDigests);
248766
- if (seen.has(buildId))
248767
- continue;
248768
- seen.add(buildId);
248769
- const prior = priorEntries[buildId];
248770
- if (prior && prior.physicalTableName) {
248771
- carried[buildId] = prior;
248772
- continue;
248773
- }
248774
- instructions.push({
248775
- buildId,
248776
- materializedTableId: `local-${buildId.substring(0, STAGING_BUILD_ID_LEN)}`,
248777
- physicalTableName: selfAssignTableName(persistSource),
248778
- realization: "COPY"
248779
- });
248767
+ for (const persistSource of iterGraphSources(graph, compiled.sources)) {
248768
+ if (include && !include.has(persistSource.name))
248769
+ continue;
248770
+ const buildId = computeBuildId(persistSource, compiled.connectionDigests);
248771
+ if (seen.has(buildId))
248772
+ continue;
248773
+ seen.add(buildId);
248774
+ const prior = priorEntries[buildId];
248775
+ if (prior && prior.physicalTableName) {
248776
+ carried[buildId] = prior;
248777
+ continue;
248780
248778
  }
248779
+ instructions.push({
248780
+ buildId,
248781
+ materializedTableId: `local-${buildId.substring(0, STAGING_BUILD_ID_LEN)}`,
248782
+ physicalTableName: selfAssignTableName(persistSource),
248783
+ realization: "COPY"
248784
+ });
248781
248785
  }
248782
248786
  }
248783
248787
  return { instructions, carried };
@@ -248851,20 +248855,15 @@ class MaterializationService {
248851
248855
  if (!connection) {
248852
248856
  throw new BadRequestError(`Connection '${graph.connectionName}' not found`);
248853
248857
  }
248854
- for (const level of graph.nodes) {
248855
- for (const node of level) {
248856
- if (signal.aborted)
248857
- throw new Error("Build cancelled");
248858
- const persistSource = sources[node.sourceID];
248859
- if (!persistSource)
248860
- continue;
248861
- const buildId = computeBuildId(persistSource, connectionDigests);
248862
- const instruction = byBuildId.get(buildId);
248863
- if (!instruction)
248864
- continue;
248865
- const entry = await this.buildOneSource(persistSource, instruction, connection, connectionDigests, manifest);
248866
- entries[buildId] = entry;
248867
- }
248858
+ for (const persistSource of iterGraphSources(graph, sources)) {
248859
+ if (signal.aborted)
248860
+ throw new Error("Build cancelled");
248861
+ const buildId = computeBuildId(persistSource, connectionDigests);
248862
+ const instruction = byBuildId.get(buildId);
248863
+ if (!instruction)
248864
+ continue;
248865
+ const entry = await this.buildOneSource(persistSource, instruction, connection, connectionDigests, manifest);
248866
+ entries[buildId] = entry;
248868
248867
  }
248869
248868
  }
248870
248869
  return entries;
@@ -248876,22 +248875,26 @@ class MaterializationService {
248876
248875
  buildManifest: manifest.buildManifest,
248877
248876
  connectionDigests
248878
248877
  });
248879
- const { bareName } = splitTablePath(physicalTableName);
248878
+ const bareName = bareTableName(physicalTableName);
248880
248879
  const stagingTableName = `${physicalTableName}${stagingSuffix(buildId)}`;
248880
+ const dialect = persistSource.dialectName;
248881
+ const quotedStaging = quoteTablePath(stagingTableName, dialect);
248882
+ const quotedPhysical = quoteTablePath(physicalTableName, dialect);
248883
+ const quotedBareName = quoteIdentifier(bareName, dialect);
248881
248884
  const startTime = performance.now();
248882
- await connection.runSQL(`DROP TABLE IF EXISTS ${stagingTableName}`);
248885
+ await connection.runSQL(`DROP TABLE IF EXISTS ${quotedStaging}`);
248883
248886
  try {
248884
- await connection.runSQL(`CREATE TABLE ${stagingTableName} AS (${buildSQL})`);
248885
- await connection.runSQL(`DROP TABLE IF EXISTS ${physicalTableName}`);
248886
- await connection.runSQL(`ALTER TABLE ${stagingTableName} RENAME TO ${bareName}`);
248887
+ await connection.runSQL(`CREATE TABLE ${quotedStaging} AS (${buildSQL})`);
248888
+ await connection.runSQL(`DROP TABLE IF EXISTS ${quotedPhysical}`);
248889
+ await connection.runSQL(`ALTER TABLE ${quotedStaging} RENAME TO ${quotedBareName}`);
248887
248890
  } catch (err) {
248888
248891
  try {
248889
- await connection.runSQL(`DROP TABLE IF EXISTS ${stagingTableName}`);
248892
+ await connection.runSQL(`DROP TABLE IF EXISTS ${quotedStaging}`);
248890
248893
  } catch (cleanupErr) {
248891
248894
  logger.warn("Failed to clean up staging table after a failed build; physical leak", {
248892
248895
  stagingTableName,
248893
248896
  connectionName: persistSource.connectionName,
248894
- cleanupError: cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)
248897
+ cleanupError: errMessage(cleanupErr)
248895
248898
  });
248896
248899
  }
248897
248900
  throw err;
@@ -248971,8 +248974,9 @@ class MaterializationService {
248971
248974
  connection = await pkg.getMalloyConnection(connectionName);
248972
248975
  connectionCache.set(connectionName, connection);
248973
248976
  }
248974
- await connection.runSQL(`DROP TABLE IF EXISTS ${physicalTableName}`);
248975
- await connection.runSQL(`DROP TABLE IF EXISTS ${physicalTableName}${stagingSuffix(entry.buildId)}`);
248977
+ const dialect = connection.dialectName;
248978
+ await connection.runSQL(`DROP TABLE IF EXISTS ${quoteTablePath(physicalTableName, dialect)}`);
248979
+ await connection.runSQL(`DROP TABLE IF EXISTS ${quoteTablePath(`${physicalTableName}${stagingSuffix(entry.buildId)}`, dialect)}`);
248976
248980
  recordDropTables("success");
248977
248981
  logger.info("Dropped materialized table on delete", {
248978
248982
  materializationId: m.id,
@@ -248985,7 +248989,7 @@ class MaterializationService {
248985
248989
  materializationId: m.id,
248986
248990
  physicalTableName,
248987
248991
  connectionName,
248988
- error: err instanceof Error ? err.message : String(err)
248992
+ error: errMessage(err)
248989
248993
  });
248990
248994
  }
248991
248995
  }
@@ -249003,6 +249007,10 @@ class MaterializationService {
249003
249007
  metadata
249004
249008
  });
249005
249009
  }
249010
+ activeConflict(packageName, activeId) {
249011
+ const suffix = activeId ? ` (${activeId})` : "";
249012
+ return new MaterializationConflictError(`Package ${packageName} already has an active materialization${suffix}`);
249013
+ }
249006
249014
  recordRun(mode, outcome, startedAtMs) {
249007
249015
  recordMaterializationRun(mode, outcome, Date.now() - startedAtMs);
249008
249016
  }
@@ -249010,7 +249018,7 @@ class MaterializationService {
249010
249018
  const abortController = new AbortController;
249011
249019
  this.runningAbortControllers.set(id, abortController);
249012
249020
  run(abortController.signal).catch(async (err) => {
249013
- const message = err instanceof Error ? err.message : String(err);
249021
+ const message = errMessage(err);
249014
249022
  const next = abortController.signal.aborted ? "CANCELLED" : "FAILED";
249015
249023
  try {
249016
249024
  await this.repository.updateMaterialization(id, {
@@ -249022,7 +249030,7 @@ class MaterializationService {
249022
249030
  logger.error("Failed to record materialization failure", {
249023
249031
  materializationId: id,
249024
249032
  originalError: message,
249025
- transitionError: transitionErr instanceof Error ? transitionErr.message : String(transitionErr)
249033
+ transitionError: errMessage(transitionErr)
249026
249034
  });
249027
249035
  }
249028
249036
  }).finally(() => {