@malloy-publisher/server 0.0.382-dev → 0.2.0
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 +145 -20
- package/README.md +65 -12
- package/dist/app/api-doc.html +5 -0
- package/dist/app/api-doc.yaml +3903 -583
- package/dist/app/assets/EnvironmentPage-DiUgLuCH.js +1 -0
- package/dist/app/assets/HomePage-MhhjhnFj.js +1 -0
- package/dist/app/assets/LightMode-BEyvNUzT.js +1 -0
- package/dist/app/assets/MainPage-D4Cfee9o.js +2 -0
- package/dist/app/assets/MaterializationsPage-nGgGqwSN.js +1 -0
- package/dist/app/assets/ModelPage-DLAcEw-F.js +1 -0
- package/dist/app/assets/PackagePage-Df0ikH7w.js +1 -0
- package/dist/app/assets/RouteError-BFQwtroH.js +1 -0
- package/dist/app/assets/ThemeEditorPage-OoskoQYN.js +1 -0
- package/dist/app/assets/WorkbookPage-CpFgvUDC.js +1 -0
- package/dist/app/assets/{core-CjeTkq8O.es-BqRc6yhC.js → core-lb28vQkR.es-BoVSOc75.js} +1 -1
- package/dist/app/assets/github-dark-DenFmJkN.es-DHJKELXO.js +1 -0
- package/dist/app/assets/index-5eLCcNmP.css +1 -0
- package/dist/app/assets/index-6ypuFH6g.js +1774 -0
- package/dist/app/assets/index-B8pnU_V_.js +615 -0
- package/dist/app/assets/index-BImcHmVP.js +1 -0
- package/dist/app/assets/index-DSoCUF-f.js +2725 -0
- package/dist/app/assets/index-DfqzSItA.js +2 -0
- package/dist/app/assets/index-w_0OQJgZ.js +23 -0
- package/dist/app/index.html +8 -4
- package/dist/cpufeatures-1yrn0vtw.node +0 -0
- package/dist/default-publisher.config.json +23 -0
- package/dist/{instrumentation.js → instrumentation.mjs} +10631 -10573
- package/dist/package_load_worker.mjs +15219 -0
- package/dist/runtime/publisher.js +331 -0
- package/dist/{server.js → server.mjs} +115548 -49590
- package/dist/sshcrypto-vd2k5hq9.node +0 -0
- package/package.json +41 -28
- package/publisher.config.example.bigquery.json +33 -0
- package/publisher.config.example.duckdb.json +23 -0
- package/.eslintrc.json +0 -44
- package/.prettierignore +0 -1
- package/.prettierrc +0 -4
- package/build.ts +0 -38
- package/dist/app/assets/HomePage-Dn3E4CuB.js +0 -1
- package/dist/app/assets/MainPage-BzB3yoqi.js +0 -2
- package/dist/app/assets/ModelPage-C9O_sAXT.js +0 -1
- package/dist/app/assets/PackagePage-DcxKEjBX.js +0 -1
- package/dist/app/assets/ProjectPage-BDj307rF.js +0 -1
- package/dist/app/assets/RouteError-DAShbVCG.js +0 -1
- package/dist/app/assets/WorkbookPage-Cs_XYEaB.js +0 -1
- package/dist/app/assets/index-15BOvhp0.js +0 -456
- package/dist/app/assets/index-Bb2jqquW.js +0 -676
- package/dist/app/assets/index-CMlGQMcl.css +0 -1
- package/dist/app/assets/index-D68X76-7.js +0 -1276
- package/dist/app/assets/index.umd-DGBekgSu.js +0 -1145
- package/dxt/malloy_bridge.py +0 -354
- package/dxt/manifest.json +0 -22
- package/eslint.config.mjs +0 -76
- package/k6-tests/README.md +0 -144
- package/k6-tests/bun.lock +0 -797
- package/k6-tests/load-test/load-test-crud-connections.ts +0 -263
- package/k6-tests/load-test/load-test-crud-packages.ts +0 -461
- package/k6-tests/load-test/load-test-crud-projects.ts +0 -160
- package/k6-tests/load-test/load-test-crud.ts +0 -142
- package/k6-tests/load-test/load-test.ts +0 -784
- package/k6-tests/package.json +0 -27
- package/k6-tests/scripts/run-with-otel.sh +0 -50
- package/k6-tests/smoke-test/smoke-test.ts +0 -158
- package/k6-tests/tsconfig.json +0 -29
- package/k6-tests/utils/client_factory.ts +0 -128
- package/k6-tests/utils/common.ts +0 -489
- package/k6-tests/utils/logger.ts +0 -62
- package/k6-tests/utils/types.d.ts +0 -35
- package/malloy_mcp.dxt +0 -0
- package/publisher.config.json +0 -33
- package/src/config.spec.ts +0 -850
- package/src/config.ts +0 -305
- package/src/constants.ts +0 -10
- package/src/controller/compile.controller.ts +0 -35
- package/src/controller/connection.controller.ts +0 -515
- package/src/controller/database.controller.ts +0 -21
- package/src/controller/manifest.controller.ts +0 -29
- package/src/controller/materialization.controller.ts +0 -125
- package/src/controller/model.controller.ts +0 -107
- package/src/controller/package.controller.ts +0 -182
- package/src/controller/query.controller.ts +0 -60
- package/src/controller/watch-mode.controller.ts +0 -98
- package/src/data_styles.ts +0 -71
- package/src/dto/connection.dto.spec.ts +0 -131
- package/src/dto/connection.dto.ts +0 -174
- package/src/dto/index.ts +0 -2
- package/src/dto/package.dto.spec.ts +0 -42
- package/src/dto/package.dto.ts +0 -12
- package/src/dto/validate.spec.ts +0 -76
- package/src/dto/validate.ts +0 -31
- package/src/errors.ts +0 -116
- package/src/health.ts +0 -189
- package/src/instrumentation.ts +0 -148
- package/src/logger.ts +0 -154
- package/src/mcp/error_messages.ts +0 -199
- package/src/mcp/handler_utils.ts +0 -239
- package/src/mcp/mcp_constants.ts +0 -22
- package/src/mcp/prompts/handlers.ts +0 -84
- package/src/mcp/prompts/index.ts +0 -11
- package/src/mcp/prompts/prompt_definitions.ts +0 -160
- package/src/mcp/prompts/prompt_service.ts +0 -67
- package/src/mcp/prompts/utils.ts +0 -62
- package/src/mcp/resource_metadata.ts +0 -47
- package/src/mcp/resources/model_resource.ts +0 -153
- package/src/mcp/resources/notebook_resource.ts +0 -137
- package/src/mcp/resources/package_resource.ts +0 -370
- package/src/mcp/resources/project_resource.ts +0 -184
- package/src/mcp/resources/query_resource.ts +0 -117
- package/src/mcp/resources/source_resource.ts +0 -141
- package/src/mcp/resources/view_resource.ts +0 -136
- package/src/mcp/server.ts +0 -62
- package/src/mcp/tools/discovery_tools.ts +0 -258
- package/src/mcp/tools/execute_query_tool.ts +0 -265
- package/src/server.ts +0 -1195
- package/src/service/connection.spec.ts +0 -1331
- package/src/service/connection.ts +0 -1511
- package/src/service/connection_service.spec.ts +0 -641
- package/src/service/connection_service.ts +0 -181
- package/src/service/db_utils.spec.ts +0 -714
- package/src/service/db_utils.ts +0 -1227
- package/src/service/filter.spec.ts +0 -447
- package/src/service/filter.ts +0 -337
- package/src/service/filter_integration.spec.ts +0 -825
- package/src/service/gcs_s3_utils.ts +0 -427
- package/src/service/manifest_service.spec.ts +0 -201
- package/src/service/manifest_service.ts +0 -106
- package/src/service/materialization_service.spec.ts +0 -648
- package/src/service/materialization_service.ts +0 -929
- package/src/service/materialized_table_gc.spec.ts +0 -383
- package/src/service/materialized_table_gc.ts +0 -279
- package/src/service/model.spec.ts +0 -257
- package/src/service/model.ts +0 -1069
- package/src/service/package.spec.ts +0 -236
- package/src/service/package.ts +0 -463
- package/src/service/project.ts +0 -681
- package/src/service/project_compile.spec.ts +0 -197
- package/src/service/project_store.spec.ts +0 -859
- package/src/service/project_store.ts +0 -1531
- package/src/service/quoting.ts +0 -41
- package/src/service/resolve_project.ts +0 -13
- package/src/storage/BaseRepository.ts +0 -31
- package/src/storage/DatabaseInterface.ts +0 -180
- package/src/storage/StorageManager.mock.ts +0 -59
- package/src/storage/StorageManager.ts +0 -210
- package/src/storage/duckdb/ConnectionRepository.ts +0 -141
- package/src/storage/duckdb/DuckDBConnection.ts +0 -177
- package/src/storage/duckdb/DuckDBManifestStore.ts +0 -70
- package/src/storage/duckdb/DuckDBRepository.ts +0 -224
- package/src/storage/duckdb/ManifestRepository.ts +0 -119
- package/src/storage/duckdb/MaterializationRepository.ts +0 -249
- package/src/storage/duckdb/PackageRepository.ts +0 -135
- package/src/storage/duckdb/ProjectRepository.ts +0 -148
- package/src/storage/duckdb/manifest_store.spec.ts +0 -133
- package/src/storage/duckdb/schema.ts +0 -147
- package/src/storage/ducklake/DuckLakeManifestStore.ts +0 -146
- package/src/utils.ts +0 -13
- package/tests/fixtures/persist-test/data/orders.csv +0 -5
- package/tests/fixtures/persist-test/persist_test.malloy +0 -11
- package/tests/fixtures/persist-test/publisher.json +0 -5
- package/tests/fixtures/publisher.config.json +0 -15
- package/tests/harness/e2e.ts +0 -96
- package/tests/harness/mcp_test_setup.ts +0 -344
- package/tests/harness/mocks.ts +0 -37
- package/tests/harness/rest_e2e.ts +0 -68
- package/tests/harness/uris.ts +0 -31
- package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +0 -470
- package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +0 -434
- package/tests/integration/mcp/mcp_resource.integration.spec.ts +0 -663
- package/tests/integration/mcp/mcp_transport.integration.spec.ts +0 -122
- package/tests/integration/mcp/setup.spec.ts +0 -5
- package/tests/unit/duckdb/attached_databases.test.ts +0 -1132
- package/tests/unit/ducklake/ducklake.test.ts +0 -950
- package/tests/unit/mcp/prompt_definitions.test.ts +0 -102
- package/tests/unit/mcp/prompt_happy.test.ts +0 -51
- package/tsconfig.json +0 -32
package/src/service/model.ts
DELETED
|
@@ -1,1069 +0,0 @@
|
|
|
1
|
-
import { DuckDBConnection } from "@malloydata/db-duckdb";
|
|
2
|
-
import {
|
|
3
|
-
Annotation,
|
|
4
|
-
API,
|
|
5
|
-
Connection,
|
|
6
|
-
FixedConnectionMap,
|
|
7
|
-
isSourceDef,
|
|
8
|
-
MalloyError,
|
|
9
|
-
ModelDef,
|
|
10
|
-
modelDefToModelInfo,
|
|
11
|
-
ModelMaterializer,
|
|
12
|
-
NamedModelObject,
|
|
13
|
-
NamedQueryDef,
|
|
14
|
-
QueryData,
|
|
15
|
-
QueryMaterializer,
|
|
16
|
-
Runtime,
|
|
17
|
-
StructDef,
|
|
18
|
-
TurtleDef,
|
|
19
|
-
} from "@malloydata/malloy";
|
|
20
|
-
import * as Malloy from "@malloydata/malloy-interfaces";
|
|
21
|
-
import {
|
|
22
|
-
MalloySQLParser,
|
|
23
|
-
MalloySQLStatementType,
|
|
24
|
-
} from "@malloydata/malloy-sql";
|
|
25
|
-
import malloyPackage from "@malloydata/malloy/package.json";
|
|
26
|
-
import { DataStyles } from "@malloydata/render";
|
|
27
|
-
import { metrics } from "@opentelemetry/api";
|
|
28
|
-
import * as fs from "fs/promises";
|
|
29
|
-
import * as path from "path";
|
|
30
|
-
import { fileURLToPath } from "url";
|
|
31
|
-
import { components } from "../api";
|
|
32
|
-
import {
|
|
33
|
-
MODEL_FILE_SUFFIX,
|
|
34
|
-
NOTEBOOK_FILE_SUFFIX,
|
|
35
|
-
ROW_LIMIT,
|
|
36
|
-
} from "../constants";
|
|
37
|
-
import { HackyDataStylesAccumulator } from "../data_styles";
|
|
38
|
-
import {
|
|
39
|
-
BadRequestError,
|
|
40
|
-
ModelCompilationError,
|
|
41
|
-
ModelNotFoundError,
|
|
42
|
-
} from "../errors";
|
|
43
|
-
import { logger } from "../logger";
|
|
44
|
-
import { BuildManifest } from "../storage/DatabaseInterface";
|
|
45
|
-
import { URL_READER } from "../utils";
|
|
46
|
-
import {
|
|
47
|
-
buildFilterClause,
|
|
48
|
-
injectFilterRefinement,
|
|
49
|
-
parseFilters,
|
|
50
|
-
FilterValidationError,
|
|
51
|
-
type FilterDefinition,
|
|
52
|
-
type FilterParams,
|
|
53
|
-
} from "./filter";
|
|
54
|
-
|
|
55
|
-
type ApiCompiledModel = components["schemas"]["CompiledModel"];
|
|
56
|
-
type ApiNotebookCell = components["schemas"]["NotebookCell"];
|
|
57
|
-
type ApiRawNotebook = components["schemas"]["RawNotebook"];
|
|
58
|
-
type ApiSource = components["schemas"]["Source"];
|
|
59
|
-
type ApiFilter = components["schemas"]["Filter"];
|
|
60
|
-
type ApiView = components["schemas"]["View"];
|
|
61
|
-
type ApiQuery = components["schemas"]["Query"];
|
|
62
|
-
export type ApiConnection = components["schemas"]["Connection"];
|
|
63
|
-
export type SnowflakeConnection = components["schemas"]["SnowflakeConnection"];
|
|
64
|
-
export type PostgresConnection = components["schemas"]["PostgresConnection"];
|
|
65
|
-
export type BigqueryConnection = components["schemas"]["BigqueryConnection"];
|
|
66
|
-
export type TrinoConnection = components["schemas"]["TrinoConnection"];
|
|
67
|
-
|
|
68
|
-
const MALLOY_VERSION = malloyPackage.version;
|
|
69
|
-
|
|
70
|
-
export type ModelType = "model" | "notebook";
|
|
71
|
-
|
|
72
|
-
interface RunnableNotebookCell {
|
|
73
|
-
type: "code" | "markdown";
|
|
74
|
-
text: string;
|
|
75
|
-
runnable?: QueryMaterializer;
|
|
76
|
-
/** Retained so we can rebuild the query with filter refinements at execution time. */
|
|
77
|
-
modelMaterializer?: ModelMaterializer;
|
|
78
|
-
newSources?: Malloy.SourceInfo[];
|
|
79
|
-
queryInfo?: Malloy.QueryInfo;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export class Model {
|
|
83
|
-
private packageName: string;
|
|
84
|
-
private modelPath: string;
|
|
85
|
-
private dataStyles: DataStyles;
|
|
86
|
-
private modelType: ModelType;
|
|
87
|
-
private modelMaterializer: ModelMaterializer | undefined;
|
|
88
|
-
private modelDef: ModelDef | undefined;
|
|
89
|
-
private modelInfo: Malloy.ModelInfo | undefined;
|
|
90
|
-
private sources: ApiSource[] | undefined;
|
|
91
|
-
private queries: ApiQuery[] | undefined;
|
|
92
|
-
private sourceInfos: Malloy.SourceInfo[] | undefined;
|
|
93
|
-
private runnableNotebookCells: RunnableNotebookCell[] | undefined;
|
|
94
|
-
private compilationError: MalloyError | Error | undefined;
|
|
95
|
-
/** Parsed #(filter) definitions keyed by source name. */
|
|
96
|
-
private filterMap: Map<string, FilterDefinition[]>;
|
|
97
|
-
private meter = metrics.getMeter("publisher");
|
|
98
|
-
private queryExecutionHistogram = this.meter.createHistogram(
|
|
99
|
-
"malloy_model_query_duration",
|
|
100
|
-
{
|
|
101
|
-
description: "How long it takes to execute a Malloy model query",
|
|
102
|
-
unit: "ms",
|
|
103
|
-
},
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
constructor(
|
|
107
|
-
packageName: string,
|
|
108
|
-
modelPath: string,
|
|
109
|
-
dataStyles: DataStyles,
|
|
110
|
-
modelType: ModelType,
|
|
111
|
-
modelMaterializer: ModelMaterializer | undefined,
|
|
112
|
-
modelDef: ModelDef | undefined,
|
|
113
|
-
// TODO(jjs) - remove these
|
|
114
|
-
sources: ApiSource[] | undefined,
|
|
115
|
-
queries: ApiQuery[] | undefined,
|
|
116
|
-
sourceInfos: Malloy.SourceInfo[] | undefined,
|
|
117
|
-
runnableNotebookCells: RunnableNotebookCell[] | undefined,
|
|
118
|
-
compilationError: MalloyError | Error | undefined,
|
|
119
|
-
filterMap?: Map<string, FilterDefinition[]>,
|
|
120
|
-
) {
|
|
121
|
-
this.packageName = packageName;
|
|
122
|
-
this.modelPath = modelPath;
|
|
123
|
-
this.dataStyles = dataStyles;
|
|
124
|
-
this.modelType = modelType;
|
|
125
|
-
this.modelDef = modelDef;
|
|
126
|
-
this.modelMaterializer = modelMaterializer;
|
|
127
|
-
this.sources = sources;
|
|
128
|
-
this.queries = queries;
|
|
129
|
-
this.sourceInfos = sourceInfos;
|
|
130
|
-
this.runnableNotebookCells = runnableNotebookCells;
|
|
131
|
-
this.compilationError = compilationError;
|
|
132
|
-
this.filterMap = filterMap ?? new Map();
|
|
133
|
-
this.modelInfo = this.modelDef
|
|
134
|
-
? modelDefToModelInfo(this.modelDef)
|
|
135
|
-
: undefined;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Get the parsed filter definitions for a given source name.
|
|
140
|
-
* Returns an empty array if no filters are declared.
|
|
141
|
-
*/
|
|
142
|
-
public getFilters(sourceName: string): FilterDefinition[] {
|
|
143
|
-
return this.filterMap.get(sourceName) ?? [];
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Best-effort extraction of a source name from an ad-hoc Malloy query string.
|
|
148
|
-
* Matches patterns like `run: source_name -> ...` or `source_name -> ...`.
|
|
149
|
-
*/
|
|
150
|
-
private extractSourceName(query?: string): string | undefined {
|
|
151
|
-
if (!query) return undefined;
|
|
152
|
-
const runMatch = query.match(/run\s*:\s*(\w+)\s*->/);
|
|
153
|
-
const arrowMatch = query.match(/^\s*(\w+)\s*->/m);
|
|
154
|
-
return runMatch?.[1] ?? arrowMatch?.[1];
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
public static async create(
|
|
158
|
-
packageName: string,
|
|
159
|
-
packagePath: string,
|
|
160
|
-
modelPath: string,
|
|
161
|
-
connections: Map<string, Connection>,
|
|
162
|
-
options?: { buildManifest?: BuildManifest["entries"] },
|
|
163
|
-
): Promise<Model> {
|
|
164
|
-
// getModelRuntime might throw a ModelNotFoundError. It's the callers responsibility
|
|
165
|
-
// to pass a valid model path or handle the error.
|
|
166
|
-
const { runtime, modelURL, importBaseURL, dataStyles, modelType } =
|
|
167
|
-
await Model.getModelRuntime(
|
|
168
|
-
packagePath,
|
|
169
|
-
modelPath,
|
|
170
|
-
connections,
|
|
171
|
-
options,
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
try {
|
|
175
|
-
const { modelMaterializer, runnableNotebookCells } =
|
|
176
|
-
await Model.getModelMaterializer(
|
|
177
|
-
runtime,
|
|
178
|
-
importBaseURL,
|
|
179
|
-
modelURL,
|
|
180
|
-
modelPath,
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
let modelDef = undefined;
|
|
184
|
-
let sources = undefined;
|
|
185
|
-
let queries = undefined;
|
|
186
|
-
let filterMap: Map<string, FilterDefinition[]> | undefined;
|
|
187
|
-
const sourceInfos: Malloy.SourceInfo[] = [];
|
|
188
|
-
if (modelMaterializer) {
|
|
189
|
-
modelDef = (await modelMaterializer.getModel())._modelDef;
|
|
190
|
-
const sourceResult = Model.getSources(modelPath, modelDef);
|
|
191
|
-
sources = sourceResult.sources;
|
|
192
|
-
filterMap = sourceResult.filterMap;
|
|
193
|
-
queries = Model.getQueries(modelPath, modelDef);
|
|
194
|
-
|
|
195
|
-
// Collect sourceInfos from imported models first
|
|
196
|
-
// This follows the same pattern as notebook imports handling
|
|
197
|
-
const imports = modelDef.imports || [];
|
|
198
|
-
const importedSourceNames = new Set<string>();
|
|
199
|
-
for (const importLocation of imports) {
|
|
200
|
-
try {
|
|
201
|
-
const modelString = await runtime.urlReader.readURL(
|
|
202
|
-
new URL(importLocation.importURL),
|
|
203
|
-
);
|
|
204
|
-
const importedModelDef = (
|
|
205
|
-
await runtime
|
|
206
|
-
.loadModel(modelString as string, { importBaseURL })
|
|
207
|
-
.getModel()
|
|
208
|
-
)._modelDef;
|
|
209
|
-
const importedModelInfo =
|
|
210
|
-
modelDefToModelInfo(importedModelDef);
|
|
211
|
-
const importedSources = importedModelInfo.entries.filter(
|
|
212
|
-
(entry) => entry.kind === "source",
|
|
213
|
-
) as Malloy.SourceInfo[];
|
|
214
|
-
for (const source of importedSources) {
|
|
215
|
-
if (!importedSourceNames.has(source.name)) {
|
|
216
|
-
sourceInfos.push(source);
|
|
217
|
-
importedSourceNames.add(source.name);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
} catch (importError) {
|
|
221
|
-
// Log but don't fail if we can't load an import's sourceInfo
|
|
222
|
-
logger.warn("Failed to load sourceInfo from import", {
|
|
223
|
-
importURL: importLocation.importURL,
|
|
224
|
-
error: importError,
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Add locally-defined sources (not already added from imports)
|
|
230
|
-
const localModelInfo = modelDefToModelInfo(modelDef);
|
|
231
|
-
const localSources = localModelInfo.entries.filter(
|
|
232
|
-
(entry) => entry.kind === "source",
|
|
233
|
-
) as Malloy.SourceInfo[];
|
|
234
|
-
for (const source of localSources) {
|
|
235
|
-
if (!importedSourceNames.has(source.name)) {
|
|
236
|
-
sourceInfos.push(source);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
return new Model(
|
|
242
|
-
packageName,
|
|
243
|
-
modelPath,
|
|
244
|
-
dataStyles,
|
|
245
|
-
modelType,
|
|
246
|
-
modelMaterializer,
|
|
247
|
-
modelDef,
|
|
248
|
-
sources,
|
|
249
|
-
queries,
|
|
250
|
-
sourceInfos.length > 0 ? sourceInfos : undefined,
|
|
251
|
-
runnableNotebookCells,
|
|
252
|
-
undefined,
|
|
253
|
-
filterMap,
|
|
254
|
-
);
|
|
255
|
-
} catch (error) {
|
|
256
|
-
let computedError = error;
|
|
257
|
-
if (error instanceof Error && error.stack) {
|
|
258
|
-
logger.error("Error stack", error.stack);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (error instanceof MalloyError) {
|
|
262
|
-
const problems = error.problems;
|
|
263
|
-
for (const problem of problems) {
|
|
264
|
-
logger.error("Problem", problem);
|
|
265
|
-
}
|
|
266
|
-
computedError = new ModelCompilationError(error);
|
|
267
|
-
}
|
|
268
|
-
return new Model(
|
|
269
|
-
packageName,
|
|
270
|
-
modelPath,
|
|
271
|
-
dataStyles,
|
|
272
|
-
modelType,
|
|
273
|
-
undefined,
|
|
274
|
-
undefined,
|
|
275
|
-
undefined,
|
|
276
|
-
undefined,
|
|
277
|
-
undefined,
|
|
278
|
-
undefined,
|
|
279
|
-
computedError as Error,
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
public getPath(): string {
|
|
285
|
-
return this.modelPath;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
public getType(): ModelType {
|
|
289
|
-
return this.modelType;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
public getSources(): ApiSource[] | undefined {
|
|
293
|
-
return this.sources;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
public getSourceInfos(): Malloy.SourceInfo[] | undefined {
|
|
297
|
-
return this.sourceInfos;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
public getQueries(): ApiQuery[] | undefined {
|
|
301
|
-
return this.queries;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
public async getModel(): Promise<ApiCompiledModel> {
|
|
305
|
-
if (this.compilationError) {
|
|
306
|
-
throw this.compilationError;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (this.modelType === "model") {
|
|
310
|
-
return this.getStandardModel();
|
|
311
|
-
} else {
|
|
312
|
-
throw new ModelNotFoundError(
|
|
313
|
-
`${this.modelPath} is not a valid model name. Model files must end in .malloy.`,
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
public getNotebookError(): MalloyError | Error | undefined {
|
|
319
|
-
return this.compilationError;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
public async getNotebook(): Promise<ApiRawNotebook> {
|
|
323
|
-
if (this.compilationError) {
|
|
324
|
-
throw this.compilationError;
|
|
325
|
-
}
|
|
326
|
-
if (this.modelType === "notebook") {
|
|
327
|
-
return this.getNotebookModel();
|
|
328
|
-
} else {
|
|
329
|
-
throw new ModelNotFoundError(
|
|
330
|
-
`${this.modelPath} is not a valid notebook name. Notebook files must end in .malloynb.`,
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
public async getQueryResults(
|
|
336
|
-
sourceName?: string,
|
|
337
|
-
queryName?: string,
|
|
338
|
-
query?: string,
|
|
339
|
-
filterParams?: FilterParams,
|
|
340
|
-
bypassFilters?: boolean,
|
|
341
|
-
): Promise<{
|
|
342
|
-
result: Malloy.Result;
|
|
343
|
-
compactResult: QueryData;
|
|
344
|
-
modelInfo: Malloy.ModelInfo;
|
|
345
|
-
dataStyles: DataStyles;
|
|
346
|
-
}> {
|
|
347
|
-
const startTime = performance.now();
|
|
348
|
-
if (this.compilationError) {
|
|
349
|
-
// Re-throw MalloyError and ModelCompilationError as-is (they map to 400/424)
|
|
350
|
-
if (
|
|
351
|
-
this.compilationError instanceof MalloyError ||
|
|
352
|
-
this.compilationError instanceof ModelCompilationError
|
|
353
|
-
) {
|
|
354
|
-
throw this.compilationError;
|
|
355
|
-
}
|
|
356
|
-
// For other compilation errors, wrap as BadRequestError (400)
|
|
357
|
-
throw new BadRequestError(
|
|
358
|
-
`Model compilation failed: ${this.compilationError.message}`,
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
let runnable: QueryMaterializer;
|
|
362
|
-
if (!this.modelMaterializer || !this.modelDef || !this.modelInfo)
|
|
363
|
-
throw new BadRequestError("Model has no queryable entities.");
|
|
364
|
-
|
|
365
|
-
// Wrap loadQuery calls in try-catch to handle query parsing errors
|
|
366
|
-
try {
|
|
367
|
-
let queryString: string;
|
|
368
|
-
if (!sourceName && !queryName && query) {
|
|
369
|
-
queryString = "\n" + query;
|
|
370
|
-
} else if (queryName && !query) {
|
|
371
|
-
queryString = `\nrun: ${sourceName ? sourceName + "->" : ""}${queryName}`;
|
|
372
|
-
} else {
|
|
373
|
-
const endTime = performance.now();
|
|
374
|
-
const executionTime = endTime - startTime;
|
|
375
|
-
this.queryExecutionHistogram.record(executionTime, {
|
|
376
|
-
"malloy.model.path": this.modelPath,
|
|
377
|
-
"malloy.model.query.name": queryName,
|
|
378
|
-
"malloy.model.query.source": sourceName,
|
|
379
|
-
"malloy.model.query.query": query,
|
|
380
|
-
"malloy.model.query.status": "error",
|
|
381
|
-
});
|
|
382
|
-
throw new BadRequestError(
|
|
383
|
-
"Invalid query request. (Query AND !sourceName) OR (queryName AND sourceName) must be defined.",
|
|
384
|
-
);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// Inject source filter predicates unless bypassed
|
|
388
|
-
if (!bypassFilters) {
|
|
389
|
-
const effectiveSource = sourceName ?? this.extractSourceName(query);
|
|
390
|
-
if (effectiveSource) {
|
|
391
|
-
const filters = this.getFilters(effectiveSource);
|
|
392
|
-
if (filters.length > 0) {
|
|
393
|
-
const filterClause = buildFilterClause(
|
|
394
|
-
filters,
|
|
395
|
-
filterParams ?? {},
|
|
396
|
-
);
|
|
397
|
-
queryString = injectFilterRefinement(
|
|
398
|
-
queryString,
|
|
399
|
-
filterClause,
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
runnable = this.modelMaterializer.loadQuery(queryString);
|
|
406
|
-
} catch (error) {
|
|
407
|
-
// Re-throw BadRequestError as-is
|
|
408
|
-
if (error instanceof BadRequestError) {
|
|
409
|
-
throw error;
|
|
410
|
-
}
|
|
411
|
-
// Source filter validation errors are client errors (400)
|
|
412
|
-
if (error instanceof FilterValidationError) {
|
|
413
|
-
throw new BadRequestError(error.message);
|
|
414
|
-
}
|
|
415
|
-
// Re-throw MalloyError as-is (maps to 400)
|
|
416
|
-
if (error instanceof MalloyError) {
|
|
417
|
-
throw error;
|
|
418
|
-
}
|
|
419
|
-
// For other query parsing errors, wrap as BadRequestError
|
|
420
|
-
const errorMessage =
|
|
421
|
-
error instanceof Error ? error.message : String(error);
|
|
422
|
-
logger.error("Query parsing error", {
|
|
423
|
-
error,
|
|
424
|
-
errorMessage,
|
|
425
|
-
projectName: this.packageName,
|
|
426
|
-
modelPath: this.modelPath,
|
|
427
|
-
query,
|
|
428
|
-
queryName,
|
|
429
|
-
sourceName,
|
|
430
|
-
});
|
|
431
|
-
throw new BadRequestError(`Invalid query: ${errorMessage}`);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const rowLimit =
|
|
435
|
-
(await runnable.getPreparedResult()).resultExplore.limit || ROW_LIMIT;
|
|
436
|
-
const endTime = performance.now();
|
|
437
|
-
const executionTime = endTime - startTime;
|
|
438
|
-
|
|
439
|
-
let queryResults;
|
|
440
|
-
try {
|
|
441
|
-
queryResults = await runnable.run({ rowLimit });
|
|
442
|
-
} catch (error) {
|
|
443
|
-
// Record error metrics
|
|
444
|
-
const errorEndTime = performance.now();
|
|
445
|
-
const errorExecutionTime = errorEndTime - startTime;
|
|
446
|
-
this.queryExecutionHistogram.record(errorExecutionTime, {
|
|
447
|
-
"malloy.model.path": this.modelPath,
|
|
448
|
-
"malloy.model.query.name": queryName,
|
|
449
|
-
"malloy.model.query.source": sourceName,
|
|
450
|
-
"malloy.model.query.query": query,
|
|
451
|
-
"malloy.model.query.status": "error",
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
// Re-throw Malloy errors as-is (they will be handled by error handler)
|
|
455
|
-
if (error instanceof MalloyError) {
|
|
456
|
-
throw error;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// For other runtime errors (like divide by zero), throw as BadRequestError
|
|
460
|
-
const errorMessage =
|
|
461
|
-
error instanceof Error ? error.message : String(error);
|
|
462
|
-
logger.error("Query execution error", {
|
|
463
|
-
error,
|
|
464
|
-
errorMessage,
|
|
465
|
-
projectName: this.packageName,
|
|
466
|
-
modelPath: this.modelPath,
|
|
467
|
-
query,
|
|
468
|
-
queryName,
|
|
469
|
-
sourceName,
|
|
470
|
-
});
|
|
471
|
-
throw new BadRequestError(`Query execution failed: ${errorMessage}`);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
this.queryExecutionHistogram.record(executionTime, {
|
|
475
|
-
"malloy.model.path": this.modelPath,
|
|
476
|
-
"malloy.model.query.name": queryName,
|
|
477
|
-
"malloy.model.query.source": sourceName,
|
|
478
|
-
"malloy.model.query.query": query,
|
|
479
|
-
"malloy.model.query.rows_limit": rowLimit,
|
|
480
|
-
"malloy.model.query.rows_total": queryResults.totalRows,
|
|
481
|
-
"malloy.model.query.connection": queryResults.connectionName,
|
|
482
|
-
"malloy.model.query.status": "success",
|
|
483
|
-
});
|
|
484
|
-
return {
|
|
485
|
-
result: API.util.wrapResult(queryResults),
|
|
486
|
-
compactResult: queryResults.data.value,
|
|
487
|
-
modelInfo: this.modelInfo,
|
|
488
|
-
dataStyles: this.dataStyles,
|
|
489
|
-
};
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
private getStandardModel(): ApiCompiledModel {
|
|
493
|
-
return {
|
|
494
|
-
type: "source",
|
|
495
|
-
packageName: this.packageName,
|
|
496
|
-
modelPath: this.modelPath,
|
|
497
|
-
malloyVersion: MALLOY_VERSION,
|
|
498
|
-
dataStyles: JSON.stringify(this.dataStyles),
|
|
499
|
-
modelDef: JSON.stringify(this.modelDef),
|
|
500
|
-
modelInfo: JSON.stringify(
|
|
501
|
-
this.modelDef ? modelDefToModelInfo(this.modelDef) : {},
|
|
502
|
-
),
|
|
503
|
-
sourceInfos: this.getSourceInfos()?.map((sourceInfo) =>
|
|
504
|
-
JSON.stringify(sourceInfo),
|
|
505
|
-
),
|
|
506
|
-
sources: this.sources,
|
|
507
|
-
queries: this.queries,
|
|
508
|
-
} as ApiCompiledModel;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
private async getNotebookModel(): Promise<ApiRawNotebook> {
|
|
512
|
-
// Return raw cell contents without executing them
|
|
513
|
-
const notebookCells: ApiNotebookCell[] = (
|
|
514
|
-
this.runnableNotebookCells as RunnableNotebookCell[]
|
|
515
|
-
).map((cell) => {
|
|
516
|
-
logger.debug("cell.queryInfo", cell.queryInfo);
|
|
517
|
-
return {
|
|
518
|
-
type: cell.type,
|
|
519
|
-
text: cell.text,
|
|
520
|
-
newSources: cell.newSources?.map((source) =>
|
|
521
|
-
JSON.stringify(source),
|
|
522
|
-
),
|
|
523
|
-
queryInfo: cell.queryInfo
|
|
524
|
-
? JSON.stringify(cell.queryInfo)
|
|
525
|
-
: undefined,
|
|
526
|
-
} as ApiNotebookCell;
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
// Collect all annotations from the inherits chain
|
|
530
|
-
const allAnnotations: string[] = [];
|
|
531
|
-
if (this.modelDef) {
|
|
532
|
-
// Traverse the inherits chain to collect all annotations
|
|
533
|
-
// Type as Annotation to handle the inherits chain properly
|
|
534
|
-
let currentAnnotation: Annotation | undefined =
|
|
535
|
-
this.modelDef.annotation;
|
|
536
|
-
|
|
537
|
-
while (currentAnnotation) {
|
|
538
|
-
if (currentAnnotation.notes) {
|
|
539
|
-
allAnnotations.push(
|
|
540
|
-
...currentAnnotation.notes.map((note) => note.text),
|
|
541
|
-
);
|
|
542
|
-
}
|
|
543
|
-
// Navigate to the inherited annotation if it exists
|
|
544
|
-
currentAnnotation = currentAnnotation.inherits;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
return {
|
|
549
|
-
type: "notebook",
|
|
550
|
-
packageName: this.packageName,
|
|
551
|
-
modelPath: this.modelPath,
|
|
552
|
-
malloyVersion: MALLOY_VERSION,
|
|
553
|
-
modelInfo: JSON.stringify(
|
|
554
|
-
this.modelDef ? modelDefToModelInfo(this.modelDef) : {},
|
|
555
|
-
),
|
|
556
|
-
sources: this.modelDef && this.sources,
|
|
557
|
-
queries: this.modelDef && this.queries,
|
|
558
|
-
annotations: allAnnotations,
|
|
559
|
-
notebookCells,
|
|
560
|
-
} as ApiRawNotebook;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
public async executeNotebookCell(
|
|
564
|
-
cellIndex: number,
|
|
565
|
-
filterParams?: FilterParams,
|
|
566
|
-
bypassFilters?: boolean,
|
|
567
|
-
): Promise<{
|
|
568
|
-
type: "code" | "markdown";
|
|
569
|
-
text: string;
|
|
570
|
-
queryName?: string;
|
|
571
|
-
result?: string;
|
|
572
|
-
newSources?: string[];
|
|
573
|
-
}> {
|
|
574
|
-
if (this.compilationError) {
|
|
575
|
-
throw this.compilationError;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
if (!this.runnableNotebookCells) {
|
|
579
|
-
throw new BadRequestError("No notebook cells available");
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
if (cellIndex < 0 || cellIndex >= this.runnableNotebookCells.length) {
|
|
583
|
-
throw new BadRequestError(
|
|
584
|
-
`Cell index ${cellIndex} out of range (0-${this.runnableNotebookCells.length - 1})`,
|
|
585
|
-
);
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
const cell = this.runnableNotebookCells[cellIndex];
|
|
589
|
-
|
|
590
|
-
if (cell.type === "markdown") {
|
|
591
|
-
return {
|
|
592
|
-
type: cell.type,
|
|
593
|
-
text: cell.text,
|
|
594
|
-
};
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
// For code cells, execute the runnable if available
|
|
598
|
-
let queryName: string | undefined = undefined;
|
|
599
|
-
let queryResult: string | undefined = undefined;
|
|
600
|
-
|
|
601
|
-
if (cell.runnable) {
|
|
602
|
-
try {
|
|
603
|
-
let runnableToExecute = cell.runnable;
|
|
604
|
-
|
|
605
|
-
// If filters need to be applied, rebuild the query with a refinement
|
|
606
|
-
if (!bypassFilters && cell.modelMaterializer) {
|
|
607
|
-
const effectiveSource = this.extractSourceName(cell.text);
|
|
608
|
-
if (effectiveSource) {
|
|
609
|
-
const filters = this.getFilters(effectiveSource);
|
|
610
|
-
if (filters.length > 0) {
|
|
611
|
-
const filterClause = buildFilterClause(
|
|
612
|
-
filters,
|
|
613
|
-
filterParams ?? {},
|
|
614
|
-
);
|
|
615
|
-
if (filterClause) {
|
|
616
|
-
const refinedQuery = injectFilterRefinement(
|
|
617
|
-
cell.text,
|
|
618
|
-
filterClause,
|
|
619
|
-
);
|
|
620
|
-
runnableToExecute =
|
|
621
|
-
cell.modelMaterializer.loadQuery(refinedQuery);
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
const rowLimit =
|
|
628
|
-
(await runnableToExecute.getPreparedResult()).resultExplore
|
|
629
|
-
.limit || ROW_LIMIT;
|
|
630
|
-
const result = await runnableToExecute.run({ rowLimit });
|
|
631
|
-
const query = (await runnableToExecute.getPreparedQuery())._query;
|
|
632
|
-
queryName = (query as NamedQueryDef).as || query.name;
|
|
633
|
-
queryResult =
|
|
634
|
-
result?._queryResult &&
|
|
635
|
-
this.modelInfo &&
|
|
636
|
-
JSON.stringify(API.util.wrapResult(result));
|
|
637
|
-
} catch (error) {
|
|
638
|
-
if (error instanceof FilterValidationError) {
|
|
639
|
-
throw new BadRequestError(error.message);
|
|
640
|
-
}
|
|
641
|
-
if (error instanceof MalloyError) {
|
|
642
|
-
throw error;
|
|
643
|
-
}
|
|
644
|
-
const errorMessage =
|
|
645
|
-
error instanceof Error ? error.message : String(error);
|
|
646
|
-
if (errorMessage.trim() === "Model has no queries.") {
|
|
647
|
-
return {
|
|
648
|
-
type: "code",
|
|
649
|
-
text: cell.text,
|
|
650
|
-
};
|
|
651
|
-
} else {
|
|
652
|
-
logger.error("Error message: ", errorMessage);
|
|
653
|
-
}
|
|
654
|
-
logger.debug("Cell content: ", cellIndex, cell.type, cell.text);
|
|
655
|
-
throw new BadRequestError(`Cell execution failed: ${errorMessage}`);
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
return {
|
|
660
|
-
type: cell.type,
|
|
661
|
-
text: cell.text,
|
|
662
|
-
queryName: queryName,
|
|
663
|
-
result: queryResult,
|
|
664
|
-
newSources: cell.newSources?.map((source) => JSON.stringify(source)),
|
|
665
|
-
};
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
static async getModelRuntime(
|
|
669
|
-
packagePath: string,
|
|
670
|
-
modelPath: string,
|
|
671
|
-
connections: Map<string, Connection>,
|
|
672
|
-
options?: { buildManifest?: BuildManifest["entries"] },
|
|
673
|
-
): Promise<{
|
|
674
|
-
runtime: Runtime;
|
|
675
|
-
modelURL: URL;
|
|
676
|
-
importBaseURL: URL;
|
|
677
|
-
dataStyles: DataStyles;
|
|
678
|
-
modelType: ModelType;
|
|
679
|
-
}> {
|
|
680
|
-
const fullModelPath = path.join(packagePath, modelPath);
|
|
681
|
-
try {
|
|
682
|
-
if (!(await fs.stat(fullModelPath)).isFile()) {
|
|
683
|
-
throw new ModelNotFoundError(`${modelPath} is not a file.`);
|
|
684
|
-
}
|
|
685
|
-
} catch {
|
|
686
|
-
throw new ModelNotFoundError(`${modelPath} does not exist.`);
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
let modelType: ModelType;
|
|
690
|
-
if (modelPath.endsWith(MODEL_FILE_SUFFIX)) {
|
|
691
|
-
modelType = "model";
|
|
692
|
-
} else if (modelPath.endsWith(NOTEBOOK_FILE_SUFFIX)) {
|
|
693
|
-
modelType = "notebook";
|
|
694
|
-
} else {
|
|
695
|
-
throw new ModelNotFoundError(
|
|
696
|
-
`${modelPath} is not a valid model name. Model files must end in .malloy or .malloynb.`,
|
|
697
|
-
);
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
const modelURL = new URL(`file://${fullModelPath}`);
|
|
701
|
-
const baseUrl = new URL(".", modelURL);
|
|
702
|
-
const fileUrl = new URL(baseUrl.pathname, "file:");
|
|
703
|
-
const workingDirectory = fileURLToPath(fileUrl);
|
|
704
|
-
const importBaseURL = new URL(baseUrl.pathname + "/", "file:");
|
|
705
|
-
const urlReader = new HackyDataStylesAccumulator(URL_READER);
|
|
706
|
-
|
|
707
|
-
const duckdbConnection = connections.get("duckdb") as DuckDBConnection;
|
|
708
|
-
await duckdbConnection.runSQL(
|
|
709
|
-
`SET FILE_SEARCH_PATH='${workingDirectory}';`,
|
|
710
|
-
);
|
|
711
|
-
|
|
712
|
-
const runtimeOptions: {
|
|
713
|
-
urlReader: typeof urlReader;
|
|
714
|
-
connections: FixedConnectionMap;
|
|
715
|
-
buildManifest?: BuildManifest;
|
|
716
|
-
} = {
|
|
717
|
-
urlReader,
|
|
718
|
-
connections: new FixedConnectionMap(connections, "duckdb"),
|
|
719
|
-
};
|
|
720
|
-
|
|
721
|
-
if (options?.buildManifest) {
|
|
722
|
-
runtimeOptions.buildManifest = {
|
|
723
|
-
entries: options.buildManifest,
|
|
724
|
-
strict: false,
|
|
725
|
-
};
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
const runtime = new Runtime(runtimeOptions);
|
|
729
|
-
const dataStyles = urlReader.getHackyAccumulatedDataStyles();
|
|
730
|
-
return { runtime, modelURL, importBaseURL, dataStyles, modelType };
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
private static getQueries(
|
|
734
|
-
modelPath: string,
|
|
735
|
-
modelDef: ModelDef,
|
|
736
|
-
): ApiQuery[] {
|
|
737
|
-
const isNamedQuery = (
|
|
738
|
-
object: NamedModelObject,
|
|
739
|
-
): object is NamedQueryDef => object.type === "query";
|
|
740
|
-
return Object.values(modelDef.contents)
|
|
741
|
-
.filter(isNamedQuery)
|
|
742
|
-
.map((queryObj: NamedQueryDef) => ({
|
|
743
|
-
name: queryObj.as || queryObj.name,
|
|
744
|
-
// What to do when the source is not a string?
|
|
745
|
-
sourceName:
|
|
746
|
-
typeof queryObj.structRef === "string"
|
|
747
|
-
? queryObj.structRef
|
|
748
|
-
: undefined,
|
|
749
|
-
annotations: queryObj?.annotation?.blockNotes
|
|
750
|
-
?.filter((note: { at: { url: string } }) =>
|
|
751
|
-
note.at.url.includes(modelPath),
|
|
752
|
-
)
|
|
753
|
-
.map((note: { text: string }) => note.text),
|
|
754
|
-
}));
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
private static getSources(
|
|
758
|
-
modelPath: string,
|
|
759
|
-
modelDef: ModelDef,
|
|
760
|
-
): {
|
|
761
|
-
sources: ApiSource[];
|
|
762
|
-
filterMap: Map<string, FilterDefinition[]>;
|
|
763
|
-
} {
|
|
764
|
-
const filterMap = new Map<string, FilterDefinition[]>();
|
|
765
|
-
|
|
766
|
-
const sources = Object.values(modelDef.contents)
|
|
767
|
-
.filter((obj) => isSourceDef(obj))
|
|
768
|
-
.map((sourceObj) => {
|
|
769
|
-
const sourceName = sourceObj.as || sourceObj.name;
|
|
770
|
-
const annotations = (sourceObj as StructDef).annotation?.blockNotes
|
|
771
|
-
?.filter((note) => note.at.url.includes(modelPath))
|
|
772
|
-
.map((note) => note.text);
|
|
773
|
-
|
|
774
|
-
// Parse #(filter) from ALL annotations, traversing the inherits
|
|
775
|
-
// chain so that filters on a base source (e.g. `recalls`) are
|
|
776
|
-
// picked up by an extending source (`manufacturer_recalls is
|
|
777
|
-
// recalls extend {}`). The Malloy compiler stores the base
|
|
778
|
-
// source's annotations in `annotation.inherits`.
|
|
779
|
-
//
|
|
780
|
-
// The chain goes child → parent, so we collect child-first.
|
|
781
|
-
// parseFilters uses "last wins" dedup, so we reverse to put
|
|
782
|
-
// parent annotations first and child annotations last (winning).
|
|
783
|
-
const collectedAnnotations: string[][] = [];
|
|
784
|
-
let curAnnotation: Annotation | undefined = (sourceObj as StructDef)
|
|
785
|
-
.annotation;
|
|
786
|
-
while (curAnnotation) {
|
|
787
|
-
if (curAnnotation.blockNotes) {
|
|
788
|
-
collectedAnnotations.push(
|
|
789
|
-
curAnnotation.blockNotes.map((note) => note.text),
|
|
790
|
-
);
|
|
791
|
-
}
|
|
792
|
-
curAnnotation = curAnnotation.inherits;
|
|
793
|
-
}
|
|
794
|
-
const allAnnotations = collectedAnnotations.reverse().flat();
|
|
795
|
-
let filters: ApiFilter[] | undefined;
|
|
796
|
-
if (allAnnotations.length > 0) {
|
|
797
|
-
try {
|
|
798
|
-
const parsed = parseFilters(allAnnotations);
|
|
799
|
-
if (parsed.length > 0) {
|
|
800
|
-
filterMap.set(sourceName, parsed);
|
|
801
|
-
const structFields = (sourceObj as StructDef).fields;
|
|
802
|
-
filters = parsed.map((f) => {
|
|
803
|
-
const field = structFields.find(
|
|
804
|
-
(fd) => (fd.as || fd.name) === f.dimension,
|
|
805
|
-
);
|
|
806
|
-
return {
|
|
807
|
-
name: f.name,
|
|
808
|
-
dimension: f.dimension,
|
|
809
|
-
type: f.type,
|
|
810
|
-
implicit: f.implicit,
|
|
811
|
-
required: f.required,
|
|
812
|
-
dimensionType: field?.type as string | undefined,
|
|
813
|
-
};
|
|
814
|
-
});
|
|
815
|
-
}
|
|
816
|
-
} catch (err) {
|
|
817
|
-
logger.warn(
|
|
818
|
-
`Failed to parse filter annotations on source "${sourceName}"`,
|
|
819
|
-
{ error: err },
|
|
820
|
-
);
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
const views = (sourceObj as StructDef).fields
|
|
825
|
-
.filter((turtleObj) => turtleObj.type === "turtle")
|
|
826
|
-
.filter((turtleObj) =>
|
|
827
|
-
// TODO(kjnesbit): Fix non-reduce views. Filter out
|
|
828
|
-
// non-reduce views, i.e., indexes. Need to discuss with Will.
|
|
829
|
-
(turtleObj as TurtleDef).pipeline
|
|
830
|
-
.map((stage) => stage.type)
|
|
831
|
-
.every((type) => type == "reduce"),
|
|
832
|
-
)
|
|
833
|
-
.map(
|
|
834
|
-
(turtleObj) =>
|
|
835
|
-
({
|
|
836
|
-
name: turtleObj.as || turtleObj.name,
|
|
837
|
-
annotations: turtleObj?.annotation?.blockNotes
|
|
838
|
-
?.filter((note) => note.at.url.includes(modelPath))
|
|
839
|
-
.map((note) => note.text),
|
|
840
|
-
}) as ApiView,
|
|
841
|
-
);
|
|
842
|
-
|
|
843
|
-
return {
|
|
844
|
-
name: sourceName,
|
|
845
|
-
annotations,
|
|
846
|
-
views,
|
|
847
|
-
filters,
|
|
848
|
-
} as ApiSource;
|
|
849
|
-
});
|
|
850
|
-
|
|
851
|
-
return { sources, filterMap };
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
static async getModelMaterializer(
|
|
855
|
-
runtime: Runtime,
|
|
856
|
-
importBaseURL: URL,
|
|
857
|
-
modelURL: URL,
|
|
858
|
-
modelPath: string,
|
|
859
|
-
): Promise<{
|
|
860
|
-
modelMaterializer: ModelMaterializer | undefined;
|
|
861
|
-
runnableNotebookCells: RunnableNotebookCell[] | undefined;
|
|
862
|
-
}> {
|
|
863
|
-
if (modelPath.endsWith(MODEL_FILE_SUFFIX)) {
|
|
864
|
-
const modelMaterializer = await Model.getStandardModelMaterializer(
|
|
865
|
-
runtime,
|
|
866
|
-
importBaseURL,
|
|
867
|
-
modelURL,
|
|
868
|
-
modelPath,
|
|
869
|
-
);
|
|
870
|
-
return {
|
|
871
|
-
modelMaterializer,
|
|
872
|
-
runnableNotebookCells: undefined,
|
|
873
|
-
};
|
|
874
|
-
} else if (modelPath.endsWith(NOTEBOOK_FILE_SUFFIX)) {
|
|
875
|
-
const { modelMaterializer: mm, runnableNotebookCells: rnc } =
|
|
876
|
-
await Model.getNotebookModelMaterializer(
|
|
877
|
-
runtime,
|
|
878
|
-
importBaseURL,
|
|
879
|
-
modelURL,
|
|
880
|
-
modelPath,
|
|
881
|
-
);
|
|
882
|
-
return {
|
|
883
|
-
modelMaterializer: mm,
|
|
884
|
-
runnableNotebookCells: rnc,
|
|
885
|
-
};
|
|
886
|
-
} else {
|
|
887
|
-
throw new Error(
|
|
888
|
-
`${modelPath} is not a valid model name. Model files must end in .malloy or .malloynb.`,
|
|
889
|
-
);
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
private static async getStandardModelMaterializer(
|
|
894
|
-
runtime: Runtime,
|
|
895
|
-
importBaseURL: URL,
|
|
896
|
-
modelURL: URL,
|
|
897
|
-
modelPath: string,
|
|
898
|
-
): Promise<ModelMaterializer> {
|
|
899
|
-
const mm = runtime.loadModel(modelURL, { importBaseURL });
|
|
900
|
-
if (!mm) {
|
|
901
|
-
throw new Error(`Invalid model ${modelPath}.`);
|
|
902
|
-
}
|
|
903
|
-
return mm;
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
private static async getNotebookModelMaterializer(
|
|
907
|
-
runtime: Runtime,
|
|
908
|
-
importBaseURL: URL,
|
|
909
|
-
modelURL: URL,
|
|
910
|
-
modelPath: string,
|
|
911
|
-
): Promise<{
|
|
912
|
-
modelMaterializer: ModelMaterializer | undefined;
|
|
913
|
-
runnableNotebookCells: RunnableNotebookCell[];
|
|
914
|
-
}> {
|
|
915
|
-
let fileContents = undefined;
|
|
916
|
-
let parse = undefined;
|
|
917
|
-
|
|
918
|
-
try {
|
|
919
|
-
fileContents = await fs.readFile(modelURL, "utf8");
|
|
920
|
-
} catch {
|
|
921
|
-
throw new ModelNotFoundError("Model not found: " + modelPath);
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
try {
|
|
925
|
-
parse = MalloySQLParser.parse(fileContents, modelPath);
|
|
926
|
-
} catch {
|
|
927
|
-
throw new Error("Could not parse model: " + modelPath);
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
let mm: ModelMaterializer | undefined = undefined;
|
|
931
|
-
const oldImports: string[] = [];
|
|
932
|
-
const oldSources: Record<string, Malloy.SourceInfo> = {};
|
|
933
|
-
// First generate the sequence of ModelMaterializers.
|
|
934
|
-
// This has to happen sync, since mm.getModel() is async and
|
|
935
|
-
// may execute out-of-order.
|
|
936
|
-
const mms = parse.statements.map((stmt) => {
|
|
937
|
-
if (stmt.type === MalloySQLStatementType.MALLOY) {
|
|
938
|
-
if (!mm) {
|
|
939
|
-
mm = runtime.loadModel(stmt.text, { importBaseURL });
|
|
940
|
-
} else {
|
|
941
|
-
mm = mm.extendModel(stmt.text, { importBaseURL });
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
return mm;
|
|
945
|
-
});
|
|
946
|
-
const runnableNotebookCells: RunnableNotebookCell[] = (
|
|
947
|
-
await Promise.all(
|
|
948
|
-
parse.statements.map(async (stmt, index) => {
|
|
949
|
-
if (stmt.type === MalloySQLStatementType.MALLOY) {
|
|
950
|
-
// Get the Materializer for the current cell/statement.
|
|
951
|
-
const localMM = mms[index];
|
|
952
|
-
if (!localMM) {
|
|
953
|
-
// This can't happen because the to be in this branch there stmt must be
|
|
954
|
-
// MalloySQLStatementType.MALLOY and we must have a model materializer.
|
|
955
|
-
throw new Error("Model materializer is undefined");
|
|
956
|
-
}
|
|
957
|
-
// Pull available sources from the current model.
|
|
958
|
-
// Add any of then that are new into newSources and then add them to oldSources.
|
|
959
|
-
const currentModelDef = (await localMM.getModel())._modelDef;
|
|
960
|
-
let newSources: Malloy.SourceInfo[] = [];
|
|
961
|
-
const newImports = currentModelDef.imports?.slice(
|
|
962
|
-
oldImports.length,
|
|
963
|
-
);
|
|
964
|
-
if (newImports) {
|
|
965
|
-
await Promise.all(
|
|
966
|
-
newImports.map(async (importLocation) => {
|
|
967
|
-
const modelString = await runtime.urlReader.readURL(
|
|
968
|
-
new URL(importLocation.importURL),
|
|
969
|
-
);
|
|
970
|
-
const importModel = (
|
|
971
|
-
await runtime
|
|
972
|
-
.loadModel(modelString as string, {
|
|
973
|
-
importBaseURL,
|
|
974
|
-
})
|
|
975
|
-
.getModel()
|
|
976
|
-
)._modelDef;
|
|
977
|
-
const importModelInfo =
|
|
978
|
-
modelDefToModelInfo(importModel);
|
|
979
|
-
newSources = importModelInfo.entries
|
|
980
|
-
.filter((entry) => entry.kind === "source")
|
|
981
|
-
.filter(
|
|
982
|
-
(source) => !(source.name in oldSources),
|
|
983
|
-
) as Malloy.SourceInfo[];
|
|
984
|
-
oldImports.push(importLocation.importURL.toString());
|
|
985
|
-
}),
|
|
986
|
-
);
|
|
987
|
-
}
|
|
988
|
-
const currentModelInfo = modelDefToModelInfo(currentModelDef);
|
|
989
|
-
newSources = newSources.concat(
|
|
990
|
-
currentModelInfo.entries
|
|
991
|
-
.filter((entry) => entry.kind === "source")
|
|
992
|
-
.filter(
|
|
993
|
-
(source) => !(source.name in oldSources),
|
|
994
|
-
) as Malloy.SourceInfo[],
|
|
995
|
-
);
|
|
996
|
-
|
|
997
|
-
for (const source of newSources) {
|
|
998
|
-
oldSources[source.name] = source;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
const runnable = localMM.loadFinalQuery();
|
|
1002
|
-
|
|
1003
|
-
// Extract QueryInfo from the runnable
|
|
1004
|
-
let queryInfo: Malloy.QueryInfo | undefined = undefined;
|
|
1005
|
-
try {
|
|
1006
|
-
const preparedQuery = await runnable.getPreparedQuery();
|
|
1007
|
-
const query = preparedQuery._query as NamedQueryDef;
|
|
1008
|
-
const queryName = query.as || query.name;
|
|
1009
|
-
const anonymousQuery =
|
|
1010
|
-
currentModelInfo.anonymous_queries[
|
|
1011
|
-
currentModelInfo.anonymous_queries.length - 1
|
|
1012
|
-
];
|
|
1013
|
-
|
|
1014
|
-
if (anonymousQuery) {
|
|
1015
|
-
queryInfo = {
|
|
1016
|
-
name: queryName,
|
|
1017
|
-
schema: anonymousQuery.schema,
|
|
1018
|
-
annotations: anonymousQuery.annotations,
|
|
1019
|
-
definition: anonymousQuery.definition,
|
|
1020
|
-
code: anonymousQuery.code,
|
|
1021
|
-
location: anonymousQuery.location,
|
|
1022
|
-
} as Malloy.QueryInfo;
|
|
1023
|
-
}
|
|
1024
|
-
} catch (_error) {
|
|
1025
|
-
// If we can't extract query info (e.g., no query in cell), that's okay
|
|
1026
|
-
// This can happen for cells that only define sources
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
return {
|
|
1030
|
-
type: "code",
|
|
1031
|
-
text: stmt.text,
|
|
1032
|
-
runnable: runnable,
|
|
1033
|
-
modelMaterializer: localMM,
|
|
1034
|
-
newSources,
|
|
1035
|
-
queryInfo,
|
|
1036
|
-
} as RunnableNotebookCell;
|
|
1037
|
-
} else if (stmt.type === MalloySQLStatementType.MARKDOWN) {
|
|
1038
|
-
return {
|
|
1039
|
-
type: "markdown",
|
|
1040
|
-
text: stmt.text,
|
|
1041
|
-
} as RunnableNotebookCell;
|
|
1042
|
-
} else {
|
|
1043
|
-
return undefined;
|
|
1044
|
-
}
|
|
1045
|
-
}),
|
|
1046
|
-
)
|
|
1047
|
-
).filter((cell) => cell !== undefined);
|
|
1048
|
-
|
|
1049
|
-
return {
|
|
1050
|
-
modelMaterializer: mm,
|
|
1051
|
-
runnableNotebookCells: runnableNotebookCells,
|
|
1052
|
-
};
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
public getModelType(): ModelType {
|
|
1056
|
-
return this.modelType;
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
public async getFileText(packagePath: string): Promise<string> {
|
|
1060
|
-
const fullPath = path.join(packagePath, this.modelPath);
|
|
1061
|
-
try {
|
|
1062
|
-
return await fs.readFile(fullPath, "utf8");
|
|
1063
|
-
} catch {
|
|
1064
|
-
throw new ModelNotFoundError(
|
|
1065
|
-
`Model file not found: ${this.modelPath}`,
|
|
1066
|
-
);
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
}
|