@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.
- package/build.ts +30 -1
- package/dist/app/api-doc.yaml +127 -111
- package/dist/app/assets/{EnvironmentPage-C7rtH4mC.js → EnvironmentPage-CgKNjySu.js} +1 -1
- package/dist/app/assets/HomePage-BPIpMBjW.js +1 -0
- package/dist/app/assets/{MainPage-D38LtZDV.js → MainPage-CAwb8U82.js} +2 -2
- package/dist/app/assets/{ModelPage-DOol8Mz7.js → ModelPage-C0Uevsw9.js} +1 -1
- package/dist/app/assets/{PackagePage-0tgzA_kO.js → PackagePage-Cu-u9k1g.js} +1 -1
- package/dist/app/assets/{RouteError-BaMsOSly.js → RouteError-DVwPh2Ql.js} +1 -1
- package/dist/app/assets/{WorkbookPage-Cx4SePkx.js → WorkbookPage-DW38R2Zv.js} +1 -1
- package/dist/app/assets/{core-CbsC6R_Y.es-Cwf6asf3.js → core-C0vCMRDQ.es-D_ytHhjS.js} +10 -10
- package/dist/app/assets/{index-DL6BZTuw.js → index-BGdcKsFF.js} +1 -1
- package/dist/app/assets/{index-DNofXMxi.js → index-CTx4v4_3.js} +1 -1
- package/dist/app/assets/index-DE6d5jEy.js +452 -0
- package/dist/app/assets/{index.umd-B68wGGkM.js → index.umd-C1Mi1uRm.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 +4198 -3648
- package/package.json +2 -3
- package/src/config.spec.ts +246 -0
- package/src/config.ts +121 -1
- package/src/constants.ts +84 -1
- package/src/controller/compile.controller.ts +3 -1
- package/src/controller/connection.controller.spec.ts +803 -0
- package/src/controller/connection.controller.ts +207 -20
- package/src/controller/model.controller.ts +19 -1
- package/src/controller/query.controller.ts +22 -6
- package/src/controller/watch-mode.controller.ts +11 -2
- package/src/errors.spec.ts +44 -0
- package/src/errors.ts +34 -0
- package/src/health.spec.ts +90 -0
- package/src/health.ts +88 -45
- package/src/heap_check.spec.ts +144 -0
- package/src/heap_check.ts +144 -0
- package/src/instrumentation.ts +50 -0
- package/src/mcp/handler_utils.ts +14 -0
- package/src/mcp/tools/execute_query_tool.ts +52 -10
- package/src/oom_guards.integration.spec.ts +261 -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/path_safety.ts +9 -3
- package/src/query_cap_metrics.spec.ts +89 -0
- package/src/query_cap_metrics.ts +115 -0
- package/src/query_concurrency.spec.ts +247 -0
- package/src/query_concurrency.ts +236 -0
- package/src/query_param_utils.ts +18 -0
- package/src/query_timeout.spec.ts +224 -0
- package/src/query_timeout.ts +178 -0
- package/src/server-old.ts +21 -1
- package/src/server.ts +61 -57
- package/src/service/connection.ts +8 -2
- package/src/service/db_utils.spec.ts +1 -1
- package/src/service/environment.ts +85 -4
- package/src/service/environment_admission.spec.ts +165 -1
- package/src/service/environment_store.spec.ts +103 -0
- package/src/service/environment_store.ts +98 -26
- 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 +298 -3
- package/src/service/model.ts +362 -23
- package/src/service/model_limits.spec.ts +181 -0
- package/src/service/model_limits.ts +110 -0
- package/src/service/package.spec.ts +12 -6
- package/src/service/package.ts +263 -146
- package/src/service/package_worker_path.spec.ts +196 -0
- package/src/service/path_injection.spec.ts +39 -0
- package/src/stream_helpers.spec.ts +280 -0
- package/src/stream_helpers.ts +162 -0
- package/src/test_helpers/metrics_harness.ts +126 -0
- package/tests/integration/concurrent_package/concurrent_package.integration.spec.ts +280 -0
- package/dist/app/assets/HomePage-DwkH7OrS.js +0 -1
- package/dist/app/assets/index-U38AyjJL.js +0 -451
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,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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
} from "../
|
|
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.
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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
|
|
439
|
-
|
|
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:
|
|
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
|
|
505
|
-
|
|
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
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
+
}
|