@malloy-publisher/server 0.0.198 → 0.0.200

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 (75) hide show
  1. package/build.ts +30 -1
  2. package/dist/app/api-doc.yaml +127 -111
  3. package/dist/app/assets/{EnvironmentPage-C7rtH4mC.js → EnvironmentPage-CgKNjySu.js} +1 -1
  4. package/dist/app/assets/HomePage-BPIpMBjW.js +1 -0
  5. package/dist/app/assets/{MainPage-D38LtZDV.js → MainPage-CAwb8U82.js} +2 -2
  6. package/dist/app/assets/{ModelPage-DOol8Mz7.js → ModelPage-C0Uevsw9.js} +1 -1
  7. package/dist/app/assets/{PackagePage-0tgzA_kO.js → PackagePage-Cu-u9k1g.js} +1 -1
  8. package/dist/app/assets/{RouteError-BaMsOSly.js → RouteError-DVwPh2Ql.js} +1 -1
  9. package/dist/app/assets/{WorkbookPage-Cx4SePkx.js → WorkbookPage-DW38R2Zv.js} +1 -1
  10. package/dist/app/assets/{core-CbsC6R_Y.es-Cwf6asf3.js → core-C0vCMRDQ.es-D_ytHhjS.js} +10 -10
  11. package/dist/app/assets/{index-DL6BZTuw.js → index-BGdcKsFF.js} +1 -1
  12. package/dist/app/assets/{index-DNofXMxi.js → index-CTx4v4_3.js} +1 -1
  13. package/dist/app/assets/index-DE6d5jEy.js +452 -0
  14. package/dist/app/assets/{index.umd-B68wGGkM.js → index.umd-C1Mi1uRm.js} +1 -1
  15. package/dist/app/index.html +1 -1
  16. package/dist/instrumentation.mjs +57 -36
  17. package/dist/package_load_worker.mjs +12213 -0
  18. package/dist/server.mjs +4198 -3648
  19. package/package.json +2 -3
  20. package/src/config.spec.ts +246 -0
  21. package/src/config.ts +121 -1
  22. package/src/constants.ts +84 -1
  23. package/src/controller/compile.controller.ts +3 -1
  24. package/src/controller/connection.controller.spec.ts +803 -0
  25. package/src/controller/connection.controller.ts +207 -20
  26. package/src/controller/model.controller.ts +19 -1
  27. package/src/controller/query.controller.ts +22 -6
  28. package/src/controller/watch-mode.controller.ts +11 -2
  29. package/src/errors.spec.ts +44 -0
  30. package/src/errors.ts +34 -0
  31. package/src/health.spec.ts +90 -0
  32. package/src/health.ts +88 -45
  33. package/src/heap_check.spec.ts +144 -0
  34. package/src/heap_check.ts +144 -0
  35. package/src/instrumentation.ts +50 -0
  36. package/src/mcp/handler_utils.ts +14 -0
  37. package/src/mcp/tools/execute_query_tool.ts +52 -10
  38. package/src/oom_guards.integration.spec.ts +261 -0
  39. package/src/package_load/package_load_pool.spec.ts +252 -0
  40. package/src/package_load/package_load_pool.ts +920 -0
  41. package/src/package_load/package_load_worker.ts +980 -0
  42. package/src/package_load/protocol.ts +336 -0
  43. package/src/path_safety.ts +9 -3
  44. package/src/query_cap_metrics.spec.ts +89 -0
  45. package/src/query_cap_metrics.ts +115 -0
  46. package/src/query_concurrency.spec.ts +247 -0
  47. package/src/query_concurrency.ts +236 -0
  48. package/src/query_param_utils.ts +18 -0
  49. package/src/query_timeout.spec.ts +224 -0
  50. package/src/query_timeout.ts +178 -0
  51. package/src/server-old.ts +21 -1
  52. package/src/server.ts +61 -57
  53. package/src/service/connection.ts +8 -2
  54. package/src/service/db_utils.spec.ts +1 -1
  55. package/src/service/environment.ts +85 -4
  56. package/src/service/environment_admission.spec.ts +165 -1
  57. package/src/service/environment_store.spec.ts +103 -0
  58. package/src/service/environment_store.ts +98 -26
  59. package/src/service/filter_integration.spec.ts +110 -0
  60. package/src/service/given.ts +80 -0
  61. package/src/service/givens_integration.spec.ts +192 -0
  62. package/src/service/model.spec.ts +298 -3
  63. package/src/service/model.ts +362 -23
  64. package/src/service/model_limits.spec.ts +181 -0
  65. package/src/service/model_limits.ts +110 -0
  66. package/src/service/package.spec.ts +12 -6
  67. package/src/service/package.ts +263 -146
  68. package/src/service/package_worker_path.spec.ts +196 -0
  69. package/src/service/path_injection.spec.ts +39 -0
  70. package/src/stream_helpers.spec.ts +280 -0
  71. package/src/stream_helpers.ts +162 -0
  72. package/src/test_helpers/metrics_harness.ts +126 -0
  73. package/tests/integration/concurrent_package/concurrent_package.integration.spec.ts +280 -0
  74. package/dist/app/assets/HomePage-DwkH7OrS.js +0 -1
  75. package/dist/app/assets/index-U38AyjJL.js +0 -451
