@malloy-publisher/server 0.0.202 → 0.0.204

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 (51) hide show
  1. package/dist/app/api-doc.yaml +25 -3
  2. package/dist/app/assets/{EnvironmentPage-CNQYDaxR.js → EnvironmentPage-CX06cjOF.js} +1 -1
  3. package/dist/app/assets/HomePage-CNFt_eUU.js +1 -0
  4. package/dist/app/assets/{MainPage-B0kNpkxT.js → MainPage-nUJ9YatG.js} +1 -1
  5. package/dist/app/assets/{PackagePage-yAh0TrOV.js → MaterializationsPage-B5goxVXW.js} +1 -1
  6. package/dist/app/assets/{ModelPage-DcVElc9L.js → ModelPage-Ba7Xh4lL.js} +1 -1
  7. package/dist/app/assets/PackagePage-BaEVdEAG.js +1 -0
  8. package/dist/app/assets/{RouteError-DknUbx_s.js → RouteError-BShQjZio.js} +1 -1
  9. package/dist/app/assets/{WorkbookPage-CCqc8otA.js → WorkbookPage-CBn6ZjJW.js} +1 -1
  10. package/dist/app/assets/{core-B3A61KGJ.es-iOUZ6RJL.js → core-DECXYL4E.es-OaRfXwuQ.js} +1 -1
  11. package/dist/app/assets/{index-W0bOLKGl.js → index-BLfPC1gy.js} +2 -2
  12. package/dist/app/assets/index-DqiJ0bWp.js +455 -0
  13. package/dist/app/assets/index-Dy3YhAZQ.js +1812 -0
  14. package/dist/app/assets/index.umd-DAN9K8yC.js +2469 -0
  15. package/dist/app/index.html +1 -1
  16. package/dist/package_load_worker.mjs +392 -67
  17. package/dist/server.mjs +418 -153
  18. package/package.json +11 -11
  19. package/src/ducklake_version.spec.ts +43 -0
  20. package/src/ducklake_version.ts +26 -0
  21. package/src/errors.ts +18 -1
  22. package/src/package_load/package_load_pool.ts +0 -5
  23. package/src/package_load/package_load_worker.ts +41 -99
  24. package/src/package_load/protocol.ts +1 -7
  25. package/src/service/annotations.spec.ts +118 -0
  26. package/src/service/annotations.ts +91 -0
  27. package/src/service/authorize.spec.ts +132 -0
  28. package/src/service/authorize.ts +241 -0
  29. package/src/service/authorize_integration.spec.ts +838 -0
  30. package/src/service/connection.ts +1 -1
  31. package/src/service/environment.ts +4 -4
  32. package/src/service/environment_store.ts +14 -2
  33. package/src/service/filter.spec.ts +14 -3
  34. package/src/service/filter.ts +5 -1
  35. package/src/service/filter_bypass.spec.ts +418 -0
  36. package/src/service/given.ts +37 -12
  37. package/src/service/givens_integration.spec.ts +34 -7
  38. package/src/service/materialization_service.ts +25 -20
  39. package/src/service/materialized_table_gc.spec.ts +6 -5
  40. package/src/service/materialized_table_gc.ts +2 -50
  41. package/src/service/model.spec.ts +203 -8
  42. package/src/service/model.ts +305 -155
  43. package/src/service/package_worker_path.spec.ts +113 -0
  44. package/src/service/quoting.ts +0 -20
  45. package/src/service/restricted_mode.spec.ts +299 -0
  46. package/src/service/source_extraction.ts +226 -0
  47. package/src/storage/StorageManager.ts +73 -0
  48. package/dist/app/assets/HomePage-DBFTIoD8.js +0 -1
  49. package/dist/app/assets/index-F_o127LC.js +0 -454
  50. package/dist/app/assets/index-QeX_e740.js +0 -1803
  51. package/dist/app/assets/index.umd-CEDRw4TK.js +0 -1145
package/dist/server.mjs CHANGED
@@ -147328,6 +147328,8 @@ function internalErrorToHttpError(error) {
147328
147328
  return httpError(400, error.message);
147329
147329
  } else if (error instanceof FrozenConfigError) {
147330
147330
  return httpError(403, error.message);
147331
+ } else if (error instanceof AccessDeniedError) {
147332
+ return httpError(403, error.message);
147331
147333
  } else if (error instanceof EnvironmentNotFoundError) {
147332
147334
  return httpError(404, error.message);
147333
147335
  } else if (error instanceof PackageNotFoundError) {
@@ -147369,7 +147371,7 @@ function httpError(code, message) {
147369
147371
  }
147370
147372
  };
147371
147373
  }
