@malloy-publisher/server 0.0.198 → 0.0.199
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build.ts +30 -1
- package/dist/app/api-doc.yaml +51 -0
- package/dist/app/assets/{EnvironmentPage-C7rtH4mC.js → EnvironmentPage-Dpee_Kn6.js} +1 -1
- package/dist/app/assets/{HomePage-DwkH7OrS.js → HomePage-DLRWTNoL.js} +1 -1
- package/dist/app/assets/{MainPage-D38LtZDV.js → MainPage-DsVt5QGM.js} +1 -1
- package/dist/app/assets/{ModelPage-DOol8Mz7.js → ModelPage-AwAugZ37.js} +1 -1
- package/dist/app/assets/{PackagePage-0tgzA_kO.js → PackagePage-XQ-EWGTC.js} +1 -1
- package/dist/app/assets/{RouteError-BaMsOSly.js → RouteError-3Mv8JQw7.js} +1 -1
- package/dist/app/assets/{WorkbookPage-Cx4SePkx.js → WorkbookPage-DHYYpcYc.js} +1 -1
- package/dist/app/assets/{core-CbsC6R_Y.es-Cwf6asf3.js → core-DfcpQGVP.es-DQggNOdX.js} +1 -1
- package/dist/app/assets/{index-DNofXMxi.js → index-BUp81Qdm.js} +1 -1
- package/dist/app/assets/{index-DL6BZTuw.js → index-D1pdwrUW.js} +1 -1
- package/dist/app/assets/{index-U38AyjJL.js → index-Dv5bF4Ii.js} +4 -4
- package/dist/app/assets/{index.umd-B68wGGkM.js → index.umd-CQH4LZU8.js} +1 -1
- package/dist/app/index.html +1 -1
- package/dist/instrumentation.mjs +57 -36
- package/dist/package_load_worker.mjs +12213 -0
- package/dist/server.mjs +2807 -2729
- package/package.json +2 -3
- package/src/controller/compile.controller.ts +3 -1
- package/src/controller/model.controller.ts +8 -1
- package/src/controller/query.controller.ts +3 -0
- package/src/health.spec.ts +90 -0
- package/src/health.ts +88 -45
- package/src/instrumentation.ts +50 -0
- package/src/mcp/tools/execute_query_tool.ts +12 -0
- package/src/package_load/package_load_pool.spec.ts +252 -0
- package/src/package_load/package_load_pool.ts +920 -0
- package/src/package_load/package_load_worker.ts +980 -0
- package/src/package_load/protocol.ts +336 -0
- package/src/query_param_utils.ts +18 -0
- package/src/server-old.ts +1 -1
- package/src/server.ts +36 -10
- package/src/service/db_utils.spec.ts +1 -1
- package/src/service/environment.ts +3 -2
- package/src/service/environment_store.ts +24 -3
- package/src/service/filter_integration.spec.ts +110 -0
- package/src/service/given.ts +80 -0
- package/src/service/givens_integration.spec.ts +192 -0
- package/src/service/model.spec.ts +105 -0
- package/src/service/model.ts +287 -16
- package/src/service/package.spec.ts +10 -0
- package/src/service/package.ts +257 -145
- package/src/service/package_worker_path.spec.ts +196 -0
- package/tests/integration/concurrent_package/concurrent_package.integration.spec.ts +280 -0
package/src/service/model.ts
CHANGED
|
@@ -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,6 +29,11 @@ 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
38
|
MODEL_FILE_SUFFIX,
|
|
33
39
|
NOTEBOOK_FILE_SUFFIX,
|
|
@@ -50,12 +56,14 @@ import {
|
|
|
50
56
|
type FilterDefinition,
|
|
51
57
|
type FilterParams,
|
|
52
58
|
} from "./filter";
|
|
59
|
+
import { malloyGivenToApi, type MalloyGiven } from "./given";
|
|
53
60
|
|
|
54
61
|
type ApiCompiledModel = components["schemas"]["CompiledModel"];
|
|
55
62
|
type ApiNotebookCell = components["schemas"]["NotebookCell"];
|
|
56
63
|
type ApiRawNotebook = components["schemas"]["RawNotebook"];
|
|
57
64
|
type ApiSource = components["schemas"]["Source"];
|
|
58
65
|
type ApiFilter = components["schemas"]["Filter"];
|
|
66
|
+
type ApiGiven = components["schemas"]["Given"];
|
|
59
67
|
type ApiView = components["schemas"]["View"];
|
|
60
68
|
type ApiQuery = components["schemas"]["Query"];
|
|
61
69
|
export type ApiConnection = components["schemas"]["Connection"];
|
|
@@ -98,6 +106,10 @@ export class Model {
|
|
|
98
106
|
private compilationError: MalloyError | Error | undefined;
|
|
99
107
|
/** Parsed #(filter) definitions keyed by source name. */
|
|
100
108
|
private filterMap: Map<string, FilterDefinition[]>;
|
|
109
|
+
/** Givens declared on the model, in declaration order. Malloy's
|
|
110
|
+
* `Model.givens` already collapses inheritance; we just stash the list
|
|
111
|
+
* for surfacing on the compiled-model response. */
|
|
112
|
+
private givens: ApiGiven[] | undefined;
|
|
101
113
|
private meter = metrics.getMeter("publisher");
|
|
102
114
|
private queryExecutionHistogram = this.meter.createHistogram(
|
|
103
115
|
"malloy_model_query_duration",
|
|
@@ -121,6 +133,16 @@ export class Model {
|
|
|
121
133
|
runnableNotebookCells: RunnableNotebookCell[] | undefined,
|
|
122
134
|
compilationError: MalloyError | Error | undefined,
|
|
123
135
|
filterMap?: Map<string, FilterDefinition[]>,
|
|
136
|
+
givens?: ApiGiven[],
|
|
137
|
+
/**
|
|
138
|
+
* Precomputed `modelDefToModelInfo(modelDef)`. The package-load
|
|
139
|
+
* worker emits it as part of `SerializedModel` so we don't
|
|
140
|
+
* re-derive it on every package load. Callers that build a
|
|
141
|
+
* `Model` from a raw `modelDef` (e.g. test fixtures via
|
|
142
|
+
* `Model.create`) can omit this and let the constructor
|
|
143
|
+
* derive it lazily.
|
|
144
|
+
*/
|
|
145
|
+
modelInfo?: Malloy.ModelInfo,
|
|
124
146
|
) {
|
|
125
147
|
this.packageName = packageName;
|
|
126
148
|
this.modelPath = modelPath;
|
|
@@ -134,9 +156,10 @@ export class Model {
|
|
|
134
156
|
this.runnableNotebookCells = runnableNotebookCells;
|
|
135
157
|
this.compilationError = compilationError;
|
|
136
158
|
this.filterMap = filterMap ?? new Map();
|
|
137
|
-
this.
|
|
138
|
-
|
|
139
|
-
|
|
159
|
+
this.givens = givens;
|
|
160
|
+
this.modelInfo =
|
|
161
|
+
modelInfo ??
|
|
162
|
+
(this.modelDef ? modelDefToModelInfo(this.modelDef) : undefined);
|
|
140
163
|
}
|
|
141
164
|
|
|
142
165
|
/**
|
|
@@ -158,6 +181,15 @@ export class Model {
|
|
|
158
181
|
return runMatch?.[1] ?? arrowMatch?.[1];
|
|
159
182
|
}
|
|
160
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Compile a single model in-process. Kept as a library entry point
|
|
186
|
+
* for test fixtures and any future caller that needs an ad-hoc
|
|
187
|
+
* `Model` from a `.malloy` / `.malloynb` file. Production package
|
|
188
|
+
* loads (`Package.create`) and reloads (`Package.reloadAllModels`)
|
|
189
|
+
* route through the package-load worker pool and dispatch through
|
|
190
|
+
* {@link Model.fromSerialized} instead — neither calls this on the
|
|
191
|
+
* main thread.
|
|
192
|
+
*/
|
|
161
193
|
public static async create(
|
|
162
194
|
packageName: string,
|
|
163
195
|
packagePath: string,
|
|
@@ -188,10 +220,21 @@ export class Model {
|
|
|
188
220
|
let sources = undefined;
|
|
189
221
|
let queries = undefined;
|
|
190
222
|
let filterMap: Map<string, FilterDefinition[]> | undefined;
|
|
223
|
+
let givens: ApiGiven[] | undefined;
|
|
191
224
|
const sourceInfos: Malloy.SourceInfo[] = [];
|
|
192
225
|
if (modelMaterializer) {
|
|
193
|
-
|
|
194
|
-
|
|
226
|
+
const compiledModel = await modelMaterializer.getModel();
|
|
227
|
+
modelDef = compiledModel._modelDef;
|
|
228
|
+
// Malloy's `Model.givens` already collapses inheritance from imports
|
|
229
|
+
// and applies any `finalizeGivens` runtime config. Just read it.
|
|
230
|
+
const malloyGivens = Array.from(
|
|
231
|
+
compiledModel.givens.values(),
|
|
232
|
+
) as MalloyGiven[];
|
|
233
|
+
givens =
|
|
234
|
+
malloyGivens.length > 0
|
|
235
|
+
? (malloyGivens.map(malloyGivenToApi) as ApiGiven[])
|
|
236
|
+
: undefined;
|
|
237
|
+
const sourceResult = Model.getSources(modelPath, modelDef, givens);
|
|
195
238
|
sources = sourceResult.sources;
|
|
196
239
|
filterMap = sourceResult.filterMap;
|
|
197
240
|
queries = Model.getQueries(modelPath, modelDef);
|
|
@@ -255,6 +298,7 @@ export class Model {
|
|
|
255
298
|
runnableNotebookCells,
|
|
256
299
|
undefined,
|
|
257
300
|
filterMap,
|
|
301
|
+
givens,
|
|
258
302
|
);
|
|
259
303
|
} catch (error) {
|
|
260
304
|
let computedError = error;
|
|
@@ -285,6 +329,123 @@ export class Model {
|
|
|
285
329
|
}
|
|
286
330
|
}
|
|
287
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Construct a `Model` from a worker-compiled `SerializedModel`. All
|
|
334
|
+
* the heavy compile work (parse, type check, IR build, sourceInfo
|
|
335
|
+
* extraction, per-cell notebook compile) already ran inside a
|
|
336
|
+
* `worker_threads` worker; this factory just rewraps the wire data
|
|
337
|
+
* into a live `Model`.
|
|
338
|
+
*
|
|
339
|
+
* Hydrates the `ModelMaterializer` (and, for notebooks, the
|
|
340
|
+
* per-cell materializers + runnables) **eagerly** via
|
|
341
|
+
* `Runtime._loadModelFromModelDef` /
|
|
342
|
+
* `ModelMaterializer._loadQueryFromQueryDef`. These are constant-time
|
|
343
|
+
* wraps around the worker's pre-compiled `modelDef` / `queryDef` —
|
|
344
|
+
* no parse, no type-check, no schema fetch — so doing it here at
|
|
345
|
+
* package-load time costs microseconds per model and keeps the
|
|
346
|
+
* resulting `Model` interchangeable with one produced by
|
|
347
|
+
* `Model.create` (no lazy-init branches in the hot path).
|
|
348
|
+
*/
|
|
349
|
+
public static fromSerialized(
|
|
350
|
+
packageName: string,
|
|
351
|
+
_packagePath: string,
|
|
352
|
+
malloyConfig: ModelConnectionInput,
|
|
353
|
+
data: SerializedModel,
|
|
354
|
+
): Model {
|
|
355
|
+
const modelDef = data.modelDef as ModelDef | undefined;
|
|
356
|
+
const modelInfo = data.modelInfo as Malloy.ModelInfo | undefined;
|
|
357
|
+
const dataStyles = (data.dataStyles ?? {}) as DataStyles;
|
|
358
|
+
const sources = data.sources as ApiSource[] | undefined;
|
|
359
|
+
const queries = data.queries as ApiQuery[] | undefined;
|
|
360
|
+
const sourceInfos = data.sourceInfos as Malloy.SourceInfo[] | undefined;
|
|
361
|
+
const givens = data.givens as ApiGiven[] | undefined;
|
|
362
|
+
const filterMap = data.filterMap
|
|
363
|
+
? new Map(data.filterMap as Array<[string, FilterDefinition[]]>)
|
|
364
|
+
: undefined;
|
|
365
|
+
|
|
366
|
+
// No modelDef → either an empty notebook (no MALLOY statements)
|
|
367
|
+
// or a corrupt worker payload. Build a Model with no materializer;
|
|
368
|
+
// downstream getQueryResults / executeNotebookCell will throw a
|
|
369
|
+
// clean BadRequestError if a caller tries to run a query. We
|
|
370
|
+
// still preserve markdown cells for an all-markdown notebook so
|
|
371
|
+
// `getNotebook()` can serve raw text.
|
|
372
|
+
if (!modelDef) {
|
|
373
|
+
return new Model(
|
|
374
|
+
packageName,
|
|
375
|
+
data.modelPath,
|
|
376
|
+
dataStyles,
|
|
377
|
+
data.modelType,
|
|
378
|
+
undefined,
|
|
379
|
+
undefined,
|
|
380
|
+
sources,
|
|
381
|
+
queries,
|
|
382
|
+
sourceInfos,
|
|
383
|
+
data.modelType === "notebook"
|
|
384
|
+
? hydrateMarkdownOnlyCells(data.notebookCells)
|
|
385
|
+
: undefined,
|
|
386
|
+
undefined,
|
|
387
|
+
filterMap,
|
|
388
|
+
givens,
|
|
389
|
+
modelInfo,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const runtime = makeHydrationRuntime(malloyConfig);
|
|
394
|
+
const modelMaterializer = runtime._loadModelFromModelDef(modelDef);
|
|
395
|
+
const runnableNotebookCells =
|
|
396
|
+
data.modelType === "notebook"
|
|
397
|
+
? hydrateNotebookCells(runtime, data.notebookCells)
|
|
398
|
+
: undefined;
|
|
399
|
+
|
|
400
|
+
return new Model(
|
|
401
|
+
packageName,
|
|
402
|
+
data.modelPath,
|
|
403
|
+
dataStyles,
|
|
404
|
+
data.modelType,
|
|
405
|
+
modelMaterializer,
|
|
406
|
+
modelDef,
|
|
407
|
+
sources,
|
|
408
|
+
queries,
|
|
409
|
+
sourceInfos,
|
|
410
|
+
runnableNotebookCells,
|
|
411
|
+
undefined, // compilationError
|
|
412
|
+
filterMap,
|
|
413
|
+
givens,
|
|
414
|
+
modelInfo,
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Build a Model representing a compilation failure (no modelDef,
|
|
420
|
+
* no materializer). Matches the shape `Model.create` returns when
|
|
421
|
+
* it catches a `MalloyError`, so the rest of the system handles
|
|
422
|
+
* both paths uniformly (the iteration loop in `Package.create`
|
|
423
|
+
* reads `compilationError` via a structural cast).
|
|
424
|
+
*/
|
|
425
|
+
public static fromCompilationError(
|
|
426
|
+
packageName: string,
|
|
427
|
+
modelPath: string,
|
|
428
|
+
modelType: ModelType,
|
|
429
|
+
error: Error,
|
|
430
|
+
): Model {
|
|
431
|
+
return new Model(
|
|
432
|
+
packageName,
|
|
433
|
+
modelPath,
|
|
434
|
+
{} as DataStyles,
|
|
435
|
+
modelType,
|
|
436
|
+
undefined,
|
|
437
|
+
undefined,
|
|
438
|
+
undefined,
|
|
439
|
+
undefined,
|
|
440
|
+
undefined,
|
|
441
|
+
undefined,
|
|
442
|
+
error,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** Look up the deserialized error helper for callers (e.g. Package.create). */
|
|
447
|
+
public static deserializeCompilationError = deserializeError;
|
|
448
|
+
|
|
288
449
|
public getPath(): string {
|
|
289
450
|
return this.modelPath;
|
|
290
451
|
}
|
|
@@ -342,6 +503,7 @@ export class Model {
|
|
|
342
503
|
query?: string,
|
|
343
504
|
filterParams?: FilterParams,
|
|
344
505
|
bypassFilters?: boolean,
|
|
506
|
+
givens?: Record<string, GivenValue>,
|
|
345
507
|
): Promise<{
|
|
346
508
|
result: Malloy.Result;
|
|
347
509
|
compactResult: QueryData;
|
|
@@ -436,13 +598,14 @@ export class Model {
|
|
|
436
598
|
}
|
|
437
599
|
|
|
438
600
|
const rowLimit =
|
|
439
|
-
(await runnable.getPreparedResult()).resultExplore.limit ||
|
|
601
|
+
(await runnable.getPreparedResult({ givens })).resultExplore.limit ||
|
|
602
|
+
ROW_LIMIT;
|
|
440
603
|
const endTime = performance.now();
|
|
441
604
|
const executionTime = endTime - startTime;
|
|
442
605
|
|
|
443
606
|
let queryResults;
|
|
444
607
|
try {
|
|
445
|
-
queryResults = await runnable.run({ rowLimit });
|
|
608
|
+
queryResults = await runnable.run({ rowLimit, givens });
|
|
446
609
|
} catch (error) {
|
|
447
610
|
// Record error metrics
|
|
448
611
|
const errorEndTime = performance.now();
|
|
@@ -501,14 +664,16 @@ export class Model {
|
|
|
501
664
|
malloyVersion: MALLOY_VERSION,
|
|
502
665
|
dataStyles: JSON.stringify(this.dataStyles),
|
|
503
666
|
modelDef: JSON.stringify(this.modelDef),
|
|
504
|
-
modelInfo
|
|
505
|
-
|
|
506
|
-
|
|
667
|
+
// `this.modelInfo` is precomputed once at construction (either
|
|
668
|
+
// by the worker or in the Model.create constructor); don't
|
|
669
|
+
// re-run `modelDefToModelInfo` on every API hit.
|
|
670
|
+
modelInfo: JSON.stringify(this.modelInfo ?? {}),
|
|
507
671
|
sourceInfos: this.getSourceInfos()?.map((sourceInfo) =>
|
|
508
672
|
JSON.stringify(sourceInfo),
|
|
509
673
|
),
|
|
510
674
|
sources: this.sources,
|
|
511
675
|
queries: this.queries,
|
|
676
|
+
givens: this.givens,
|
|
512
677
|
} as ApiCompiledModel;
|
|
513
678
|
}
|
|
514
679
|
|
|
@@ -554,9 +719,7 @@ export class Model {
|
|
|
554
719
|
packageName: this.packageName,
|
|
555
720
|
modelPath: this.modelPath,
|
|
556
721
|
malloyVersion: MALLOY_VERSION,
|
|
557
|
-
modelInfo: JSON.stringify(
|
|
558
|
-
this.modelDef ? modelDefToModelInfo(this.modelDef) : {},
|
|
559
|
-
),
|
|
722
|
+
modelInfo: JSON.stringify(this.modelInfo ?? {}),
|
|
560
723
|
sources: this.modelDef && this.sources,
|
|
561
724
|
queries: this.modelDef && this.queries,
|
|
562
725
|
annotations: allAnnotations,
|
|
@@ -568,6 +731,7 @@ export class Model {
|
|
|
568
731
|
cellIndex: number,
|
|
569
732
|
filterParams?: FilterParams,
|
|
570
733
|
bypassFilters?: boolean,
|
|
734
|
+
givens?: Record<string, GivenValue>,
|
|
571
735
|
): Promise<{
|
|
572
736
|
type: "code" | "markdown";
|
|
573
737
|
text: string;
|
|
@@ -629,9 +793,9 @@ export class Model {
|
|
|
629
793
|
}
|
|
630
794
|
|
|
631
795
|
const rowLimit =
|
|
632
|
-
(await runnableToExecute.getPreparedResult())
|
|
633
|
-
.limit || ROW_LIMIT;
|
|
634
|
-
const result = await runnableToExecute.run({ rowLimit });
|
|
796
|
+
(await runnableToExecute.getPreparedResult({ givens }))
|
|
797
|
+
.resultExplore.limit || ROW_LIMIT;
|
|
798
|
+
const result = await runnableToExecute.run({ rowLimit, givens });
|
|
635
799
|
const query = (await runnableToExecute.getPreparedQuery())._query;
|
|
636
800
|
queryName = (query as NamedQueryDef).as || query.name;
|
|
637
801
|
queryResult =
|
|
@@ -758,6 +922,7 @@ export class Model {
|
|
|
758
922
|
private static getSources(
|
|
759
923
|
modelPath: string,
|
|
760
924
|
modelDef: ModelDef,
|
|
925
|
+
givens?: ApiGiven[],
|
|
761
926
|
): {
|
|
762
927
|
sources: ApiSource[];
|
|
763
928
|
filterMap: Map<string, FilterDefinition[]>;
|
|
@@ -846,6 +1011,12 @@ export class Model {
|
|
|
846
1011
|
annotations,
|
|
847
1012
|
views,
|
|
848
1013
|
filters,
|
|
1014
|
+
// Malloy exposes givens at the model level, not per-source.
|
|
1015
|
+
// First pass: surface the full model-level list on every source
|
|
1016
|
+
// — matches how filter introspection already collapses
|
|
1017
|
+
// inheritance into the per-source list. Refine to view-scoped
|
|
1018
|
+
// filtering if a customer asks.
|
|
1019
|
+
givens,
|
|
849
1020
|
} as ApiSource;
|
|
850
1021
|
});
|
|
851
1022
|
|
|
@@ -1068,3 +1239,103 @@ export class Model {
|
|
|
1068
1239
|
}
|
|
1069
1240
|
}
|
|
1070
1241
|
}
|
|
1242
|
+
|
|
1243
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
1244
|
+
// Helpers for hydrating worker-compiled models on the main thread
|
|
1245
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
1246
|
+
|
|
1247
|
+
/**
|
|
1248
|
+
* Minimal subset of `Runtime` we use here. The `_` methods are
|
|
1249
|
+
* marked `@internal` in Malloy but are the only API for constructing
|
|
1250
|
+
* a materializer / query materializer from an existing `modelDef` /
|
|
1251
|
+
* queryDef — the public `loadModel(url)` path always recompiles.
|
|
1252
|
+
*/
|
|
1253
|
+
type HydrationRuntime = Runtime & {
|
|
1254
|
+
_loadModelFromModelDef(modelDef: ModelDef): ModelMaterializer;
|
|
1255
|
+
};
|
|
1256
|
+
type HydrationMaterializer = ModelMaterializer & {
|
|
1257
|
+
_loadQueryFromQueryDef(query: unknown): QueryMaterializer;
|
|
1258
|
+
};
|
|
1259
|
+
|
|
1260
|
+
function makeHydrationRuntime(
|
|
1261
|
+
malloyConfig: ModelConnectionInput,
|
|
1262
|
+
): HydrationRuntime {
|
|
1263
|
+
const urlReader = new HackyDataStylesAccumulator(URL_READER);
|
|
1264
|
+
const config =
|
|
1265
|
+
malloyConfig instanceof MalloyConfig
|
|
1266
|
+
? malloyConfig
|
|
1267
|
+
: (() => {
|
|
1268
|
+
const c = new MalloyConfig({ connections: {} });
|
|
1269
|
+
c.wrapConnections(
|
|
1270
|
+
() => new FixedConnectionMap(malloyConfig, "duckdb"),
|
|
1271
|
+
);
|
|
1272
|
+
return c;
|
|
1273
|
+
})();
|
|
1274
|
+
return new Runtime({ urlReader, config }) as HydrationRuntime;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* Build the live `RunnableNotebookCell[]` from worker-emitted
|
|
1279
|
+
* per-cell data. Each MALLOY cell is hydrated via
|
|
1280
|
+
* `Runtime._loadModelFromModelDef` (for the cell's scope) and
|
|
1281
|
+
* `ModelMaterializer._loadQueryFromQueryDef` (for the cell's
|
|
1282
|
+
* runnable) — no recompile.
|
|
1283
|
+
*/
|
|
1284
|
+
function hydrateNotebookCells(
|
|
1285
|
+
runtime: HydrationRuntime,
|
|
1286
|
+
notebookCells: SerializedNotebookCell[] | undefined,
|
|
1287
|
+
): RunnableNotebookCell[] {
|
|
1288
|
+
if (!notebookCells) return [];
|
|
1289
|
+
return notebookCells.map((sc): RunnableNotebookCell => {
|
|
1290
|
+
if (sc.type === "markdown") {
|
|
1291
|
+
return { type: "markdown", text: sc.text };
|
|
1292
|
+
}
|
|
1293
|
+
const cellModelDef = sc.cellModelDef as ModelDef | undefined;
|
|
1294
|
+
let modelMaterializer: ModelMaterializer | undefined;
|
|
1295
|
+
let runnable: QueryMaterializer | undefined;
|
|
1296
|
+
if (cellModelDef) {
|
|
1297
|
+
modelMaterializer = runtime._loadModelFromModelDef(cellModelDef);
|
|
1298
|
+
if (sc.cellQueryDef !== undefined) {
|
|
1299
|
+
try {
|
|
1300
|
+
runnable = (
|
|
1301
|
+
modelMaterializer as HydrationMaterializer
|
|
1302
|
+
)._loadQueryFromQueryDef(sc.cellQueryDef);
|
|
1303
|
+
} catch (error) {
|
|
1304
|
+
// Hydration shouldn't fail for a queryDef the worker
|
|
1305
|
+
// already prepared, but if Malloy's internal shape
|
|
1306
|
+
// drifts we'd rather drop the runnable than crash the
|
|
1307
|
+
// whole notebook. The cell remains markdown-runnable.
|
|
1308
|
+
logger.warn("Failed to hydrate notebook cell queryDef", {
|
|
1309
|
+
error,
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
return {
|
|
1315
|
+
type: "code",
|
|
1316
|
+
text: sc.text,
|
|
1317
|
+
runnable,
|
|
1318
|
+
modelMaterializer,
|
|
1319
|
+
newSources: sc.newSources as Malloy.SourceInfo[] | undefined,
|
|
1320
|
+
queryInfo: sc.queryInfo as Malloy.QueryInfo | undefined,
|
|
1321
|
+
};
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/**
|
|
1326
|
+
* For an all-markdown notebook (no MALLOY statements → no
|
|
1327
|
+
* `modelDef`), we still want to preserve the cell list so
|
|
1328
|
+
* `getNotebook()` can serve raw text. This skips materializer
|
|
1329
|
+
* hydration (there's nothing to hydrate) and returns markdown-only
|
|
1330
|
+
* cells.
|
|
1331
|
+
*/
|
|
1332
|
+
function hydrateMarkdownOnlyCells(
|
|
1333
|
+
notebookCells: SerializedNotebookCell[] | undefined,
|
|
1334
|
+
): RunnableNotebookCell[] | undefined {
|
|
1335
|
+
if (!notebookCells) return undefined;
|
|
1336
|
+
return notebookCells.map((sc): RunnableNotebookCell => {
|
|
1337
|
+
if (sc.type === "markdown") return { type: "markdown", text: sc.text };
|
|
1338
|
+
// A code cell without a hydratable scope — surface text only.
|
|
1339
|
+
return { type: "code", text: sc.text };
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { DuckDBConnection } from "@malloydata/db-duckdb";
|
|
2
|
+
import "@malloydata/db-duckdb/native";
|
|
1
3
|
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
4
|
import { Stats } from "fs";
|
|
3
5
|
import fs from "fs/promises";
|
|
@@ -340,10 +342,18 @@ describe("service/package", () => {
|
|
|
340
342
|
it("should return the size of the database file", async () => {
|
|
341
343
|
sinon.stub(fs, "stat").resolves({ size: 13 } as Stats);
|
|
342
344
|
|
|
345
|
+
// `getDatabaseInfo` now requires the caller to pass in the
|
|
346
|
+
// shared DuckDB connection (resolved once by `readDatabases`
|
|
347
|
+
// off the package's MalloyConfig). For this isolated unit
|
|
348
|
+
// test we mint a fresh ephemeral one — production paths
|
|
349
|
+
// reuse a single connection per package via `Package.create`.
|
|
350
|
+
const conn = new DuckDBConnection("duckdb");
|
|
351
|
+
|
|
343
352
|
// @ts-expect-error Accessing private static method for testing
|
|
344
353
|
const info = await Package.getDatabaseInfo(
|
|
345
354
|
testPackageDirectory,
|
|
346
355
|
"database.csv",
|
|
356
|
+
conn,
|
|
347
357
|
);
|
|
348
358
|
|
|
349
359
|
expect(info).toEqual({
|