@@ -3,6 +3,7 @@ import {
3
3
  API,
4
4
  Connection,
5
5
  FixedConnectionMap,
6
+ GivenValue,
6
7
  isSourceDef,
7
8
  MalloyConfig,
8
9
  MalloyError,
@@ -28,16 +29,23 @@ import * as fs from "fs/promises";
28
29
  import { createRequire } from "module";
29
30
  import * as path from "path";
30
31
  import { components } from "../api";
32
+ import { deserializeError } from "../package_load/package_load_pool";
33
+ import type {
34
+ SerializedModel,
35
+ SerializedNotebookCell,
36
+ } from "../package_load/protocol";
31
37
  import {
32
- MODEL_FILE_SUFFIX,
33
- NOTEBOOK_FILE_SUFFIX,
34
- ROW_LIMIT,
35
- } from "../constants";
38
+ getDefaultQueryRowLimit,
39
+ getMaxQueryRows,
40
+ getMaxResponseBytes,
41
+ } from "../config";
42
+ import { MODEL_FILE_SUFFIX, NOTEBOOK_FILE_SUFFIX } from "../constants";
36
43
  import { HackyDataStylesAccumulator } from "../data_styles";
37
44
  import {
38
45
  BadRequestError,
39
46
  ModelCompilationError,
40
47
  ModelNotFoundError,
48
+ PayloadTooLargeError,
41
49
  } from "../errors";
42
50
  import { logger } from "../logger";
43
51
  import { BuildManifest } from "../storage/DatabaseInterface";
@@ -50,12 +58,18 @@ import {
50
58
  type FilterDefinition,
51
59
  type FilterParams,
52
60
  } from "./filter";
61
+ import { malloyGivenToApi, type MalloyGiven } from "./given";
62
+ import {
63
+ assertWithinModelResponseLimits,
64
+ resolveModelQueryRowLimit,
65
+ } from "./model_limits";
53
66
 
54
67
  type ApiCompiledModel = components["schemas"]["CompiledModel"];
55
68
  type ApiNotebookCell = components["schemas"]["NotebookCell"];
56
69
  type ApiRawNotebook = components["schemas"]["RawNotebook"];
57
70
  type ApiSource = components["schemas"]["Source"];
58
71
  type ApiFilter = components["schemas"]["Filter"];
72
+ type ApiGiven = components["schemas"]["Given"];
59
73
  type ApiView = components["schemas"]["View"];
60
74
  type ApiQuery = components["schemas"]["Query"];
61
75
  export type ApiConnection = components["schemas"]["Connection"];
