@malloy-publisher/server 0.0.231 → 0.0.233
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/README.docker.md +4 -0
- package/dist/app/api-doc.yaml +135 -13
- package/dist/app/assets/{EnvironmentPage-wa_EPkwK.js → EnvironmentPage-DutP7T8h.js} +1 -1
- package/dist/app/assets/{HomePage-jnCrupQp.js → HomePage-BcxDrBfl.js} +1 -1
- package/dist/app/assets/{LightMode-DYbwNULZ.js → LightMode-BJukGxgz.js} +1 -1
- package/dist/app/assets/{MainPage-CuJLrPNI.js → MainPage-DXbwlMeF.js} +2 -2
- package/dist/app/assets/{MaterializationsPage-D_67x2ee.js → MaterializationsPage-BBQksmTU.js} +1 -1
- package/dist/app/assets/{ModelPage-D5JtAWqR.js → ModelPage-C6tK51uU.js} +1 -1
- package/dist/app/assets/{PackagePage-BRwtqUSG.js → PackagePage-Bo3cwwZE.js} +1 -1
- package/dist/app/assets/{RouteError-CBNNrnSD.js → RouteError-BufkcAKE.js} +1 -1
- package/dist/app/assets/{ThemeEditorPage-CTCeBneA.js → ThemeEditorPage-DICvvKpa.js} +1 -1
- package/dist/app/assets/{WorkbookPage-SN6f1RBm.js → WorkbookPage-Dkwt75Nj.js} +1 -1
- package/dist/app/assets/{core-Dp3q5Ieu.es-CD5FvM2s.js → core-C0nunIQT.es-DlMLKZBK.js} +1 -1
- package/dist/app/assets/{index-DU4r7GdU.js → index-BabP-V-S.js} +346 -321
- package/dist/app/assets/{index-BLCx1EdC.js → index-BusxL5Pt.js} +1 -1
- package/dist/app/assets/{index-C_tJstcx.js → index-CmEVVe-8.js} +15 -15
- package/dist/app/assets/{index-CfmBVB6M.js → index-Cs4WVm2z.js} +1 -1
- package/dist/app/assets/{index-B3Nn8Vm2.js → index-qnhU9CGo.js} +266 -265
- package/dist/app/index.html +1 -1
- package/dist/instrumentation.mjs +2 -0
- package/dist/package_load_worker.mjs +11 -1
- package/dist/server.mjs +22627 -1260
- package/package.json +12 -12
- package/scripts/bake-duckdb-extensions.js +4 -1
- package/src/config.spec.ts +39 -0
- package/src/config.ts +135 -0
- package/src/controller/materialization.controller.spec.ts +62 -0
- package/src/controller/materialization.controller.ts +15 -0
- package/src/controller/package.controller.spec.ts +6 -0
- package/src/controller/package.controller.ts +7 -2
- package/src/controller/query.controller.ts +26 -21
- package/src/errors.ts +19 -0
- package/src/json_utils.spec.ts +51 -0
- package/src/json_utils.ts +33 -0
- package/src/logger.spec.ts +18 -1
- package/src/logger.ts +3 -1
- package/src/materialization_metrics.spec.ts +89 -4
- package/src/materialization_metrics.ts +155 -5
- package/src/mcp/query_envelope.spec.ts +229 -0
- package/src/mcp/query_envelope.ts +230 -0
- package/src/mcp/server.protocol.spec.ts +128 -16
- package/src/mcp/skills/build_skills_bundle.ts +94 -4
- package/src/mcp/skills/skills_bundle.json +1 -1
- package/src/mcp/skills/skills_bundle.spec.ts +113 -4
- package/src/mcp/tool_response.spec.ts +108 -0
- package/src/mcp/tool_response.ts +138 -0
- package/src/mcp/tools/compile_tool.spec.ts +112 -4
- package/src/mcp/tools/compile_tool.ts +61 -30
- package/src/mcp/tools/docs_search_tool.ts +6 -16
- package/src/mcp/tools/embedding_index.spec.ts +1236 -0
- package/src/mcp/tools/embedding_index.ts +808 -0
- package/src/mcp/tools/execute_query_tool.spec.ts +23 -3
- package/src/mcp/tools/execute_query_tool.ts +90 -151
- package/src/mcp/tools/get_context_eval.ts +194 -45
- package/src/mcp/tools/get_context_tool.spec.ts +358 -5
- package/src/mcp/tools/get_context_tool.ts +202 -56
- package/src/mcp/tools/reload_package_tool.ts +3 -29
- package/src/pg_helpers.spec.ts +201 -0
- package/src/pg_helpers.ts +44 -5
- package/src/server.ts +24 -0
- package/src/service/build_plan.spec.ts +128 -2
- package/src/service/build_plan.ts +239 -17
- package/src/service/compile_fragment_techniques.spec.ts +156 -0
- package/src/service/connection.spec.ts +371 -1
- package/src/service/connection.ts +339 -20
- package/src/service/connection_config.spec.ts +108 -0
- package/src/service/connection_config.ts +47 -8
- package/src/service/connection_federation.spec.ts +184 -0
- package/src/service/duckdb_instance_isolation.spec.ts +137 -0
- package/src/service/embedding_provider.spec.ts +329 -0
- package/src/service/embedding_provider.ts +236 -0
- package/src/service/environment.ts +274 -12
- package/src/service/environment_store.spec.ts +678 -3
- package/src/service/environment_store.ts +449 -33
- package/src/service/environment_store_clone.spec.ts +350 -0
- package/src/service/manifest_loader.spec.ts +68 -13
- package/src/service/manifest_loader.ts +67 -19
- package/src/service/materialization_build_session.spec.ts +435 -0
- package/src/service/materialization_build_session.ts +681 -0
- package/src/service/materialization_eligibility.spec.ts +158 -0
- package/src/service/materialization_eligibility.ts +305 -0
- package/src/service/materialization_serve_transform.spec.ts +1003 -0
- package/src/service/materialization_serve_transform.ts +779 -0
- package/src/service/materialization_service.spec.ts +774 -7
- package/src/service/materialization_service.ts +1107 -42
- package/src/service/materialization_test_fixtures.ts +7 -0
- package/src/service/model.spec.ts +207 -0
- package/src/service/model.ts +569 -59
- package/src/service/model_limits.spec.ts +28 -0
- package/src/service/model_limits.ts +21 -0
- package/src/service/model_storage_serve.spec.ts +193 -0
- package/src/service/model_storage_serve_joins.spec.ts +193 -0
- package/src/service/package.spec.ts +196 -0
- package/src/service/package.ts +385 -17
- package/src/service/persistence_policy.spec.ts +109 -0
- package/src/storage/duckdb/schema.ts +37 -0
- package/tests/fixtures/xlsx/database.xlsx +0 -0
- package/tests/integration/first_boot/readiness_line.integration.spec.ts +177 -0
- package/tests/integration/materialization/manifest_binding.integration.spec.ts +104 -0
- package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +37 -12
- package/tests/integration/mcp/mcp_get_context_semantic.integration.spec.ts +235 -0
package/src/service/model.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
Connection,
|
|
5
5
|
FixedConnectionMap,
|
|
6
6
|
GivenValue,
|
|
7
|
+
InMemoryURLReader,
|
|
7
8
|
isBasicArray,
|
|
8
9
|
isJoined,
|
|
9
10
|
isRepeatedRecord,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
Runtime,
|
|
20
21
|
type FieldDef,
|
|
21
22
|
type SourceDef,
|
|
23
|
+
type VirtualMap,
|
|
22
24
|
} from "@malloydata/malloy";
|
|
23
25
|
import * as Malloy from "@malloydata/malloy-interfaces";
|
|
24
26
|
import {
|
|
@@ -27,9 +29,15 @@ import {
|
|
|
27
29
|
} from "@malloydata/malloy-sql";
|
|
28
30
|
import { DataStyles } from "@malloydata/render";
|
|
29
31
|
import { publisherMeter } from "../telemetry";
|
|
32
|
+
import {
|
|
33
|
+
recordServeShapeTierDrop,
|
|
34
|
+
recordStorageServeRouting,
|
|
35
|
+
} from "../materialization_metrics";
|
|
30
36
|
import * as fs from "fs/promises";
|
|
37
|
+
import { readFileSync } from "fs";
|
|
31
38
|
import { createRequire } from "module";
|
|
32
39
|
import * as path from "path";
|
|
40
|
+
import { fileURLToPath } from "url";
|
|
33
41
|
import { components } from "../api";
|
|
34
42
|
import {
|
|
35
43
|
getDefaultQueryRowLimit,
|
|
@@ -46,7 +54,20 @@ import {
|
|
|
46
54
|
NotQueryableError,
|
|
47
55
|
PayloadTooLargeError,
|
|
48
56
|
} from "../errors";
|
|
57
|
+
import { getPersistStorageMode } from "../config";
|
|
49
58
|
import { logger } from "../logger";
|
|
59
|
+
import {
|
|
60
|
+
buildServeShapeModelForBindings,
|
|
61
|
+
buildVirtualMap,
|
|
62
|
+
extractJoins,
|
|
63
|
+
extractRefinements,
|
|
64
|
+
extractViews,
|
|
65
|
+
narrowSchemaToPublic,
|
|
66
|
+
sliceSourceRange,
|
|
67
|
+
type ServeBinding,
|
|
68
|
+
type SourceLocation,
|
|
69
|
+
} from "./materialization_serve_transform";
|
|
70
|
+
import { evaluateManifestFreshness } from "./freshness";
|
|
50
71
|
import { deserializeError } from "../package_load/package_load_pool";
|
|
51
72
|
import type {
|
|
52
73
|
SerializedModel,
|
|
@@ -71,6 +92,8 @@ import {
|
|
|
71
92
|
import { malloyGivenToApi, type MalloyGiven } from "./given";
|
|
72
93
|
import {
|
|
73
94
|
assertWithinModelResponseLimits,
|
|
95
|
+
type QueryRowLimitSource,
|
|
96
|
+
queryRowLimitSource,
|
|
74
97
|
resolveModelQueryRowLimit,
|
|
75
98
|
} from "./model_limits";
|
|
76
99
|
import { buildSourceAliasMap, extractRunTargetSourceName } from "./query_text";
|
|
@@ -164,6 +187,20 @@ export class Model {
|
|
|
164
187
|
private modelMaterializer: ModelMaterializer | undefined;
|
|
165
188
|
private modelDef: ModelDef | undefined;
|
|
166
189
|
private modelInfo: Malloy.ModelInfo | undefined;
|
|
190
|
+
/**
|
|
191
|
+
* Connection config to compile a transient serve-shape model against, when a
|
|
192
|
+
* query is routed through the `storage=` virtual-source transform. Captured
|
|
193
|
+
* at hydration (fromSerialized) so serve can build a fresh Runtime.
|
|
194
|
+
*/
|
|
195
|
+
private serveMalloyConfig?: ModelConnectionInput;
|
|
196
|
+
/**
|
|
197
|
+
* The package's `storage=` serve bindings, set by the owning Package when a
|
|
198
|
+
* build/manifest binds materialized-into-storage sources. Empty ⇒ no serve
|
|
199
|
+
* routing (the common case; the serve path is unchanged).
|
|
200
|
+
*/
|
|
201
|
+
private serveBindings: ServeBinding[] = [];
|
|
202
|
+
/** Memoized serve-shape materializer, keyed by the bound source set. */
|
|
203
|
+
private serveShapeCache?: { key: string; materializer: ModelMaterializer };
|
|
167
204
|
private sources: ApiSource[] | undefined;
|
|
168
205
|
private queries: ApiQuery[] | undefined;
|
|
169
206
|
private sourceInfos: Malloy.SourceInfo[] | undefined;
|
|
@@ -1275,7 +1312,7 @@ export class Model {
|
|
|
1275
1312
|
? hydrateNotebookCells(runtime, data.notebookCells)
|
|
1276
1313
|
: undefined;
|
|
1277
1314
|
|
|
1278
|
-
|
|
1315
|
+
const model = new Model(
|
|
1279
1316
|
packageName,
|
|
1280
1317
|
data.modelPath,
|
|
1281
1318
|
dataStyles,
|
|
@@ -1291,6 +1328,10 @@ export class Model {
|
|
|
1291
1328
|
givens,
|
|
1292
1329
|
modelInfo,
|
|
1293
1330
|
);
|
|
1331
|
+
// Capture the config so a storage= serve query can compile a transient
|
|
1332
|
+
// serve-shape model against the same connections.
|
|
1333
|
+
model.setServeMalloyConfig(malloyConfig);
|
|
1334
|
+
return model;
|
|
1294
1335
|
}
|
|
1295
1336
|
|
|
1296
1337
|
/**
|
|
@@ -1724,6 +1765,282 @@ export class Model {
|
|
|
1724
1765
|
}
|
|
1725
1766
|
}
|
|
1726
1767
|
|
|
1768
|
+
/** Capture the config used to compile a transient serve-shape model. */
|
|
1769
|
+
public setServeMalloyConfig(config: ModelConnectionInput): void {
|
|
1770
|
+
this.serveMalloyConfig = config;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
/**
|
|
1774
|
+
* Set (or clear) this model's `storage=` serve bindings. Invalidates the
|
|
1775
|
+
* memoized serve-shape materializer so the next routed query recompiles
|
|
1776
|
+
* against the new binding set.
|
|
1777
|
+
*/
|
|
1778
|
+
public setServeBindings(bindings: ServeBinding[]): void {
|
|
1779
|
+
this.serveBindings = bindings;
|
|
1780
|
+
this.serveShapeCache = undefined;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
/**
|
|
1784
|
+
* Compile an untrusted query against the transient serve-shape model that
|
|
1785
|
+
* rebinds this package's materialized-into-storage sources to virtual sources
|
|
1786
|
+
* on their storage connection. Returns the runnable and the `virtualMap` that
|
|
1787
|
+
* binds each virtual handle to its physical table.
|
|
1788
|
+
*
|
|
1789
|
+
* Throwing is the eligibility signal: a query that references a refinement
|
|
1790
|
+
* the serve shape does not reproduce (a measure/dim/join defined on the
|
|
1791
|
+
* source in the author's model, or a source with no binding) fails to compile
|
|
1792
|
+
* here, and {@link getQueryResults} falls back to serving it live. The
|
|
1793
|
+
* compiled materializer is memoized per binding set (the serve-variant cache).
|
|
1794
|
+
*/
|
|
1795
|
+
/**
|
|
1796
|
+
* The serve bindings that may serve from their materialized table right NOW:
|
|
1797
|
+
* fresh, un-gated, or stale-under-`stale_ok`. A stale binding whose fallback is
|
|
1798
|
+
* `live`/`fail` evaluates to `serve_live` and is dropped, so the serve shape
|
|
1799
|
+
* omits that source and any query touching it falls through to live — the SAME
|
|
1800
|
+
* freshness gate the colocated serve applies (`getFreshBuildManifest`
|
|
1801
|
+
* → `evaluateManifestFreshness`). Placement (`storage=`) is orthogonal to
|
|
1802
|
+
* freshness: flip a colocated build ↔ `storage=lake` and this behaves identically.
|
|
1803
|
+
*/
|
|
1804
|
+
private freshServeBindings(now: number): ServeBinding[] {
|
|
1805
|
+
const at = new Date(now);
|
|
1806
|
+
return this.serveBindings.filter(
|
|
1807
|
+
(b) =>
|
|
1808
|
+
evaluateManifestFreshness(
|
|
1809
|
+
{
|
|
1810
|
+
tableName: b.tablePath,
|
|
1811
|
+
dataAsOf: b.freshAsOf,
|
|
1812
|
+
freshnessWindowSeconds: b.freshnessWindowSeconds,
|
|
1813
|
+
freshnessFallback: b.freshnessFallback,
|
|
1814
|
+
},
|
|
1815
|
+
at,
|
|
1816
|
+
) === "serve_table",
|
|
1817
|
+
);
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
private async loadServeShapeQuery(queryString: string): Promise<{
|
|
1821
|
+
runnable: QueryMaterializer;
|
|
1822
|
+
virtualMap: VirtualMap;
|
|
1823
|
+
/**
|
|
1824
|
+
* The fresh bindings the shape was built from. Returned so a caller making
|
|
1825
|
+
* a per-binding decision reads the set that actually produced this shape
|
|
1826
|
+
* rather than the package-wide field.
|
|
1827
|
+
*/
|
|
1828
|
+
bindings: ServeBinding[];
|
|
1829
|
+
}> {
|
|
1830
|
+
// Gate by freshness first: only bindings that should serve their table now
|
|
1831
|
+
// enter the shape. Keying the cache on the FRESH subset means it recompiles
|
|
1832
|
+
// when a binding crosses its window (drops out) — the storage analogue of
|
|
1833
|
+
// the colocated path's memoized getFreshBuildManifest.
|
|
1834
|
+
const freshBindings = this.freshServeBindings(Date.now());
|
|
1835
|
+
if (freshBindings.length === 0) {
|
|
1836
|
+
// Every bound source is stale past its window with a live/fail fallback —
|
|
1837
|
+
// nothing to serve from storage; fall through to live (caller's catch).
|
|
1838
|
+
throw new Error("no fresh storage serve bindings for this query");
|
|
1839
|
+
}
|
|
1840
|
+
const key = freshBindings
|
|
1841
|
+
.map((b) => `${b.sourceName}@${b.connectionName}/${b.virtualHandle}`)
|
|
1842
|
+
.sort()
|
|
1843
|
+
.join("|");
|
|
1844
|
+
if (!this.serveShapeCache || this.serveShapeCache.key !== key) {
|
|
1845
|
+
this.serveShapeCache = {
|
|
1846
|
+
key,
|
|
1847
|
+
materializer: await this.compileServeShape(
|
|
1848
|
+
this.serveBindingsWithRefinements(freshBindings),
|
|
1849
|
+
),
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
const virtualMap = buildVirtualMap(freshBindings);
|
|
1853
|
+
const runnable =
|
|
1854
|
+
this.serveShapeCache.materializer.loadRestrictedQuery(queryString);
|
|
1855
|
+
// Compile eagerly so ineligibility (a refinement the serve shape lacks, an
|
|
1856
|
+
// unbound source, a bad connection) surfaces HERE — Malloy compiles lazily,
|
|
1857
|
+
// so without this the error would escape at prepare/run instead of at the
|
|
1858
|
+
// caller's try, defeating the safe fallback. Cheap relative to the run. The
|
|
1859
|
+
// serve shape is pure virtual sources, so no buildManifest is needed.
|
|
1860
|
+
await runnable.getSQL({ virtualMap });
|
|
1861
|
+
return { runnable, virtualMap, bindings: freshBindings };
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
/**
|
|
1865
|
+
* Compile the serve-shape materializer for a binding set, degrading
|
|
1866
|
+
* gracefully if the richest shape does not compile. A single un-carriable
|
|
1867
|
+
* refinement (a join or view that reaches a non-materialized source, a
|
|
1868
|
+
* non-portable expression) would otherwise fail the WHOLE shape's compile and
|
|
1869
|
+
* disable storage serving for every source in the package. So the shape is
|
|
1870
|
+
* validated once (per binding set — the result is cached) and, on failure,
|
|
1871
|
+
* the riskiest refinement category is dropped and it retries: full → drop
|
|
1872
|
+
* views → drop views + joins → base-only. Base-only is pure virtual sources
|
|
1873
|
+
* and always compiles, so it is the guaranteed floor. Each surviving tier
|
|
1874
|
+
* still serves everything it can; the per-query eager compile in
|
|
1875
|
+
* {@link loadServeShapeQuery} remains the final net for query-specific
|
|
1876
|
+
* ineligibility.
|
|
1877
|
+
*/
|
|
1878
|
+
private async compileServeShape(
|
|
1879
|
+
enriched: ServeBinding[],
|
|
1880
|
+
): Promise<ModelMaterializer> {
|
|
1881
|
+
// Richest first; each predicate keeps fewer refinement kinds than the last.
|
|
1882
|
+
const keepKinds: Array<ReadonlySet<string>> = [
|
|
1883
|
+
new Set(["join", "dimension", "measure", "view"]),
|
|
1884
|
+
new Set(["join", "dimension", "measure"]),
|
|
1885
|
+
new Set(["dimension", "measure"]),
|
|
1886
|
+
new Set(),
|
|
1887
|
+
];
|
|
1888
|
+
// Skip escalation entirely when nothing beyond the base is carried.
|
|
1889
|
+
const hasRefinements = enriched.some(
|
|
1890
|
+
(b) => (b.refinements ?? []).length > 0,
|
|
1891
|
+
);
|
|
1892
|
+
const lastTier = keepKinds.length - 1;
|
|
1893
|
+
for (let tier = 0; tier <= lastTier; tier++) {
|
|
1894
|
+
const keep = keepKinds[tier];
|
|
1895
|
+
const shaped =
|
|
1896
|
+
tier === 0
|
|
1897
|
+
? enriched
|
|
1898
|
+
: enriched.map((b) =>
|
|
1899
|
+
b.refinements
|
|
1900
|
+
? {
|
|
1901
|
+
...b,
|
|
1902
|
+
refinements: b.refinements.filter((r) =>
|
|
1903
|
+
keep.has(r.kind),
|
|
1904
|
+
),
|
|
1905
|
+
}
|
|
1906
|
+
: b,
|
|
1907
|
+
);
|
|
1908
|
+
const materializer = this.buildServeShapeMaterializer(shaped);
|
|
1909
|
+
// Base-only (last tier) always compiles; trust it without a probe. And
|
|
1910
|
+
// when there are no refinements at all, tier 0 IS the base — skip too.
|
|
1911
|
+
if (tier === lastTier || (tier === 0 && !hasRefinements)) {
|
|
1912
|
+
return materializer;
|
|
1913
|
+
}
|
|
1914
|
+
try {
|
|
1915
|
+
await materializer.getModel();
|
|
1916
|
+
return materializer;
|
|
1917
|
+
} catch (err) {
|
|
1918
|
+
recordServeShapeTierDrop(tier);
|
|
1919
|
+
logger.warn(
|
|
1920
|
+
"Storage serve shape failed to compile; dropping the riskiest refinement category and retrying",
|
|
1921
|
+
{
|
|
1922
|
+
model: this.modelPath,
|
|
1923
|
+
tier,
|
|
1924
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1925
|
+
},
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
// Unreachable: the last tier returns above. Satisfy the type checker.
|
|
1930
|
+
return this.buildServeShapeMaterializer(
|
|
1931
|
+
enriched.map((b) => ({ ...b, refinements: [] })),
|
|
1932
|
+
);
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
/** Build the transient serve-shape materializer for a set of bindings. */
|
|
1936
|
+
private buildServeShapeMaterializer(
|
|
1937
|
+
bindings: ServeBinding[],
|
|
1938
|
+
): ModelMaterializer {
|
|
1939
|
+
const { modelText } = buildServeShapeModelForBindings(bindings);
|
|
1940
|
+
const root = "file:///storage-serve-shape/";
|
|
1941
|
+
const url = `${root}shape.malloy`;
|
|
1942
|
+
const runtime = new Runtime({
|
|
1943
|
+
urlReader: new InMemoryURLReader(new Map([[url, modelText]])),
|
|
1944
|
+
config: Model.toMalloyConfig(this.serveMalloyConfig!),
|
|
1945
|
+
});
|
|
1946
|
+
return runtime.loadModel(new URL(url), {
|
|
1947
|
+
importBaseURL: new URL(root),
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
/**
|
|
1952
|
+
* The serve bindings enriched with the refinements to re-declare on each
|
|
1953
|
+
* virtual base — the source's dimensions/measures (computed from the stored
|
|
1954
|
+
* columns), its joins whose target source is ALSO materialized (the join runs
|
|
1955
|
+
* over the stored tables), and its views (turtles). All are read from this
|
|
1956
|
+
* model's compiled definition. Analytic source-fields are not carried; a
|
|
1957
|
+
* query using one falls back to live.
|
|
1958
|
+
*
|
|
1959
|
+
* Join and view declaration text is lifted verbatim from the author's source
|
|
1960
|
+
* files by location (read once per file, best-effort — an unreadable file
|
|
1961
|
+
* just drops that source's joins/views, which fall back). The join
|
|
1962
|
+
* materialization gate is a `sourceID`-keyed lookup, so it never depends on
|
|
1963
|
+
* parsing that text; views are emitted optimistically and pruned by the
|
|
1964
|
+
* shape-compile escalation in {@link compileServeShape} if they don't hold.
|
|
1965
|
+
*/
|
|
1966
|
+
private serveBindingsWithRefinements(
|
|
1967
|
+
bindings: ServeBinding[] = this.serveBindings,
|
|
1968
|
+
): ServeBinding[] {
|
|
1969
|
+
const contents = (
|
|
1970
|
+
this.modelDef as
|
|
1971
|
+
| {
|
|
1972
|
+
contents?: Record<
|
|
1973
|
+
string,
|
|
1974
|
+
{ sourceID?: unknown; fields?: unknown[] }
|
|
1975
|
+
>;
|
|
1976
|
+
}
|
|
1977
|
+
| undefined
|
|
1978
|
+
)?.contents;
|
|
1979
|
+
// sourceID -> author source name, for the join materialization gate.
|
|
1980
|
+
const sourceNameById = new Map<string, string>();
|
|
1981
|
+
for (const [name, def] of Object.entries(contents ?? {})) {
|
|
1982
|
+
if (typeof def?.sourceID === "string") {
|
|
1983
|
+
sourceNameById.set(def.sourceID, name);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
const materializedSourceNames = new Set(
|
|
1987
|
+
bindings.map((b) => b.sourceName),
|
|
1988
|
+
);
|
|
1989
|
+
// Cache each source file's text (or null when unreadable) across bindings.
|
|
1990
|
+
const fileCache = new Map<string, string | null>();
|
|
1991
|
+
const liftText = (location: SourceLocation): string | undefined => {
|
|
1992
|
+
if (!location?.url?.startsWith("file:")) return undefined;
|
|
1993
|
+
if (!fileCache.has(location.url)) {
|
|
1994
|
+
try {
|
|
1995
|
+
fileCache.set(
|
|
1996
|
+
location.url,
|
|
1997
|
+
readFileSync(fileURLToPath(location.url), "utf8"),
|
|
1998
|
+
);
|
|
1999
|
+
} catch {
|
|
2000
|
+
fileCache.set(location.url, null);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
const text = fileCache.get(location.url);
|
|
2004
|
+
return text ? sliceSourceRange(text, location.range) : undefined;
|
|
2005
|
+
};
|
|
2006
|
+
return (
|
|
2007
|
+
bindings
|
|
2008
|
+
.map((b) => {
|
|
2009
|
+
const fields = contents?.[b.sourceName]?.fields;
|
|
2010
|
+
// Narrow the declared ::Shape to the source's PUBLIC columns: the
|
|
2011
|
+
// build materializes every projected column (incl. `except:`-ed /
|
|
2012
|
+
// access-restricted ones), so the captured schema can be wider
|
|
2013
|
+
// than the source's public surface. Declaring a hidden column
|
|
2014
|
+
// would expose it over storage when live hides it — always applied
|
|
2015
|
+
// (even with no refinements), so the serve surface never widens
|
|
2016
|
+
// the source's.
|
|
2017
|
+
const schema = narrowSchemaToPublic(b.schema, fields);
|
|
2018
|
+
const refinements = [
|
|
2019
|
+
...extractJoins(fields, {
|
|
2020
|
+
sourceNameById,
|
|
2021
|
+
materializedSourceNames,
|
|
2022
|
+
liftText,
|
|
2023
|
+
}),
|
|
2024
|
+
...extractRefinements(fields),
|
|
2025
|
+
...extractViews(fields, liftText),
|
|
2026
|
+
];
|
|
2027
|
+
return { ...b, schema, refinements };
|
|
2028
|
+
})
|
|
2029
|
+
// Drop any binding whose public schema is empty. Bindings are pushed
|
|
2030
|
+
// to EVERY model in the package, so a model receives bindings for
|
|
2031
|
+
// sources it doesn't define (defined in a sibling model) — those have
|
|
2032
|
+
// no field list here, hence an empty narrowed schema. An empty
|
|
2033
|
+
// `type: X__shape is {}` is a Malloy parse error that would fail the
|
|
2034
|
+
// ENTIRE serve-shape model (breaking the base-only-always-compiles
|
|
2035
|
+
// fallback invariant) and silently drop storage serving for this
|
|
2036
|
+
// model's own sources too. Omitting them sends queries on an
|
|
2037
|
+
// undefined source to live (where this model refuses them anyway) and
|
|
2038
|
+
// keeps a source from being served through a model that doesn't
|
|
2039
|
+
// declare it.
|
|
2040
|
+
.filter((b) => b.schema.length > 0)
|
|
2041
|
+
);
|
|
2042
|
+
}
|
|
2043
|
+
|
|
1727
2044
|
public async getQueryResults(
|
|
1728
2045
|
sourceName?: string,
|
|
1729
2046
|
queryName?: string,
|
|
@@ -1744,6 +2061,10 @@ export class Model {
|
|
|
1744
2061
|
compactResult: QueryData;
|
|
1745
2062
|
modelInfo: Malloy.ModelInfo;
|
|
1746
2063
|
dataStyles: DataStyles;
|
|
2064
|
+
/** Row cap pushed into the SQL: the query's own LIMIT, else the default. */
|
|
2065
|
+
rowLimit: number;
|
|
2066
|
+
/** Which of those two the cap came from. */
|
|
2067
|
+
rowLimitSource: QueryRowLimitSource;
|
|
1747
2068
|
}> {
|
|
1748
2069
|
const startTime = performance.now();
|
|
1749
2070
|
if (this.compilationError) {
|
|
@@ -1761,6 +2082,23 @@ export class Model {
|
|
|
1761
2082
|
}
|
|
1762
2083
|
|
|
1763
2084
|
let runnable: QueryMaterializer;
|
|
2085
|
+
let liveRunnable: QueryMaterializer | undefined;
|
|
2086
|
+
// Set when this query is routed through the `storage=` serve-shape
|
|
2087
|
+
// transform; threaded into prepare + run so the virtual sources resolve to
|
|
2088
|
+
// their physical tables. Undefined ⇒ served live (the default path).
|
|
2089
|
+
let serveVirtualMap: VirtualMap | undefined;
|
|
2090
|
+
// The bindings that actually PRODUCED the serve shape — the fresh subset,
|
|
2091
|
+
// not the package's whole set. Per-binding decisions (freshnessFallback)
|
|
2092
|
+
// must read this: `this.serveBindings` is pushed package-wide, so a
|
|
2093
|
+
// sibling source's value would otherwise decide this query's behavior.
|
|
2094
|
+
let serveShapeBindings: ServeBinding[] = [];
|
|
2095
|
+
// How the answer was ultimately produced, for the query histogram. Absent
|
|
2096
|
+
// when the query never routed (an off/live deployment's histogram is
|
|
2097
|
+
// unchanged); "storage" when served from a materialized table;
|
|
2098
|
+
// "live_fallback" when it routed but a run-time store failure degraded it
|
|
2099
|
+
// to live — which must NOT count as a storage hit, since the hit rate is
|
|
2100
|
+
// the tier's headline KPI and would otherwise rise while the tier is down.
|
|
2101
|
+
let servedFrom: "storage" | "live_fallback" | undefined;
|
|
1764
2102
|
if (!this.modelMaterializer || !this.modelDef || !this.modelInfo)
|
|
1765
2103
|
throw new BadRequestError("Model has no queryable entities.");
|
|
1766
2104
|
|
|
@@ -1852,6 +2190,51 @@ export class Model {
|
|
|
1852
2190
|
// `sourceName`/`queryName` pair are untrusted, so both compile here;
|
|
1853
2191
|
// only author-curated notebook cells use the unrestricted `loadQuery`.
|
|
1854
2192
|
runnable = this.modelMaterializer.loadRestrictedQuery(queryString);
|
|
2193
|
+
// Kept so a routed query can still be answered live if the store fails
|
|
2194
|
+
// underneath it at RUN time (see the freshnessFallback retry below).
|
|
2195
|
+
liveRunnable = runnable;
|
|
2196
|
+
|
|
2197
|
+
// storage= serve routing: when enabled and this package has sources
|
|
2198
|
+
// materialized into a storage destination, try compiling the query
|
|
2199
|
+
// against the transient serve-shape model (materialized sources rebound
|
|
2200
|
+
// to virtual sources on their storage connection). If it compiles, we
|
|
2201
|
+
// serve from the materialized tables via the virtualMap; if it does not
|
|
2202
|
+
// (a refinement the shape lacks, or an unbound source), we keep the
|
|
2203
|
+
// original runnable and serve live — safe fallback, no behavior change
|
|
2204
|
+
// for anything the transform can't yet reproduce. Off / write-only and
|
|
2205
|
+
// packages with no storage bindings skip this entirely.
|
|
2206
|
+
if (
|
|
2207
|
+
getPersistStorageMode() === "on" &&
|
|
2208
|
+
this.serveBindings.length > 0 &&
|
|
2209
|
+
this.serveMalloyConfig
|
|
2210
|
+
) {
|
|
2211
|
+
try {
|
|
2212
|
+
const shaped = await this.loadServeShapeQuery(queryString);
|
|
2213
|
+
runnable = shaped.runnable;
|
|
2214
|
+
serveVirtualMap = shaped.virtualMap;
|
|
2215
|
+
serveShapeBindings = shaped.bindings;
|
|
2216
|
+
servedFrom = "storage";
|
|
2217
|
+
recordStorageServeRouting("storage");
|
|
2218
|
+
logger.info("Serving query from storage tier (virtual-source)", {
|
|
2219
|
+
modelPath: this.modelPath,
|
|
2220
|
+
// The sources in the SHAPE, not the package's whole binding set:
|
|
2221
|
+
// a stale binding is dropped before the shape is built.
|
|
2222
|
+
storageSources: shaped.bindings.map((b) => b.sourceName),
|
|
2223
|
+
});
|
|
2224
|
+
} catch (shapeErr) {
|
|
2225
|
+
recordStorageServeRouting("live_fallback");
|
|
2226
|
+
logger.debug(
|
|
2227
|
+
"storage serve-shape ineligible for this query; serving live",
|
|
2228
|
+
{
|
|
2229
|
+
modelPath: this.modelPath,
|
|
2230
|
+
error:
|
|
2231
|
+
shapeErr instanceof Error
|
|
2232
|
+
? shapeErr.message
|
|
2233
|
+
: String(shapeErr),
|
|
2234
|
+
},
|
|
2235
|
+
);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
1855
2238
|
} catch (error) {
|
|
1856
2239
|
// Re-throw BadRequestError as-is
|
|
1857
2240
|
if (error instanceof BadRequestError) {
|
|
@@ -1926,6 +2309,14 @@ export class Model {
|
|
|
1926
2309
|
// (for the row limit) and the run so a stale persist source falls back per
|
|
1927
2310
|
// its declared policy — and prep/run agree on the same substitution.
|
|
1928
2311
|
const buildManifest = this.resolveFreshBuildManifest();
|
|
2312
|
+
// The serve-shape runnable resolves its tables through `virtualMap`, not
|
|
2313
|
+
// the same-connection build manifest, and its transient model carries no
|
|
2314
|
+
// `##! experimental.persistence` — so passing a non-empty buildManifest to
|
|
2315
|
+
// it errors. When routing through the shape, suppress the manifest; the
|
|
2316
|
+
// original (live) runnable still gets it.
|
|
2317
|
+
const effectiveBuildManifest = serveVirtualMap
|
|
2318
|
+
? undefined
|
|
2319
|
+
: buildManifest;
|
|
1929
2320
|
|
|
1930
2321
|
// Prepare INSIDE the run try/catch: a bad-given / value-type throw at
|
|
1931
2322
|
// prepare time (getPreparedResult binds the givens) gets the same
|
|
@@ -1933,6 +2324,7 @@ export class Model {
|
|
|
1933
2324
|
// a 500. `executionTime` is still captured after prepare and before run,
|
|
1934
2325
|
// preserving the pre-existing timing recorded by the success histogram.
|
|
1935
2326
|
let rowLimit = 0;
|
|
2327
|
+
let rowLimitSource: QueryRowLimitSource = "server_default";
|
|
1936
2328
|
let executionTime = 0;
|
|
1937
2329
|
let queryResults;
|
|
1938
2330
|
// Givens supplied only so a joined source's authorize gate could see
|
|
@@ -1940,79 +2332,180 @@ export class Model {
|
|
|
1940
2332
|
// the real query if this model doesn't itself surface them — see
|
|
1941
2333
|
// filterGivensToModelSurface.
|
|
1942
2334
|
const querySurfaceGivens = this.filterGivensToModelSurface(givens);
|
|
2335
|
+
// Same reason as effectiveBuildManifest: the serve shape is built from
|
|
2336
|
+
// given-FREE sources, so it surfaces no `given:` and Malloy rejects any
|
|
2337
|
+
// supplied name with "unknown given" — a spurious 400, past the routing
|
|
2338
|
+
// fallback, on a query that should just serve from storage. Nothing in the
|
|
2339
|
+
// shape can read them; the authorize gate above already saw the full set.
|
|
2340
|
+
const effectiveGivens = serveVirtualMap ? undefined : querySurfaceGivens;
|
|
1943
2341
|
try {
|
|
1944
|
-
|
|
1945
|
-
(
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
2342
|
+
const preparedLimit = (
|
|
2343
|
+
await runnable.getPreparedResult({
|
|
2344
|
+
givens: effectiveGivens,
|
|
2345
|
+
buildManifest: effectiveBuildManifest,
|
|
2346
|
+
virtualMap: serveVirtualMap,
|
|
2347
|
+
})
|
|
2348
|
+
).resultExplore.limit;
|
|
2349
|
+
rowLimitSource = queryRowLimitSource(preparedLimit);
|
|
2350
|
+
rowLimit = resolveModelQueryRowLimit(preparedLimit, {
|
|
2351
|
+
defaultLimit: getDefaultQueryRowLimit(),
|
|
2352
|
+
maxRows,
|
|
2353
|
+
});
|
|
1953
2354
|
executionTime = performance.now() - startTime;
|
|
1954
2355
|
|
|
1955
2356
|
queryResults = await runnable.run({
|
|
1956
2357
|
rowLimit,
|
|
1957
|
-
givens:
|
|
2358
|
+
givens: effectiveGivens,
|
|
1958
2359
|
abortSignal,
|
|
1959
|
-
buildManifest,
|
|
2360
|
+
buildManifest: effectiveBuildManifest,
|
|
2361
|
+
virtualMap: serveVirtualMap,
|
|
1960
2362
|
});
|
|
1961
2363
|
} catch (error) {
|
|
1962
|
-
//
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
2364
|
+
// A binding that declares `freshnessFallback=live` is saying the tier is
|
|
2365
|
+
// a performance optimisation, not a dependency — so a store that fails
|
|
2366
|
+
// UNDER a routed query should degrade to serving live, the same answer
|
|
2367
|
+
// the compile-time ladder already gives when the shape can't be built.
|
|
2368
|
+
// Without this the store is a hard dependency the moment a query routes:
|
|
2369
|
+
// a not-yet-converged rebind or an over-eager GC turns into a user-facing
|
|
2370
|
+
// error for a source explicitly marked as safe to serve live.
|
|
2371
|
+
//
|
|
2372
|
+
// Deliberately narrow:
|
|
2373
|
+
// - only when the query actually routed to storage (`serveVirtualMap`);
|
|
2374
|
+
// - only when every binding THAT PRODUCED THIS SHAPE says `live` —
|
|
2375
|
+
// `fail` (and the `stale_ok` default) keep surfacing, so one
|
|
2376
|
+
// fail-closed source is not degraded by a permissive neighbour.
|
|
2377
|
+
// Read off the shape's own bindings, not the package's: bindings are
|
|
2378
|
+
// pushed package-wide and `freshnessFallback` is per entry, so a
|
|
2379
|
+
// mixed set is normal and a sibling must not decide this query;
|
|
2380
|
+
// - never for a client error (a bad given) or an abort, where a retry
|
|
2381
|
+
// would just reproduce it or defy the caller;
|
|
2382
|
+
// - the retry re-supplies the REAL givens. The storage path suppresses
|
|
2383
|
+
// them because the shape is built from given-free sources; the live
|
|
2384
|
+
// source may filter on them, and running it without them would serve
|
|
2385
|
+
// unfiltered rows.
|
|
2386
|
+
const canDegradeToLive =
|
|
2387
|
+
!!serveVirtualMap &&
|
|
2388
|
+
!!liveRunnable &&
|
|
2389
|
+
!abortSignal?.aborted &&
|
|
2390
|
+
!String((error as { code?: string })?.code ?? "").startsWith(
|
|
2391
|
+
"runtime-given-",
|
|
2392
|
+
) &&
|
|
2393
|
+
serveShapeBindings.length > 0 &&
|
|
2394
|
+
serveShapeBindings.every((b) => b.freshnessFallback === "live");
|
|
2395
|
+
// Both the original failure and a failure OF THE RETRY end here: record
|
|
2396
|
+
// the error metric, then map the error. A broad outage takes the source
|
|
2397
|
+
// warehouse down alongside the store, so the retry failing is ordinary —
|
|
2398
|
+
// and without this it would escape the given-mapping, the MalloyError
|
|
2399
|
+
// rethrow and the error metric, turning a clean 400 into an untracked 500.
|
|
2400
|
+
// Annotated on the CONST, not just the arrow: control-flow analysis only
|
|
2401
|
+
// treats a call as never-returning when the callee has an explicit type
|
|
2402
|
+
// annotation, and the fall-through below depends on that narrowing.
|
|
2403
|
+
const failQuery: (err: unknown) => never = (err) => {
|
|
2404
|
+
// Record error metrics
|
|
2405
|
+
const errorEndTime = performance.now();
|
|
2406
|
+
const errorExecutionTime = errorEndTime - startTime;
|
|
2407
|
+
this.queryExecutionHistogram.record(errorExecutionTime, {
|
|
2408
|
+
"malloy.model.path": this.modelPath,
|
|
2409
|
+
"malloy.model.query.name": queryName,
|
|
2410
|
+
"malloy.model.query.source": sourceName,
|
|
2411
|
+
"malloy.model.query.query": query,
|
|
2412
|
+
"malloy.model.query.status": "error",
|
|
2413
|
+
// Ships dark: only tag queries that routed. A live/off query gets
|
|
2414
|
+
// no new attribute, so an off deployment's histogram is unchanged.
|
|
2415
|
+
...(servedFrom
|
|
2416
|
+
? { "malloy.model.query.served_from": servedFrom }
|
|
2417
|
+
: {}),
|
|
2418
|
+
});
|
|
1972
2419
|
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
2420
|
+
// Bad client-supplied givens (unknown name, wrong-typed value, an
|
|
2421
|
+
// operator-finalized override, ...) all surface as a Malloy
|
|
2422
|
+
// `runtime-given-*` error. Malloy is the single validator; the publisher
|
|
2423
|
+
// just maps its rejection to a clean 400. Duck-type on `.code`
|
|
2424
|
+
// (MalloyCompileError extends Error, not MalloyError, and isn't
|
|
2425
|
+
// root-exported). The `runtime-given-` prefix is a pinned coupling to
|
|
2426
|
+
// Malloy's error codes (@malloydata/malloy given_binding.ts / runtime.ts);
|
|
2427
|
+
// if they're renamed upstream, update it here (and in environment.ts) —
|
|
2428
|
+
// otherwise these fall through to the generic 400 below with a worse
|
|
2429
|
+
// message, and the /compile path silently omits `sql`.
|
|
2430
|
+
const givenCode = (err as { code?: string })?.code;
|
|
2431
|
+
if (
|
|
2432
|
+
typeof givenCode === "string" &&
|
|
2433
|
+
givenCode.startsWith("runtime-given-")
|
|
2434
|
+
) {
|
|
2435
|
+
logger.debug("Rejected client-supplied given", {
|
|
2436
|
+
environmentName: this.packageName,
|
|
2437
|
+
modelPath: this.modelPath,
|
|
2438
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2439
|
+
});
|
|
2440
|
+
throw new BadRequestError(
|
|
2441
|
+
err instanceof Error ? err.message : String(err),
|
|
2442
|
+
);
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
// Re-throw Malloy errors as-is (they will be handled by error handler)
|
|
2446
|
+
if (err instanceof MalloyError) {
|
|
2447
|
+
throw err;
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
// For other runtime errors (like divide by zero), throw as BadRequestError
|
|
2451
|
+
const errorMessage =
|
|
2452
|
+
err instanceof Error ? err.message : String(err);
|
|
2453
|
+
logger.error("Query execution error", {
|
|
2454
|
+
error: err,
|
|
2455
|
+
errorMessage,
|
|
1989
2456
|
environmentName: this.packageName,
|
|
1990
2457
|
modelPath: this.modelPath,
|
|
1991
|
-
|
|
2458
|
+
query,
|
|
2459
|
+
queryName,
|
|
2460
|
+
sourceName,
|
|
1992
2461
|
});
|
|
1993
2462
|
throw new BadRequestError(
|
|
1994
|
-
|
|
2463
|
+
`Query execution failed: ${errorMessage}`,
|
|
1995
2464
|
);
|
|
2465
|
+
};
|
|
2466
|
+
if (!canDegradeToLive) failQuery(error);
|
|
2467
|
+
logger.warn(
|
|
2468
|
+
"Storage-served query failed at run time; falling back to live (freshnessFallback=live)",
|
|
2469
|
+
{
|
|
2470
|
+
modelPath: this.modelPath,
|
|
2471
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2472
|
+
},
|
|
2473
|
+
);
|
|
2474
|
+
recordStorageServeRouting("runtime_live_fallback");
|
|
2475
|
+
try {
|
|
2476
|
+
// Re-derive the row cap from the LIVE shape. `rowLimit` is assigned
|
|
2477
|
+
// inside the try above, from the storage prepare — so a store failure
|
|
2478
|
+
// that surfaces AT PREPARE (a manifest naming a table that is missing
|
|
2479
|
+
// or malformed resolves through `virtualMap` there) leaves it 0, which
|
|
2480
|
+
// the connector reads as a hard cap and stops before the first row: a
|
|
2481
|
+
// successful, EMPTY answer. Asking the live shape is also the honest
|
|
2482
|
+
// limit, since the live shape is what runs.
|
|
2483
|
+
const livePreparedLimit = (
|
|
2484
|
+
await liveRunnable!.getPreparedResult({
|
|
2485
|
+
givens: querySurfaceGivens,
|
|
2486
|
+
buildManifest,
|
|
2487
|
+
})
|
|
2488
|
+
).resultExplore.limit;
|
|
2489
|
+
rowLimitSource = queryRowLimitSource(livePreparedLimit);
|
|
2490
|
+
rowLimit = resolveModelQueryRowLimit(livePreparedLimit, {
|
|
2491
|
+
defaultLimit: getDefaultQueryRowLimit(),
|
|
2492
|
+
maxRows,
|
|
2493
|
+
});
|
|
2494
|
+
queryResults = await liveRunnable!.run({
|
|
2495
|
+
rowLimit,
|
|
2496
|
+
givens: querySurfaceGivens,
|
|
2497
|
+
abortSignal,
|
|
2498
|
+
buildManifest,
|
|
2499
|
+
});
|
|
2500
|
+
} catch (retryError) {
|
|
2501
|
+
failQuery(retryError);
|
|
1996
2502
|
}
|
|
1997
|
-
|
|
1998
|
-
//
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
// For other runtime errors (like divide by zero), throw as BadRequestError
|
|
2004
|
-
const errorMessage =
|
|
2005
|
-
error instanceof Error ? error.message : String(error);
|
|
2006
|
-
logger.error("Query execution error", {
|
|
2007
|
-
error,
|
|
2008
|
-
errorMessage,
|
|
2009
|
-
environmentName: this.packageName,
|
|
2010
|
-
modelPath: this.modelPath,
|
|
2011
|
-
query,
|
|
2012
|
-
queryName,
|
|
2013
|
-
sourceName,
|
|
2014
|
-
});
|
|
2015
|
-
throw new BadRequestError(`Query execution failed: ${errorMessage}`);
|
|
2503
|
+
// The answer came from the live warehouse, so it is NOT a storage hit —
|
|
2504
|
+
// `runtime_live_fallback` above is the signal that the tier is degraded.
|
|
2505
|
+
servedFrom = "live_fallback";
|
|
2506
|
+
executionTime = performance.now() - startTime;
|
|
2507
|
+
// Fall through: `queryResults` is set, so the normal post-run path
|
|
2508
|
+
// wraps and returns it exactly as a live query would.
|
|
2016
2509
|
}
|
|
2017
2510
|
|
|
2018
2511
|
const wrappedResult = API.util.wrapResult(queryResults);
|
|
@@ -2042,12 +2535,29 @@ export class Model {
|
|
|
2042
2535
|
"malloy.model.query.rows_total": queryResults.totalRows,
|
|
2043
2536
|
"malloy.model.query.connection": queryResults.connectionName,
|
|
2044
2537
|
"malloy.model.query.status": "success",
|
|
2538
|
+
// Ships dark: only tag queries that routed (see the error path).
|
|
2539
|
+
// "live_fallback" here is a SUCCESS answered by the live warehouse, so
|
|
2540
|
+
// it must not inflate the storage hit rate.
|
|
2541
|
+
...(servedFrom
|
|
2542
|
+
? { "malloy.model.query.served_from": servedFrom }
|
|
2543
|
+
: {}),
|
|
2045
2544
|
});
|
|
2046
2545
|
return {
|
|
2047
2546
|
result: wrappedResult,
|
|
2048
2547
|
compactResult: queryResults.data.value,
|
|
2049
2548
|
modelInfo: this.modelInfo,
|
|
2050
2549
|
dataStyles: this.dataStyles,
|
|
2550
|
+
// The cap actually pushed into the SQL. A caller cannot otherwise tell
|
|
2551
|
+
// a complete result from one the row limit cut off: with no LIMIT of
|
|
2552
|
+
// its own a query silently gets DEFAULT_QUERY_ROW_LIMIT rows, and that
|
|
2553
|
+
// is under maxRows, so assertWithinModelResponseLimits raises nothing.
|
|
2554
|
+
// Returning the number lets a caller compare it against the row count
|
|
2555
|
+
// and say so, with no extra query.
|
|
2556
|
+
rowLimit,
|
|
2557
|
+
// Whether that cap was the author's own limit:/top: or the silent
|
|
2558
|
+
// default. Only the second means rows were probably left behind; a
|
|
2559
|
+
// deliberate `top: 10` returning 10 rows is a complete answer.
|
|
2560
|
+
rowLimitSource,
|
|
2051
2561
|
};
|
|
2052
2562
|
}
|
|
2053
2563
|
|