147372
- var NotImplementedError, BadRequestError, EnvironmentNotFoundError, PackageNotFoundError, ModelNotFoundError, ConnectionNotFoundError, ConnectionError, ConnectionAuthError, ModelCompilationError, FrozenConfigError, MaterializationNotFoundError, MaterializationConflictError, InvalidStateTransitionError, ServiceUnavailableError, PayloadTooLargeError, QueryTimeoutError;
147374
+ var NotImplementedError, BadRequestError, EnvironmentNotFoundError, PackageNotFoundError, ModelNotFoundError, ConnectionNotFoundError, ConnectionError, ConnectionAuthError, ModelCompilationError, FrozenConfigError, AccessDeniedError, MaterializationNotFoundError, MaterializationConflictError, InvalidStateTransitionError, ServiceUnavailableError, PayloadTooLargeError, QueryTimeoutError;
147373
147375
  var init_errors = __esm(() => {
147374
147376
  init_constants();
147375
147377
  NotImplementedError = class NotImplementedError extends Error {
@@ -147422,6 +147424,12 @@ var init_errors = __esm(() => {
147422
147424
  super(message);
147423
147425
  }
147424
147426
  };
147427
+ AccessDeniedError = class AccessDeniedError extends Error {
147428
+ constructor(message) {
147429
+ super(message);
147430
+ this.name = "AccessDeniedError";
147431
+ }
147432
+ };
147425
147433
  MaterializationNotFoundError = class MaterializationNotFoundError extends Error {
147426
147434
  constructor(message) {
147427
147435
  super(message);
@@ -199146,9 +199154,6 @@ function buildFetchOptions(options) {
199146
199154
  if (options.refreshTimestamp !== undefined) {
199147
199155
  out.refreshTimestamp = options.refreshTimestamp;
199148
199156
  }
199149
- if (options.modelAnnotation !== undefined) {
199150
- out.modelAnnotation = options.modelAnnotation;
199151
- }
199152
199157
  return out;
199153
199158
  }
199154
199159
  function adaptResult(result) {
@@ -221830,7 +221835,7 @@ function buildEnvironmentMalloyConfig(connections = [], environmentPath = "", is
221830
221835
  ...azureDuckDBCache.values()
221831
221836
  ];
221832
221837
  const closeResults = await Promise.allSettled([
221833
- malloyConfig.releaseConnections(),
221838
+ malloyConfig.shutdown("close"),
221834
221839
  ...wrapperPromises.map(async (promise) => {
221835
221840
  const connection = await promise;
221836
221841
  await connection.close();
@@ -229646,10 +229651,23 @@ function registerHealthEndpoints(app) {
229646
229651
  // src/service/environment_store.ts
229647
229652
  init_logger();
229648
229653
 
229654
+ // src/storage/StorageManager.ts
229655
+ import * as crypto3 from "crypto";
229656
+
229657
+ // src/ducklake_version.ts
229658
+ var SUPPORTED_CATALOG_VERSIONS = [
229659
+ "0.1",
229660
+ "0.2",
229661
+ "0.3-dev1",
229662
+ "0.3"
229663
+ ];
229664
+ function isCatalogVersionSupported(version) {
229665
+ return SUPPORTED_CATALOG_VERSIONS.includes(version);
229666
+ }
229667
+
229649
229668
  // src/storage/StorageManager.ts
229650
229669
  init_errors();
229651
229670
  init_logger();
229652
- import * as crypto3 from "crypto";
229653
229671
 
229654
229672
  // src/storage/duckdb/DuckDBConnection.ts
229655
229673
  import duckdb from "duckdb";
@@ -230673,6 +230691,30 @@ function catalogNameForConfig(c) {
230673
230691
  const hash = crypto3.createHash("sha256").update(configKey(c)).digest("hex").slice(0, 8);
230674
230692
  return `manifest_lake_${hash}`;
230675
230693
  }
230694
+ async function readDuckLakeCatalogVersion(connection, catalogUrl, catalogName) {
230695
+ if (!catalogUrl.startsWith("postgres:")) {
230696
+ return;
230697
+ }
230698
+ const pgConnString = catalogUrl.slice("postgres:".length);
230699
+ const tempDb = `${catalogName}_preflight`;
230700
+ const escaped = escapeSQL2(pgConnString);
230701
+ try {
230702
+ await connection.run(`ATTACH '${escaped}' AS ${tempDb} (TYPE postgres, READ_ONLY);`);
230703
+ const rows = await connection.all(`SELECT value FROM ${tempDb}.ducklake_metadata WHERE key = 'version' LIMIT 1;`);
230704
+ const value = rows[0]?.value;
230705
+ return typeof value === "string" ? value : undefined;
230706
+ } catch (error) {
230707
+ logger.warn("DuckLake catalog version preflight failed; falling back to ATTACH", {
230708
+ catalogName,
230709
+ error: redactPgSecrets(error instanceof Error ? error.message : String(error))
230710
+ });
230711
+ return;
230712
+ } finally {
230713
+ try {
230714
+ await connection.run(`DETACH ${tempDb};`);
230715
+ } catch {}
230716
+ }
230717
+ }
230676
230718
 
230677
230719
  class StorageManager {
230678
230720
  connection = null;
@@ -230751,6 +230793,13 @@ class StorageManager {
230751
230793
  if (isCloudStorage) {
230752
230794
  await connection.run("INSTALL httpfs; LOAD httpfs;");
230753
230795
  }
230796
+ if (isPostgres) {
230797
+ const catalogVersion = await readDuckLakeCatalogVersion(connection, catalogUrl, catalogName);
230798
+ if (catalogVersion && !isCatalogVersionSupported(catalogVersion)) {
230799
+ const supportedMax = SUPPORTED_CATALOG_VERSIONS[SUPPORTED_CATALOG_VERSIONS.length - 1];
230800
+ throw new ConnectionAuthError(`DuckLake catalog version ${catalogVersion} is newer than this Publisher's extension supports (max ${supportedMax}). Upgrade the Publisher image or downgrade the catalog.`);
230801
+ }
230802
+ }
230754
230803
  let attachCmd = `ATTACH 'ducklake:${escapedCatalogUrl}' AS ${catalogName}`;
230755
230804
  const attachOpts = [
230756
230805
  `DATA_PATH '${escapedDataPath}'`,
@@ -230861,9 +230910,9 @@ import {
230861
230910
  init_package_load_pool();
230862
230911
  var import_api4 = __toESM(require_src(), 1);
230863
230912
  import {
230913
+ Annotations as Annotations2,
230864
230914
  API,
230865
230915
  FixedConnectionMap,
230866
- isSourceDef,
230867
230916
  MalloyConfig as MalloyConfig2,
230868
230917
  MalloyError as MalloyError2,
230869
230918
  modelDefToModelInfo,
@@ -231081,7 +231130,8 @@ function injectFilterRefinement(query, filterClause) {
231081
231130
  if (!filterClause) {
231082
231131
  return query;
231083
231132
  }
231084
- return `${query.trimEnd()} + {where: ${filterClause}}`;
231133
+ return `${query.trimEnd()}
231134
+ + {where: ${filterClause}}`;
231085
231135
  }
231086
231136
 
231087
231137
  class FilterValidationError extends Error {
@@ -231120,6 +231170,149 @@ function tokenize(input) {
231120
231170
  return tokens;
231121
231171
  }
231122
231172
 
231173
+ // src/service/authorize.ts
231174
+ init_errors();
231175
+ var SOURCE_PREFIX = "#(authorize)";
231176
+ var FILE_PREFIX = "##(authorize)";
231177
+ function buildAuthorizeProbe(exprs) {
231178
+ const selects = exprs.map((expr, i) => `__auth_${i} is (${expr})`).join(`
231179
+ `);
231180
+ return `run: duckdb.sql("SELECT 1 AS __authorize_probe_row") -> {
231181
+ select:
231182
+ ${selects}
231183
+ limit: 1
231184
+ }`;
231185
+ }
231186
+ function isProbeTrue(cell) {
231187
+ return cell === true || cell === 1 || cell === "true";
231188
+ }
231189
+ async function evaluateAuthorize(executor, exprs, givens) {
231190
+ for (const expr of exprs) {
231191
+ try {
231192
+ const result = await executor.loadQuery(buildAuthorizeProbe([expr])).run({ rowLimit: 1, givens });
231193
+ const row = result?.data?.value?.[0];
231194
+ if (row && isProbeTrue(row.__auth_0)) {
231195
+ return true;
231196
+ }
231197
+ } catch {
231198
+ continue;
231199
+ }
231200
+ }
231201
+ return false;
231202
+ }
231203
+ async function validateAuthorizeProbes(compiler, sources) {
231204
+ for (const source of sources) {
231205
+ const exprs = source.authorize;
231206
+ if (!exprs || exprs.length === 0)
231207
+ continue;
231208
+ try {
231209
+ await compiler.loadQuery(buildAuthorizeProbe(exprs)).getPreparedQuery();
231210
+ } catch (err) {
231211
+ const detail = err instanceof Error ? err.message : String(err);
231212
+ throw new ModelCompilationError({
231213
+ message: `Invalid #(authorize) annotation on source "${source.name ?? "(unnamed)"}" [${exprs.join(" | ")}]: ${detail}`
231214
+ });
231215
+ }
231216
+ }
231217
+ }
231218
+ function parseAuthorizeAnnotation(annotation) {
231219
+ const trimmed2 = annotation.trim();
231220
+ let body;
231221
+ if (trimmed2.startsWith(FILE_PREFIX)) {
231222
+ body = trimmed2.slice(FILE_PREFIX.length).trim();
231223
+ } else if (trimmed2.startsWith(SOURCE_PREFIX)) {
231224
+ body = trimmed2.slice(SOURCE_PREFIX.length).trim();
231225
+ } else {
231226
+ return null;
231227
+ }
231228
+ return unwrapQuotedExpression(body);
231229
+ }
231230
+ function collectAuthorizeExprs(annotations) {
231231
+ const exprs = [];
231232
+ for (const annotation of annotations) {
231233
+ const expr = parseAuthorizeAnnotation(annotation);
231234
+ if (expr !== null) {
231235
+ exprs.push(expr);
231236
+ }
231237
+ }
231238
+ return exprs;
231239
+ }
231240
+ function unwrapQuotedExpression(body) {
231241
+ if (body.length < 2 || body[0] !== '"') {
231242
+ throw new Error(`authorize annotation expression must be a double-quoted string, got: ${body || "(empty)"}`);
231243
+ }
231244
+ let expr = "";
231245
+ let i = 1;
231246
+ let closed = false;
231247
+ for (;i < body.length; i++) {
231248
+ const ch = body[i];
231249
+ if (ch === "\\" && i + 1 < body.length) {
231250
+ const next = body[i + 1];
231251
+ if (next === '"' || next === "\\") {
231252
+ expr += next;
231253
+ i++;
231254
+ continue;
231255
+ }
231256
+ }
231257
+ if (ch === '"') {
231258
+ closed = true;
231259
+ i++;
231260
+ break;
231261
+ }
231262
+ expr += ch;
231263
+ }
231264
+ if (!closed) {
231265
+ throw new Error(`authorize annotation has mismatched quotes: ${body}`);
231266
+ }
231267
+ const rest = body.slice(i).trim();
231268
+ if (rest.length > 0) {
231269
+ throw new Error(`authorize annotation has unexpected content after the expression: ${rest}`);
231270
+ }
231271
+ if (expr.trim().length === 0) {
231272
+ throw new Error("authorize annotation has an empty expression body");
231273
+ }
231274
+ return expr;
231275
+ }
231276
+
231277
+ // src/service/annotations.ts
231278
+ import { Annotations } from "@malloydata/malloy";
231279
+ function isReservedRoute(route) {
231280
+ return route === "" || !/[\p{L}\p{N}]/u.test(route);
231281
+ }
231282
+ function modelAnnotations(modelDef) {
231283
+ const registry = modelDef.modelAnnotations ?? {};
231284
+ const visited = new Set;
231285
+ const order = [];
231286
+ const visit = (id) => {
231287
+ if (visited.has(id))
231288
+ return;
231289
+ visited.add(id);
231290
+ const entry = registry[id];
231291
+ if (!entry)
231292
+ return;
231293
+ for (const dep of entry.inheritsFrom)
231294
+ visit(dep);
231295
+ order.push(id);
231296
+ };
231297
+ visit(modelDef.modelID);
231298
+ let folded;
231299
+ for (const id of order) {
231300
+ const own = registry[id].ownNotes;
231301
+ if (!own.notes?.length && !own.blockNotes?.length)
231302
+ continue;
231303
+ folded = {
231304
+ notes: own.notes,
231305
+ blockNotes: own.blockNotes,
231306
+ inherits: folded
231307
+ };
231308
+ }
231309
+ return folded ?? {};
231310
+ }
231311
+ function annotationTexts(annote) {
231312
+ const texts = new Annotations(annote).texts();
231313
+ return texts.length > 0 ? texts : undefined;
231314
+ }
231315
+
231123
231316
  // src/service/given.ts
231124
231317
  function malloyGivenToApi(given) {
231125
231318
  const type = given.type;
@@ -231127,10 +231320,89 @@ function malloyGivenToApi(given) {
231127
231320
  return {
231128
231321
  name: given.name,
231129
231322
  type: renderedType,
231130
- annotations: given.getTaglines(/^#\(/)
231323
+ annotations: given.annotations.forRoute(undefined).filter((note) => !isReservedRoute(note.route)).map((note) => note.text),
231324
+ default: given._internal?.defaultText
231131
231325
  };
231132
231326
  }
231133
231327
 
231328
+ // src/service/source_extraction.ts
231329
+ import {
231330
+ isSourceDef
231331
+ } from "@malloydata/malloy";
231332
+ function extractSourcesFromModelDef(modelDef, givens, onParseError) {
231333
+ const filterMap = new Map;
231334
+ const authorizeMap = new Map;
231335
+ const fileLevelAuthorize = collectAuthorizeExprs((modelAnnotations(modelDef).notes ?? []).map((note) => note.text));
231336
+ const sources = Object.values(modelDef.contents).filter((obj) => isSourceDef(obj)).map((sourceObj) => {
231337
+ const struct = sourceObj;
231338
+ const sourceName = struct.as || struct.name;
231339
+ const annotations = annotationTexts(struct.annotations);
231340
+ const collected = [];
231341
+ let cur = struct.annotations;
231342
+ while (cur) {
231343
+ if (cur.blockNotes) {
231344
+ collected.push(cur.blockNotes.map((note) => note.text));
231345
+ }
231346
+ cur = cur.inherits;
231347
+ }
231348
+ const allAnnotations = collected.reverse().flat();
231349
+ let filters;
231350
+ if (allAnnotations.length > 0) {
231351
+ try {
231352
+ const parsed = parseFilters(allAnnotations);
231353
+ if (parsed.length > 0) {
231354
+ filterMap.set(sourceName, parsed);
231355
+ const fields = struct.fields;
231356
+ filters = parsed.map((f) => {
231357
+ const field = fields.find((fd) => (fd.as || fd.name) === f.dimension);
231358
+ return {
231359
+ name: f.name,
231360
+ dimension: f.dimension,
231361
+ type: f.type,
231362
+ implicit: f.implicit,
231363
+ required: f.required,
231364
+ dimensionType: field?.type
231365
+ };
231366
+ });
231367
+ }
231368
+ } catch (err) {
231369
+ onParseError?.(sourceName, err);
231370
+ }
231371
+ }
231372
+ const ownNotes = (struct.annotations?.blockNotes ?? []).map((note) => note.text);
231373
+ const effective = [
231374
+ ...fileLevelAuthorize,
231375
+ ...collectAuthorizeExprs(ownNotes)
231376
+ ];
231377
+ let authorize;
231378
+ if (effective.length > 0) {
231379
+ authorizeMap.set(sourceName, effective);
231380
+ authorize = effective;
231381
+ }
231382
+ const views = struct.fields.filter((field) => field.type === "turtle").filter((turtle) => turtle.pipeline.map((stage) => stage.type).every((type) => type === "reduce")).map((turtle) => ({
231383
+ name: turtle.as || turtle.name,
231384
+ annotations: annotationTexts(turtle.annotations)
231385
+ }));
231386
+ return {
231387
+ name: sourceName,
231388
+ annotations,
231389
+ views,
231390
+ filters,
231391
+ givens,
231392
+ authorize
231393
+ };
231394
+ });
231395
+ return { sources, filterMap, authorizeMap };
231396
+ }
231397
+ function extractQueriesFromModelDef(modelDef) {
231398
+ const isNamedQuery = (obj) => obj.type === "query";
231399
+ return Object.values(modelDef.contents).filter(isNamedQuery).map((queryObj) => ({
231400
+ name: queryObj.as || queryObj.name,
231401
+ sourceName: typeof queryObj.structRef === "string" ? queryObj.structRef : undefined,
231402
+ annotations: annotationTexts(queryObj.annotations)
231403
+ }));
231404
+ }
231405
+
231134
231406
  // src/service/model_limits.ts
231135
231407
  init_errors();
231136
231408
  function resolveModelQueryRowLimit(userLimit, { defaultLimit, maxRows }) {
@@ -231168,6 +231440,7 @@ class Model {
231168
231440
  compilationError;
231169
231441
  filterMap;
231170
231442
  givens;
231443
+ fileLevelAuthorize = [];
231171
231444
  meter = import_api4.metrics.getMeter("publisher");
231172
231445
  queryExecutionHistogram = this.meter.createHistogram("malloy_model_query_duration", {
231173
231446
  description: "How long it takes to execute a Malloy model query",
@@ -231187,6 +231460,11 @@ class Model {
231187
231460
  this.compilationError = compilationError;
231188
231461
  this.filterMap = filterMap ?? new Map;
231189
231462
  this.givens = givens;
231463
+ try {
231464
+ this.fileLevelAuthorize = this.modelDef ? collectAuthorizeExprs((modelAnnotations(this.modelDef).notes ?? []).map((note) => note.text)) : [];
231465
+ } catch {
231466
+ this.fileLevelAuthorize = [];
231467
+ }
231190
231468
  this.modelInfo = modelInfo ?? (this.modelDef ? modelDefToModelInfo(this.modelDef) : undefined);
231191
231469
  if (this.filterMap.size > 0) {
231192
231470
  logger.warn(`Model "${packageName}/${modelPath}" uses deprecated #(filter) annotations. Migrate to given: — see https://github.com/malloydata/publisher/blob/main/docs/givens.md`, {
@@ -231199,12 +231477,78 @@ class Model {
231199
231477
  getFilters(sourceName) {
231200
231478
  return this.filterMap.get(sourceName) ?? [];
231201
231479
  }
231480
+ getAuthorize(sourceName) {
231481
+ return this.sources?.find((source) => source.name === sourceName)?.authorize ?? [];
231482
+ }
231483
+ effectiveAuthorizeFor(sourceName) {
231484
+ if (sourceName && this.sources?.some((s) => s.name === sourceName)) {
231485
+ return this.getAuthorize(sourceName);
231486
+ }
231487
+ return this.fileLevelAuthorize;
231488
+ }
231489
+ async assertAuthorized(sourceName, givens) {
231490
+ const exprs = this.effectiveAuthorizeFor(sourceName);
231491
+ if (exprs.length === 0)
231492
+ return;
231493
+ const label = sourceName ?? "(query)";
231494
+ const deny = () => {
231495
+ throw new AccessDeniedError(`Access denied for source "${label}".`);
231496
+ };
231497
+ if (!this.modelMaterializer)
231498
+ deny();
231499
+ let passed = false;
231500
+ try {
231501
+ passed = await evaluateAuthorize(this.modelMaterializer, exprs, givens);
231502
+ } catch (err) {
231503
+ logger.debug("Authorize probe failed; denying", {
231504
+ sourceName: label,
231505
+ modelPath: this.modelPath,
231506
+ error: err instanceof Error ? err.message : String(err)
231507
+ });
231508
+ deny();
231509
+ }
231510
+ if (!passed)
231511
+ deny();
231512
+ }
231513
+ async resolveAuthorizeSourceFromRunnable(runnable) {
231514
+ try {
231515
+ const prepared = await runnable.getPreparedQuery();
231516
+ const structRef = prepared._query?.structRef;
231517
+ if (typeof structRef === "string")
231518
+ return structRef;
231519
+ if (structRef && typeof structRef === "object") {
231520
+ const s = structRef;
231521
+ return s.as || s.name;
231522
+ }
231523
+ } catch {}
231524
+ return;
231525
+ }
231202
231526
  extractSourceName(query) {
231203
231527
  if (!query)
231204
231528
  return;
231205
- const runMatch = query.match(/run\s*:\s*(\w+)\s*->/);
231206
- const arrowMatch = query.match(/^\s*(\w+)\s*->/m);
231207
- return runMatch?.[1] ?? arrowMatch?.[1];
231529
+ const runMatch = query.match(/run\s*:\s*(?:`([^`]+)`|(\w+))\s*->/);
231530
+ const arrowMatch = query.match(/^\s*(?:`([^`]+)`|(\w+))\s*->/m);
231531
+ return runMatch?.[1] ?? runMatch?.[2] ?? arrowMatch?.[1] ?? arrowMatch?.[2];
231532
+ }
231533
+ resolveFilterSource(query) {
231534
+ const target = this.extractSourceName(query);
231535
+ if (!target || !query)
231536
+ return;
231537
+ const aliasOf = new Map;
231538
+ const declRe = /source\s*:\s*(\w+)\s+is\s+(\w+)/g;
231539
+ let match;
231540
+ while ((match = declRe.exec(query)) !== null) {
231541
+ aliasOf.set(match[1], match[2]);
231542
+ }
231543
+ let current = target;
231544
+ const seen = new Set;
231545
+ while (current && !seen.has(current)) {
231546
+ if (this.filterMap.has(current))
231547
+ return current;
231548
+ seen.add(current);
231549
+ current = aliasOf.get(current);
231550
+ }
231551
+ return;
231208
231552
  }
231209
231553
  static async create(packageName, packagePath, modelPath, malloyConfig, options) {
231210
231554
  const { runtime, modelURL, importBaseURL, dataStyles, modelType } = await Model.getModelRuntime(packagePath, modelPath, malloyConfig, options);
@@ -231221,10 +231565,11 @@ class Model {
231221
231565
  modelDef = compiledModel._modelDef;
231222
231566
  const malloyGivens = Array.from(compiledModel.givens.values());
231223
231567
  givens = malloyGivens.length > 0 ? malloyGivens.map(malloyGivenToApi) : undefined;
231224
- const sourceResult = Model.getSources(modelPath, modelDef, givens);
231568
+ const sourceResult = Model.getSources(modelDef, givens);
231225
231569
  sources = sourceResult.sources;
231226
231570
  filterMap = sourceResult.filterMap;
231227
- queries = Model.getQueries(modelPath, modelDef);
231571
+ queries = Model.getQueries(modelDef);
231572
+ await validateAuthorizeProbes(modelMaterializer, sources ?? []);
231228
231573
  const imports = modelDef.imports || [];
231229
231574
  const importedSourceNames = new Set;
231230
231575
  for (const importLocation of imports) {
@@ -231340,6 +231685,10 @@ class Model {
231340
231685
  let runnable;
231341
231686
  if (!this.modelMaterializer || !this.modelDef || !this.modelInfo)
231342
231687
  throw new BadRequestError("Model has no queryable entities.");
231688
+ const earlySource = sourceName || (queryName ? this.queries?.find((q) => q.name === queryName)?.sourceName : undefined) || this.extractSourceName(query);
231689
+ if (earlySource) {
231690
+ await this.assertAuthorized(earlySource, givens ?? {});
231691
+ }
231343
231692
  try {
231344
231693
  let queryString;
231345
231694
  if (!sourceName && !queryName && query) {
@@ -231360,8 +231709,9 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`;
231360
231709
  });
231361
231710
  throw new BadRequestError("Invalid query request. (Query AND !sourceName) OR (queryName AND sourceName) must be defined.");
231362
231711
  }
231712
+ const isAdHocQuery = !sourceName && !queryName && !!query;
231363
231713
  if (!bypassFilters) {
231364
- const effectiveSource = sourceName ?? this.extractSourceName(query);
231714
+ const effectiveSource = isAdHocQuery ? this.resolveFilterSource(query) : sourceName;
231365
231715
  if (effectiveSource) {
231366
231716
  const filters = this.getFilters(effectiveSource);
231367
231717
  if (filters.length > 0) {
@@ -231370,7 +231720,7 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`;
231370
231720
  }
231371
231721
  }
231372
231722
  }
231373
- runnable = this.modelMaterializer.loadQuery(queryString);
231723
+ runnable = this.modelMaterializer.loadRestrictedQuery(queryString);
231374
231724
  } catch (error) {
231375
231725
  if (error instanceof BadRequestError) {
231376
231726
  throw error;
@@ -231393,6 +231743,10 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`;
231393
231743
  });
231394
231744
  throw new BadRequestError(`Invalid query: ${errorMessage}`);
231395
231745
  }
231746
+ const compiledSource = await this.resolveAuthorizeSourceFromRunnable(runnable);
231747
+ if (!(compiledSource && compiledSource === earlySource)) {
231748
+ await this.assertAuthorized(compiledSource, givens ?? {});
231749
+ }
231396
231750
  const maxRows = getMaxQueryRows();
231397
231751
  const maxBytes = getMaxResponseBytes();
231398
231752
  const rowLimit = resolveModelQueryRowLimit((await runnable.getPreparedResult({ givens })).resultExplore.limit, { defaultLimit: getDefaultQueryRowLimit(), maxRows });
@@ -231461,25 +231815,19 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`;
231461
231815
  givens: this.givens
231462
231816
  };
231463
231817
  }
231818
+ serializeNewSources(newSources) {
231819
+ return newSources?.map((source) => JSON.stringify(this.givens && this.givens.length > 0 ? { ...source, givens: this.givens } : source));
231820
+ }
231464
231821
  async getNotebookModel() {
231465
231822
  const notebookCells = this.runnableNotebookCells.map((cell) => {
231466
231823
  return {
231467
231824
  type: cell.type,
231468
231825
  text: cell.text,
231469
- newSources: cell.newSources?.map((source) => JSON.stringify(source)),
231826
+ newSources: this.serializeNewSources(cell.newSources),
231470
231827
  queryInfo: cell.queryInfo ? JSON.stringify(cell.queryInfo) : undefined
231471
231828
  };
231472
231829
  });
231473
- const allAnnotations = [];
231474
- if (this.modelDef) {
231475
- let currentAnnotation = this.modelDef.annotation;
231476
- while (currentAnnotation) {
231477
- if (currentAnnotation.notes) {
231478
- allAnnotations.push(...currentAnnotation.notes.map((note) => note.text));
231479
- }
231480
- currentAnnotation = currentAnnotation.inherits;
231481
- }
231482
- }
231830
+ const allAnnotations = this.modelDef ? new Annotations2(modelAnnotations(this.modelDef)).texts() : [];
231483
231831
  return {
231484
231832
  type: "notebook",
231485
231833
  packageName: this.packageName,
@@ -231509,6 +231857,10 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`;
231509
231857
  text: cell.text
231510
231858
  };
231511
231859
  }
231860
+ if (cell.runnable) {
231861
+ const authorizeSource = await this.resolveAuthorizeSourceFromRunnable(cell.runnable);
231862
+ await this.assertAuthorized(authorizeSource, givens ?? {});
231863
+ }
231512
231864
  let queryName = undefined;
231513
231865
  let queryResult = undefined;
231514
231866
  if (cell.runnable) {
@@ -231571,7 +231923,7 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`;
231571
231923
  text: cell.text,
231572
231924
  queryName,
231573
231925
  result: queryResult,
231574
- newSources: cell.newSources?.map((source) => JSON.stringify(source))
231926
+ newSources: this.serializeNewSources(cell.newSources)
231575
231927
  };
231576
231928
  }
231577
231929
  static async getModelRuntime(packagePath, modelPath, malloyConfig, options) {
@@ -231611,63 +231963,11 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`;
231611
231963
  malloyConfig.wrapConnections(() => new FixedConnectionMap(input, "duckdb"));
231612
231964
  return malloyConfig;
231613
231965
  }
231614
- static getQueries(modelPath, modelDef) {
231615
- const isNamedQuery = (object) => object.type === "query";
231616
- return Object.values(modelDef.contents).filter(isNamedQuery).map((queryObj) => ({
231617
- name: queryObj.as || queryObj.name,
231618
- sourceName: typeof queryObj.structRef === "string" ? queryObj.structRef : undefined,
231619
- annotations: queryObj?.annotation?.blockNotes?.filter((note) => note.at.url.includes(modelPath)).map((note) => note.text)
231620
- }));
231966
+ static getQueries(modelDef) {
231967
+ return extractQueriesFromModelDef(modelDef);
231621
231968
  }
231622
- static getSources(modelPath, modelDef, givens) {
231623
- const filterMap = new Map;
231624
- const sources = Object.values(modelDef.contents).filter((obj) => isSourceDef(obj)).map((sourceObj) => {
231625
- const sourceName = sourceObj.as || sourceObj.name;
231626
- const annotations = sourceObj.annotation?.blockNotes?.filter((note) => note.at.url.includes(modelPath)).map((note) => note.text);
231627
- const collectedAnnotations = [];
231628
- let curAnnotation = sourceObj.annotation;
231629
- while (curAnnotation) {
231630
- if (curAnnotation.blockNotes) {
231631
- collectedAnnotations.push(curAnnotation.blockNotes.map((note) => note.text));
231632
- }
231633
- curAnnotation = curAnnotation.inherits;
231634
- }
231635
- const allAnnotations = collectedAnnotations.reverse().flat();
231636
- let filters;
231637
- if (allAnnotations.length > 0) {
231638
- try {
231639
- const parsed = parseFilters(allAnnotations);
231640
- if (parsed.length > 0) {
231641
- filterMap.set(sourceName, parsed);
231642
- const structFields = sourceObj.fields;
231643
- filters = parsed.map((f) => {
231644
- const field = structFields.find((fd) => (fd.as || fd.name) === f.dimension);
231645
- return {
231646
- name: f.name,
231647
- dimension: f.dimension,
231648
- type: f.type,
231649
- implicit: f.implicit,
231650
- required: f.required,
231651
- dimensionType: field?.type
231652
- };
231653
- });
231654
- }
231655
- } catch (err) {
231656
- logger.warn(`Failed to parse filter annotations on source "${sourceName}"`, { error: err });
231657
- }
231658
- }
231659
- const views = sourceObj.fields.filter((turtleObj) => turtleObj.type === "turtle").filter((turtleObj) => turtleObj.pipeline.map((stage) => stage.type).every((type) => type == "reduce")).map((turtleObj) => ({
231660
- name: turtleObj.as || turtleObj.name,
231661
- annotations: turtleObj?.annotation?.blockNotes?.filter((note) => note.at.url.includes(modelPath)).map((note) => note.text)
231662
- }));
231663
- return {
231664
- name: sourceName,
231665
- annotations,
231666
- views,
231667
- filters,
231668
- givens
231669
- };
231670
- });
231969
+ static getSources(modelDef, givens) {
231970
+ const { sources, filterMap } = extractSourcesFromModelDef(modelDef, givens, (sourceName, err) => logger.warn(`Failed to parse filter annotations on source "${sourceName}"`, { error: err }));
231671
231971
  return { sources, filterMap };
231672
231972
  }
231673
231973
  static async getModelMaterializer(runtime, importBaseURL, modelURL, modelPath) {
@@ -232470,7 +232770,7 @@ ${source}` : source;
232470
232770
  const packagePath = safeJoinUnderRoot(this.environmentPath, packageName);
232471
232771
  const _package = await Package.create(this.environmentName, packageName, packagePath, () => this.malloyConfig.malloyConfig);
232472
232772
  if (existingPackage !== undefined && reload) {
232473
- this.retireConnectionGeneration(`package ${packageName}`, () => existingPackage.getMalloyConfig().releaseConnections());
232773
+ this.retireConnectionGeneration(`package ${packageName}`, () => existingPackage.getMalloyConfig().shutdown("close"));
232474
232774
  }
232475
232775
  this.packages.set(packageName, _package);
232476
232776
  this.setPackageStatus(packageName, "serving" /* SERVING */);
@@ -232592,7 +232892,7 @@ ${source}` : source;
232592
232892
  this.packages.set(packageName, newPackage);
232593
232893
  this.setPackageStatus(packageName, "serving" /* SERVING */);
232594
232894
  if (oldPackage) {
232595
- this.retireConnectionGeneration(`package ${packageName}`, () => oldPackage.getMalloyConfig().releaseConnections());
232895
+ this.retireConnectionGeneration(`package ${packageName}`, () => oldPackage.getMalloyConfig().shutdown("close"));
232596
232896
  }
232597
232897
  if (retiredPath) {
232598
232898
  const pathToClean = retiredPath;
@@ -232708,7 +233008,7 @@ ${source}` : source;
232708
233008
  } else if (packageStatus?.status === "serving" /* SERVING */) {
232709
233009
  this.setPackageStatus(packageName, "unloading" /* UNLOADING */);
232710
233010
  }
232711
- this.retireConnectionGeneration(`package ${packageName}`, () => _package.getMalloyConfig().releaseConnections());
233011
+ this.retireConnectionGeneration(`package ${packageName}`, () => _package.getMalloyConfig().shutdown("close"));
232712
233012
  const canonicalPath = safeJoinUnderRoot(this.environmentPath, packageName);
232713
233013
  const retiredPath = this.allocateRetiredPath(packageName);
232714
233014
  let renamed = false;
@@ -232761,7 +233061,7 @@ ${source}` : source;
232761
233061
  }
232762
233062
  }
232763
233063
  async closeAllConnections() {
232764
- const packageReleases = await Promise.allSettled(Array.from(this.packages.values(), (pkg) => pkg.getMalloyConfig().releaseConnections()));
233064
+ const packageReleases = await Promise.allSettled(Array.from(this.packages.values(), (pkg) => pkg.getMalloyConfig().shutdown("close")));
232765
233065
  for (const result of packageReleases) {
232766
233066
  if (result.status === "rejected") {
232767
233067
  logger.error(`Error closing package connections for environment ${this.environmentName}`, { error: result.reason });
@@ -233188,12 +233488,14 @@ class EnvironmentStore {
233188
233488
  return Promise.all(Array.from(this.environments.values()).map((environment) => environment.serialize()));
233189
233489
  }
233190
233490
  async getStatus() {
233491
+ const baseState = getOperationalState();
233492
+ const operationalState2 = baseState !== "draining" && (this.memoryGovernor?.isBackpressured() ?? false) ? "throttled" : baseState;
233191
233493
  const status = {
233192
233494
  timestamp: Date.now(),
233193
233495
  environments: [],
233194
233496
  initialized: this.isInitialized,
233195
233497
  frozenConfig: isPublisherConfigFrozen(this.serverRootPath),
233196
- operationalState: getOperationalState()
233498
+ operationalState: operationalState2
233197
233499
  };
233198
233500
  const environments = await this.listEnvironments(true);
233199
233501
  await Promise.all(environments.map(async (environment) => {
@@ -238655,41 +238957,6 @@ import { Manifest } from "@malloydata/malloy";
238655
238957
 
238656
238958
  // src/service/materialized_table_gc.ts
238657
238959
  init_logger();
238658
- import {
238659
- DatabricksDialect,
238660
- DuckDBDialect,
238661
- MySQLDialect,
238662
- PostgresDialect,
238663
- SnowflakeDialect,
238664
- StandardSQLDialect,
238665
- TrinoDialect
238666
- } from "@malloydata/malloy";
238667
-
238668
- // src/service/quoting.ts
238669
- function quoteTablePath(path10, dialect) {
238670
- return path10.split(".").map((seg) => dialect.quoteTablePath(seg)).join(".");
238671
- }
238672
- function splitTablePath(tableName) {
238673
- const lastDot = tableName.lastIndexOf(".");
238674
- if (lastDot >= 0) {
238675
- return {
238676
- schemaPrefix: tableName.substring(0, lastDot + 1),
238677
- bareName: tableName.substring(lastDot + 1)
238678
- };
238679
- }
238680
- return { schemaPrefix: "", bareName: tableName };
238681
- }
238682
-
238683
- // src/service/materialized_table_gc.ts
238684
- var DIALECTS = Object.freeze({
238685
- duckdb: new DuckDBDialect,
238686
- standardsql: new StandardSQLDialect,
238687
- trino: new TrinoDialect,
238688
- postgres: new PostgresDialect,
238689
- snowflake: new SnowflakeDialect,
238690
- mysql: new MySQLDialect,
238691
- databricks: new DatabricksDialect
238692
- });
238693
238960
  function liveTableKey(connectionName, tableName) {
238694
238961
  return `${connectionName}::${tableName}`;
238695
238962
  }
@@ -238735,17 +239002,6 @@ async function processOneEntry(entry, ctx, liveTables) {
238735
239002
  }
238736
239003
  };
238737
239004
  }
238738
- const dialect = DIALECTS[connection.dialectName];
238739
- if (!dialect) {
238740
- return {
238741
- error: {
238742
- buildId: entry.buildId,
238743
- tableName: entry.tableName,
238744
- connectionName: entry.connectionName,
238745
- error: `No dialect registered for '${connection.dialectName}'`
238746
- }
238747
- };
238748
- }
238749
239005
  if (ctx.dryRun) {
238750
239006
  return {
238751
239007
  dropped: {
@@ -238757,7 +239013,6 @@ async function processOneEntry(entry, ctx, liveTables) {
238757
239013
  }
238758
239014
  };
238759
239015
  }
238760
- const quoted = (p) => quoteTablePath(p, dialect);
238761
239016
  try {
238762
239017
  await ctx.manifestService.deleteEntry(ctx.environmentId, entry.id);
238763
239018
  } catch (err) {
@@ -238784,7 +239039,7 @@ async function processOneEntry(entry, ctx, liveTables) {
238784
239039
  });
238785
239040
  } else {
238786
239041
  try {
238787
- await connection.runSQL(`DROP TABLE IF EXISTS ${quoted(entry.tableName)}`);
239042
+ await connection.runSQL(`DROP TABLE IF EXISTS ${entry.tableName}`);
238788
239043
  } catch (err) {
238789
239044
  logger.warn("GC: deleted manifest row but failed to drop materialized table (orphaned)", {
238790
239045
  tableName: entry.tableName,
@@ -238794,7 +239049,7 @@ async function processOneEntry(entry, ctx, liveTables) {
238794
239049
  }
238795
239050
  }
238796
239051
  try {
238797
- await connection.runSQL(`DROP TABLE IF EXISTS ${quoted(stagingTableName)}`);
239052
+ await connection.runSQL(`DROP TABLE IF EXISTS ${stagingTableName}`);
238798
239053
  } catch (err) {
238799
239054
  logger.warn("GC: failed to drop staging table (best-effort)", {
238800
239055
  stagingTableName,
@@ -238826,6 +239081,18 @@ async function dropManifestEntries(entries, ctx) {
238826
239081
  return { dropped, errors: errors2 };
238827
239082
  }
238828
239083
 
239084
+ // src/service/quoting.ts
239085
+ function splitTablePath(tableName) {
239086
+ const lastDot = tableName.lastIndexOf(".");
239087
+ if (lastDot >= 0) {
239088
+ return {
239089
+ schemaPrefix: tableName.substring(0, lastDot + 1),
239090
+ bareName: tableName.substring(lastDot + 1)
239091
+ };
239092
+ }
239093
+ return { schemaPrefix: "", bareName: tableName };
239094
+ }
239095
+
238829
239096
  // src/service/materialization_service.ts
238830
239097
  var STAGING_BUILD_ID_LEN = 12;
238831
239098
  function stagingSuffix(buildId) {
@@ -238851,9 +239118,9 @@ async function resolvePackageConnections(pkg, names) {
238851
239118
  function manifestTableKey(connectionName, tableName) {
238852
239119
  return `${connectionName}::${tableName}`;
238853
239120
  }
238854
- async function tablePhysicallyExists(connection, quotedTableName) {
239121
+ async function tablePhysicallyExists(connection, tableName) {
238855
239122
  try {
238856
- await connection.runSQL(`SELECT 1 FROM ${quotedTableName} WHERE 1=0`);
239123
+ await connection.runSQL(`SELECT 1 FROM ${tableName} WHERE 1=0`);
238857
239124
  return true;
238858
239125
  } catch {
238859
239126
  return false;
@@ -239108,7 +239375,7 @@ class MaterializationService {
239108
239375
  importBaseURL
239109
239376
  });
239110
239377
  const malloyModel = await modelMaterializer.getModel();
239111
- const modelTag = malloyModel.tagParse({ prefix: /^##! / }).tag;
239378
+ const modelTag = malloyModel.annotations.parseAsTag("!").tag;
239112
239379
  if (!modelTag.has("experimental", "persistence")) {
239113
239380
  logger.debug("Model has no ##! experimental.persistence tag, skipping", { modelPath });
239114
239381
  continue;
@@ -239138,7 +239405,7 @@ class MaterializationService {
239138
239405
  });
239139
239406
  const tableOwners = new Map;
239140
239407
  for (const [sourceID, source] of Object.entries(allSources)) {
239141
- const tableName = source.tagParse({ prefix: /^#@ / }).tag.text("name") || source.name;
239408
+ const tableName = source.annotations.parseAsTag("@").tag.text("name") || source.name;
239142
239409
  const key = `${source.connectionName}::${tableName}`;
239143
239410
  const existing = tableOwners.get(key);
239144
239411
  if (existing) {
@@ -239177,14 +239444,12 @@ class MaterializationService {
239177
239444
  connectionDigests
239178
239445
  });
239179
239446
  const connectionName = persistSource.connectionName;
239180
- const tableName = persistSource.tagParse({ prefix: /^#@ / }).tag.text("name") || persistSource.name;
239181
- const { schemaPrefix, bareName } = splitTablePath(tableName);
239182
- const stagingTableName = `${schemaPrefix}${bareName}${stagingSuffix(buildId)}`;
239183
- const dialect = persistSource.dialect;
239184
- const quoted = (p) => quoteTablePath(p, dialect);
239447
+ const tableName = persistSource.annotations.parseAsTag("@").tag.text("name") || persistSource.name;
239448
+ const { bareName } = splitTablePath(tableName);
239449
+ const stagingTableName = `${tableName}${stagingSuffix(buildId)}`;
239185
239450
  const tableKey = manifestTableKey(connectionName, tableName);
239186
239451
  if (!knownMaterializedTables.has(tableKey)) {
239187
- if (await tablePhysicallyExists(connection, quoted(tableName))) {
239452
+ if (await tablePhysicallyExists(connection, tableName)) {
239188
239453
  throw new BadRequestError(`Refusing to materialize source '${persistSource.name}': ` + `target table '${tableName}' already exists on connection ` + `'${connectionName}' but was not created by a previous ` + `materialization build. Use '#@ persist name=...' to ` + `choose a different table name, or drop the existing ` + `table manually if it is no longer needed.`);
239189
239454
  }
239190
239455
  }
@@ -239193,14 +239458,14 @@ class MaterializationService {
239193
239458
  connectionName
239194
239459
  });
239195
239460
  const startTime = performance.now();
239196
- await connection.runSQL(`DROP TABLE IF EXISTS ${quoted(stagingTableName)}`);
239461
+ await connection.runSQL(`DROP TABLE IF EXISTS ${stagingTableName}`);
239197
239462
  try {
239198
- await connection.runSQL(`CREATE TABLE ${quoted(stagingTableName)} AS (${buildSQL})`);
239199
- await connection.runSQL(`DROP TABLE IF EXISTS ${quoted(tableName)}`);
239200
- await connection.runSQL(`ALTER TABLE ${quoted(stagingTableName)} RENAME TO ${dialect.quoteTablePath(bareName)}`);
239463
+ await connection.runSQL(`CREATE TABLE ${stagingTableName} AS (${buildSQL})`);
239464
+ await connection.runSQL(`DROP TABLE IF EXISTS ${tableName}`);
239465
+ await connection.runSQL(`ALTER TABLE ${stagingTableName} RENAME TO ${bareName}`);
239201
239466
  } catch (err) {
239202
239467
  try {
239203
- await connection.runSQL(`DROP TABLE IF EXISTS ${quoted(stagingTableName)}`);
239468
+ await connection.runSQL(`DROP TABLE IF EXISTS ${stagingTableName}`);
239204
239469
  } catch (cleanupErr) {
239205
239470
  logger.warn("Build: failed to clean up staging table after a failed rebuild; physical leak", {
239206
239471
  stagingTableName,