@@ -98,6 +112,10 @@ export class Model {
98
112
  private compilationError: MalloyError | Error | undefined;
99
113
  /** Parsed #(filter) definitions keyed by source name. */
100
114
  private filterMap: Map<string, FilterDefinition[]>;
115
+ /** Givens declared on the model, in declaration order. Malloy's
116
+ * `Model.givens` already collapses inheritance; we just stash the list
117
+ * for surfacing on the compiled-model response. */
118
+ private givens: ApiGiven[] | undefined;
101
119
  private meter = metrics.getMeter("publisher");
102
120
  private queryExecutionHistogram = this.meter.createHistogram(
103
121
  "malloy_model_query_duration",
@@ -121,6 +139,16 @@ export class Model {
121
139
  runnableNotebookCells: RunnableNotebookCell[] | undefined,
122
140
  compilationError: MalloyError | Error | undefined,
123
141
  filterMap?: Map<string, FilterDefinition[]>,
142
+ givens?: ApiGiven[],
143
+ /**
144
+ * Precomputed `modelDefToModelInfo(modelDef)`. The package-load
145
+ * worker emits it as part of `SerializedModel` so we don't
146
+ * re-derive it on every package load. Callers that build a
147
+ * `Model` from a raw `modelDef` (e.g. test fixtures via
148
+ * `Model.create`) can omit this and let the constructor
149
+ * derive it lazily.
150
+ */
151
+ modelInfo?: Malloy.ModelInfo,
124
152
  ) {
125
153
  this.packageName = packageName;
126
154
  this.modelPath = modelPath;
@@ -134,9 +162,10 @@ export class Model {
134
162
  this.runnableNotebookCells = runnableNotebookCells;
135
163
  this.compilationError = compilationError;
136
164
  this.filterMap = filterMap ?? new Map();
137
- this.modelInfo = this.modelDef
138
- ? modelDefToModelInfo(this.modelDef)
139
- : undefined;
165
+ this.givens = givens;
166
+ this.modelInfo =
167
+ modelInfo ??
168
+ (this.modelDef ? modelDefToModelInfo(this.modelDef) : undefined);
140
169
  }
141
170
 
142
171
  /**
@@ -158,6 +187,15 @@ export class Model {
158
187
  return runMatch?.[1] ?? arrowMatch?.[1];
159
188
  }
160
189
 
190
+ /**
191
+ * Compile a single model in-process. Kept as a library entry point
192
+ * for test fixtures and any future caller that needs an ad-hoc
193
+ * `Model` from a `.malloy` / `.malloynb` file. Production package
194
+ * loads (`Package.create`) and reloads (`Package.reloadAllModels`)
195
+ * route through the package-load worker pool and dispatch through
196
+ * {@link Model.fromSerialized} instead — neither calls this on the
197
+ * main thread.
198
+ */
161
199
  public static async create(
162
200
  packageName: string,
163
201
  packagePath: string,
@@ -188,10 +226,21 @@ export class Model {
188
226
  let sources = undefined;
189
227
  let queries = undefined;
190
228
  let filterMap: Map<string, FilterDefinition[]> | undefined;
229
+ let givens: ApiGiven[] | undefined;
191
230
  const sourceInfos: Malloy.SourceInfo[] = [];
192
231
  if (modelMaterializer) {
193
- modelDef = (await modelMaterializer.getModel())._modelDef;
194
- const sourceResult = Model.getSources(modelPath, modelDef);
232
+ const compiledModel = await modelMaterializer.getModel();
233
+ modelDef = compiledModel._modelDef;
234
+ // Malloy's `Model.givens` already collapses inheritance from imports
235
+ // and applies any `finalizeGivens` runtime config. Just read it.
236
+ const malloyGivens = Array.from(
237
+ compiledModel.givens.values(),
238
+ ) as MalloyGiven[];
239
+ givens =
240
+ malloyGivens.length > 0
241
+ ? (malloyGivens.map(malloyGivenToApi) as ApiGiven[])
242
+ : undefined;
243
+ const sourceResult = Model.getSources(modelPath, modelDef, givens);
195
244
  sources = sourceResult.sources;
196
245
  filterMap = sourceResult.filterMap;
197
246
  queries = Model.getQueries(modelPath, modelDef);
@@ -255,6 +304,7 @@ export class Model {
255
304
  runnableNotebookCells,
256
305
  undefined,
257
306
  filterMap,
307
+ givens,
258
308
  );
259
309
  } catch (error) {
260
310
  let computedError = error;
@@ -285,6 +335,123 @@ export class Model {
285
335
  }
286
336
  }
287
337
 
338
+ /**
339
+ * Construct a `Model` from a worker-compiled `SerializedModel`. All
340
+ * the heavy compile work (parse, type check, IR build, sourceInfo
341
+ * extraction, per-cell notebook compile) already ran inside a
342
+ * `worker_threads` worker; this factory just rewraps the wire data
343
+ * into a live `Model`.
344
+ *
345
+ * Hydrates the `ModelMaterializer` (and, for notebooks, the
346
+ * per-cell materializers + runnables) **eagerly** via
347
+ * `Runtime._loadModelFromModelDef` /
348
+ * `ModelMaterializer._loadQueryFromQueryDef`. These are constant-time
349
+ * wraps around the worker's pre-compiled `modelDef` / `queryDef` —
350
+ * no parse, no type-check, no schema fetch — so doing it here at
351
+ * package-load time costs microseconds per model and keeps the
352
+ * resulting `Model` interchangeable with one produced by
353
+ * `Model.create` (no lazy-init branches in the hot path).
354
+ */
355
+ public static fromSerialized(
356
+ packageName: string,
357
+ _packagePath: string,
358
+ malloyConfig: ModelConnectionInput,
359
+ data: SerializedModel,
360
+ ): Model {
361
+ const modelDef = data.modelDef as ModelDef | undefined;
362
+ const modelInfo = data.modelInfo as Malloy.ModelInfo | undefined;
363
+ const dataStyles = (data.dataStyles ?? {}) as DataStyles;
364
+ const sources = data.sources as ApiSource[] | undefined;
365
+ const queries = data.queries as ApiQuery[] | undefined;
366
+ const sourceInfos = data.sourceInfos as Malloy.SourceInfo[] | undefined;
367
+ const givens = data.givens as ApiGiven[] | undefined;
368
+ const filterMap = data.filterMap
369
+ ? new Map(data.filterMap as Array<[string, FilterDefinition[]]>)
370
+ : undefined;
371
+
372
+ // No modelDef → either an empty notebook (no MALLOY statements)
373
+ // or a corrupt worker payload. Build a Model with no materializer;
374
+ // downstream getQueryResults / executeNotebookCell will throw a
375
+ // clean BadRequestError if a caller tries to run a query. We
376
+ // still preserve markdown cells for an all-markdown notebook so
377
+ // `getNotebook()` can serve raw text.
378
+ if (!modelDef) {
379
+ return new Model(
380
+ packageName,
381
+ data.modelPath,
382
+ dataStyles,
383
+ data.modelType,
384
+ undefined,
385
+ undefined,
386
+ sources,
387
+ queries,
388
+ sourceInfos,
389
+ data.modelType === "notebook"
390
+ ? hydrateMarkdownOnlyCells(data.notebookCells)
391
+ : undefined,
392
+ undefined,
393
+ filterMap,
394
+ givens,
395
+ modelInfo,
396
+ );
397
+ }
398
+
399
+ const runtime = makeHydrationRuntime(malloyConfig);
400
+ const modelMaterializer = runtime._loadModelFromModelDef(modelDef);
401
+ const runnableNotebookCells =
402
+ data.modelType === "notebook"
403
+ ? hydrateNotebookCells(runtime, data.notebookCells)
404
+ : undefined;
405
+
406
+ return new Model(
407
+ packageName,
408
+ data.modelPath,
409
+ dataStyles,
410
+ data.modelType,
411
+ modelMaterializer,
412
+ modelDef,
413
+ sources,
414
+ queries,
415
+ sourceInfos,
416
+ runnableNotebookCells,
417
+ undefined, // compilationError
418
+ filterMap,
419
+ givens,
420
+ modelInfo,
421
+ );
422
+ }
423
+
424
+ /**
425
+ * Build a Model representing a compilation failure (no modelDef,
426
+ * no materializer). Matches the shape `Model.create` returns when
427
+ * it catches a `MalloyError`, so the rest of the system handles
428
+ * both paths uniformly (the iteration loop in `Package.create`
429
+ * reads `compilationError` via a structural cast).
430
+ */
431
+ public static fromCompilationError(
432
+ packageName: string,
433
+ modelPath: string,
434
+ modelType: ModelType,
435
+ error: Error,
436
+ ): Model {
437
+ return new Model(
438
+ packageName,
439
+ modelPath,
440
+ {} as DataStyles,
441
+ modelType,
442
+ undefined,
443
+ undefined,
444
+ undefined,
445
+ undefined,
446
+ undefined,
447
+ undefined,
448
+ error,
449
+ );
450
+ }
451
+
452
+ /** Look up the deserialized error helper for callers (e.g. Package.create). */
453
+ public static deserializeCompilationError = deserializeError;
454
+
288
455
  public getPath(): string {
289
456
  return this.modelPath;
290
457
  }
@@ -342,6 +509,15 @@ export class Model {
342
509
  query?: string,
343
510
  filterParams?: FilterParams,
344
511
  bypassFilters?: boolean,
512
+ givens?: Record<string, GivenValue>,
513
+ // Optional caller-supplied abort signal. Plumbed straight into
514
+ // `runnable.run` so a publisher-issued query timeout (see
515
+ // `runWithQueryTimeout`) actually cancels the work in flight
516
+ // instead of just unblocking the awaiter. Pass `undefined` to
517
+ // keep the legacy "no timeout" behavior — useful for
518
+ // background callers (materialization, tests) that own their
519
+ // own deadline.
520
+ abortSignal?: AbortSignal,
345
521
  ): Promise<{
346
522
  result: Malloy.Result;
347
523
  compactResult: QueryData;
@@ -435,14 +611,18 @@ export class Model {
435
611
  throw new BadRequestError(`Invalid query: ${errorMessage}`);
436
612
  }
437
613
 
438
- const rowLimit =
439
- (await runnable.getPreparedResult()).resultExplore.limit || ROW_LIMIT;
614
+ const maxRows = getMaxQueryRows();
615
+ const maxBytes = getMaxResponseBytes();
616
+ const rowLimit = resolveModelQueryRowLimit(
617
+ (await runnable.getPreparedResult({ givens })).resultExplore.limit,
618
+ { defaultLimit: getDefaultQueryRowLimit(), maxRows },
619
+ );
440
620
  const endTime = performance.now();
441
621
  const executionTime = endTime - startTime;
442
622
 
443
623
  let queryResults;
444
624
  try {
445
- queryResults = await runnable.run({ rowLimit });
625
+ queryResults = await runnable.run({ rowLimit, givens, abortSignal });
446
626
  } catch (error) {
447
627
  // Record error metrics
448
628
  const errorEndTime = performance.now();
@@ -475,6 +655,24 @@ export class Model {
475
655
  throw new BadRequestError(`Query execution failed: ${errorMessage}`);
476
656
  }
477
657
 
658
+ const wrappedResult = API.util.wrapResult(queryResults);
659
+ // Best-effort byte check: we've already buffered `queryResults` and
660
+ // built `wrappedResult` by the time we get here, so this surfaces
661
+ // oversize responses with a clean HTTP 413 instead of letting the
662
+ // controller transmit a half-megabyte payload — it is not OOM
663
+ // prevention. True prevention requires streaming `Result`
664
+ // construction, which is out of scope for this step. The row cap
665
+ // above is the primary OOM defense.
666
+ const serializedBytes =
667
+ maxBytes > 0
668
+ ? Buffer.byteLength(JSON.stringify(wrappedResult), "utf8")
669
+ : 0;
670
+ assertWithinModelResponseLimits(
671
+ queryResults.totalRows,
672
+ serializedBytes,
673
+ { maxRows, maxBytes },
674
+ "model_query",
675
+ );
478
676
  this.queryExecutionHistogram.record(executionTime, {
479
677
  "malloy.model.path": this.modelPath,
480
678
  "malloy.model.query.name": queryName,
@@ -486,7 +684,7 @@ export class Model {
486
684
  "malloy.model.query.status": "success",
487
685
  });
488
686
  return {
489
- result: API.util.wrapResult(queryResults),
687
+ result: wrappedResult,
490
688
  compactResult: queryResults.data.value,
491
689
  modelInfo: this.modelInfo,
492
690
  dataStyles: this.dataStyles,
@@ -501,14 +699,16 @@ export class Model {
501
699
  malloyVersion: MALLOY_VERSION,
502
700
  dataStyles: JSON.stringify(this.dataStyles),
503
701
  modelDef: JSON.stringify(this.modelDef),
504
- modelInfo: JSON.stringify(
505
- this.modelDef ? modelDefToModelInfo(this.modelDef) : {},
506
- ),
702
+ // `this.modelInfo` is precomputed once at construction (either
703
+ // by the worker or in the Model.create constructor); don't
704
+ // re-run `modelDefToModelInfo` on every API hit.
705
+ modelInfo: JSON.stringify(this.modelInfo ?? {}),
507
706
  sourceInfos: this.getSourceInfos()?.map((sourceInfo) =>
508
707
  JSON.stringify(sourceInfo),
509
708
  ),
510
709
  sources: this.sources,
511
710
  queries: this.queries,
711
+ givens: this.givens,
512
712
  } as ApiCompiledModel;
513
713
  }
514
714
 
@@ -554,9 +754,7 @@ export class Model {
554
754
  packageName: this.packageName,
555
755
  modelPath: this.modelPath,
556
756
  malloyVersion: MALLOY_VERSION,
557
- modelInfo: JSON.stringify(
558
- this.modelDef ? modelDefToModelInfo(this.modelDef) : {},
559
- ),
757
+ modelInfo: JSON.stringify(this.modelInfo ?? {}),
560
758
  sources: this.modelDef && this.sources,
561
759
  queries: this.modelDef && this.queries,
562
760
  annotations: allAnnotations,
@@ -568,6 +766,10 @@ export class Model {
568
766
  cellIndex: number,
569
767
  filterParams?: FilterParams,
570
768
  bypassFilters?: boolean,
769
+ givens?: Record<string, GivenValue>,
770
+ // See `getQueryResults`: forwarded into `runnable.run` so the
771
+ // publisher's wall-clock timeout actually cancels the query.
772
+ abortSignal?: AbortSignal,
571
773
  ): Promise<{
572
774
  type: "code" | "markdown";
573
775
  text: string;
@@ -628,16 +830,40 @@ export class Model {
628
830
  }
629
831
  }
630
832
 
631
- const rowLimit =
632
- (await runnableToExecute.getPreparedResult()).resultExplore
633
- .limit || ROW_LIMIT;
634
- const result = await runnableToExecute.run({ rowLimit });
833
+ const cellMaxRows = getMaxQueryRows();
834
+ const cellMaxBytes = getMaxResponseBytes();
835
+ const rowLimit = resolveModelQueryRowLimit(
836
+ (await runnableToExecute.getPreparedResult({ givens }))
837
+ .resultExplore.limit,
838
+ {
839
+ defaultLimit: getDefaultQueryRowLimit(),
840
+ maxRows: cellMaxRows,
841
+ },
842
+ );
843
+ const result = await runnableToExecute.run({
844
+ rowLimit,
845
+ givens,
846
+ abortSignal,
847
+ });
635
848
  const query = (await runnableToExecute.getPreparedQuery())._query;
636
849
  queryName = (query as NamedQueryDef).as || query.name;
637
850
  queryResult =
638
851
  result?._queryResult &&
639
852
  this.modelInfo &&
640
853
  JSON.stringify(API.util.wrapResult(result));
854
+ // Same caveat as `getQueryResults`: by the time we measure
855
+ // bytes the response has already been buffered and stringified,
856
+ // so this is loud-failure detection (clean 413 instead of
857
+ // partial transmission), not OOM prevention. The row cap above
858
+ // is the primary defense.
859
+ if (result?._queryResult && queryResult) {
860
+ assertWithinModelResponseLimits(
861
+ result.totalRows,
862
+ Buffer.byteLength(queryResult, "utf8"),
863
+ { maxRows: cellMaxRows, maxBytes: cellMaxBytes },
864
+ "notebook_cell",
865
+ );
866
+ }
641
867
  } catch (error) {
642
868
  if (error instanceof FilterValidationError) {
643
869
  throw new BadRequestError(error.message);
@@ -645,6 +871,12 @@ export class Model {
645
871
  if (error instanceof MalloyError) {
646
872
  throw error;
647
873
  }
874
+ // Surface PayloadTooLargeError as-is so the error middleware
875
+ // maps it to HTTP 413; without this it would get swallowed
876
+ // into a generic 400 BadRequestError below.
877
+ if (error instanceof PayloadTooLargeError) {
878
+ throw error;
879
+ }
648
880
  const errorMessage =
649
881
  error instanceof Error ? error.message : String(error);
650
882
  if (errorMessage.trim() === "Model has no queries.") {
@@ -758,6 +990,7 @@ export class Model {
758
990
  private static getSources(
759
991
  modelPath: string,
760
992
  modelDef: ModelDef,
993
+ givens?: ApiGiven[],
761
994
  ): {
762
995
  sources: ApiSource[];
763
996
  filterMap: Map<string, FilterDefinition[]>;
@@ -846,6 +1079,12 @@ export class Model {
846
1079
  annotations,
847
1080
  views,
848
1081
  filters,
1082
+ // Malloy exposes givens at the model level, not per-source.
1083
+ // First pass: surface the full model-level list on every source
1084
+ // — matches how filter introspection already collapses
1085
+ // inheritance into the per-source list. Refine to view-scoped
1086
+ // filtering if a customer asks.
1087
+ givens,
849
1088
  } as ApiSource;
850
1089
  });
851
1090
 
@@ -1068,3 +1307,103 @@ export class Model {
1068
1307
  }
1069
1308
  }
1070
1309
  }
1310
+
1311
+ // ──────────────────────────────────────────────────────────────────────
1312
+ // Helpers for hydrating worker-compiled models on the main thread
1313
+ // ──────────────────────────────────────────────────────────────────────
1314
+
1315
+ /**
1316
+ * Minimal subset of `Runtime` we use here. The `_` methods are
1317
+ * marked `@internal` in Malloy but are the only API for constructing
1318
+ * a materializer / query materializer from an existing `modelDef` /
1319
+ * queryDef — the public `loadModel(url)` path always recompiles.
1320
+ */
1321
+ type HydrationRuntime = Runtime & {
1322
+ _loadModelFromModelDef(modelDef: ModelDef): ModelMaterializer;
1323
+ };
1324
+ type HydrationMaterializer = ModelMaterializer & {
1325
+ _loadQueryFromQueryDef(query: unknown): QueryMaterializer;
1326
+ };
1327
+
1328
+ function makeHydrationRuntime(
1329
+ malloyConfig: ModelConnectionInput,
1330
+ ): HydrationRuntime {
1331
+ const urlReader = new HackyDataStylesAccumulator(URL_READER);
1332
+ const config =
1333
+ malloyConfig instanceof MalloyConfig
1334
+ ? malloyConfig
1335
+ : (() => {
1336
+ const c = new MalloyConfig({ connections: {} });
1337
+ c.wrapConnections(
1338
+ () => new FixedConnectionMap(malloyConfig, "duckdb"),
1339
+ );
1340
+ return c;
1341
+ })();
1342
+ return new Runtime({ urlReader, config }) as HydrationRuntime;
1343
+ }
1344
+
1345
+ /**
1346
+ * Build the live `RunnableNotebookCell[]` from worker-emitted
1347
+ * per-cell data. Each MALLOY cell is hydrated via
1348
+ * `Runtime._loadModelFromModelDef` (for the cell's scope) and
1349
+ * `ModelMaterializer._loadQueryFromQueryDef` (for the cell's
1350
+ * runnable) — no recompile.
1351
+ */
1352
+ function hydrateNotebookCells(
1353
+ runtime: HydrationRuntime,
1354
+ notebookCells: SerializedNotebookCell[] | undefined,
1355
+ ): RunnableNotebookCell[] {
1356
+ if (!notebookCells) return [];
1357
+ return notebookCells.map((sc): RunnableNotebookCell => {
1358
+ if (sc.type === "markdown") {
1359
+ return { type: "markdown", text: sc.text };
1360
+ }
1361
+ const cellModelDef = sc.cellModelDef as ModelDef | undefined;
1362
+ let modelMaterializer: ModelMaterializer | undefined;
1363
+ let runnable: QueryMaterializer | undefined;
1364
+ if (cellModelDef) {
1365
+ modelMaterializer = runtime._loadModelFromModelDef(cellModelDef);
1366
+ if (sc.cellQueryDef !== undefined) {
1367
+ try {
1368
+ runnable = (
1369
+ modelMaterializer as HydrationMaterializer
1370
+ )._loadQueryFromQueryDef(sc.cellQueryDef);
1371
+ } catch (error) {
1372
+ // Hydration shouldn't fail for a queryDef the worker
1373
+ // already prepared, but if Malloy's internal shape
1374
+ // drifts we'd rather drop the runnable than crash the
1375
+ // whole notebook. The cell remains markdown-runnable.
1376
+ logger.warn("Failed to hydrate notebook cell queryDef", {
1377
+ error,
1378
+ });
1379
+ }
1380
+ }
1381
+ }
1382
+ return {
1383
+ type: "code",
1384
+ text: sc.text,
1385
+ runnable,
1386
+ modelMaterializer,
1387
+ newSources: sc.newSources as Malloy.SourceInfo[] | undefined,
1388
+ queryInfo: sc.queryInfo as Malloy.QueryInfo | undefined,
1389
+ };
1390
+ });
1391
+ }
1392
+
1393
+ /**
1394
+ * For an all-markdown notebook (no MALLOY statements → no
1395
+ * `modelDef`), we still want to preserve the cell list so
1396
+ * `getNotebook()` can serve raw text. This skips materializer
1397
+ * hydration (there's nothing to hydrate) and returns markdown-only
1398
+ * cells.
1399
+ */
1400
+ function hydrateMarkdownOnlyCells(
1401
+ notebookCells: SerializedNotebookCell[] | undefined,
1402
+ ): RunnableNotebookCell[] | undefined {
1403
+ if (!notebookCells) return undefined;
1404
+ return notebookCells.map((sc): RunnableNotebookCell => {
1405
+ if (sc.type === "markdown") return { type: "markdown", text: sc.text };
1406
+ // A code cell without a hydratable scope — surface text only.
1407
+ return { type: "code", text: sc.text };
1408
+ });
1409
+ }