@malloy-publisher/server 0.0.198 → 0.0.200
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build.ts +30 -1
- package/dist/app/api-doc.yaml +127 -111
- package/dist/app/assets/{EnvironmentPage-C7rtH4mC.js → EnvironmentPage-CgKNjySu.js} +1 -1
- package/dist/app/assets/HomePage-BPIpMBjW.js +1 -0
- package/dist/app/assets/{MainPage-D38LtZDV.js → MainPage-CAwb8U82.js} +2 -2
- package/dist/app/assets/{ModelPage-DOol8Mz7.js → ModelPage-C0Uevsw9.js} +1 -1
- package/dist/app/assets/{PackagePage-0tgzA_kO.js → PackagePage-Cu-u9k1g.js} +1 -1
- package/dist/app/assets/{RouteError-BaMsOSly.js → RouteError-DVwPh2Ql.js} +1 -1
- package/dist/app/assets/{WorkbookPage-Cx4SePkx.js → WorkbookPage-DW38R2Zv.js} +1 -1
- package/dist/app/assets/{core-CbsC6R_Y.es-Cwf6asf3.js → core-C0vCMRDQ.es-D_ytHhjS.js} +10 -10
- package/dist/app/assets/{index-DL6BZTuw.js → index-BGdcKsFF.js} +1 -1
- package/dist/app/assets/{index-DNofXMxi.js → index-CTx4v4_3.js} +1 -1
- package/dist/app/assets/index-DE6d5jEy.js +452 -0
- package/dist/app/assets/{index.umd-B68wGGkM.js → index.umd-C1Mi1uRm.js} +1 -1
- package/dist/app/index.html +1 -1
- package/dist/instrumentation.mjs +57 -36
- package/dist/package_load_worker.mjs +12213 -0
- package/dist/server.mjs +4198 -3648
- package/package.json +2 -3
- package/src/config.spec.ts +246 -0
- package/src/config.ts +121 -1
- package/src/constants.ts +84 -1
- package/src/controller/compile.controller.ts +3 -1
- package/src/controller/connection.controller.spec.ts +803 -0
- package/src/controller/connection.controller.ts +207 -20
- package/src/controller/model.controller.ts +19 -1
- package/src/controller/query.controller.ts +22 -6
- package/src/controller/watch-mode.controller.ts +11 -2
- package/src/errors.spec.ts +44 -0
- package/src/errors.ts +34 -0
- package/src/health.spec.ts +90 -0
- package/src/health.ts +88 -45
- package/src/heap_check.spec.ts +144 -0
- package/src/heap_check.ts +144 -0
- package/src/instrumentation.ts +50 -0
- package/src/mcp/handler_utils.ts +14 -0
- package/src/mcp/tools/execute_query_tool.ts +52 -10
- package/src/oom_guards.integration.spec.ts +261 -0
- package/src/package_load/package_load_pool.spec.ts +252 -0
- package/src/package_load/package_load_pool.ts +920 -0
- package/src/package_load/package_load_worker.ts +980 -0
- package/src/package_load/protocol.ts +336 -0
- package/src/path_safety.ts +9 -3
- package/src/query_cap_metrics.spec.ts +89 -0
- package/src/query_cap_metrics.ts +115 -0
- package/src/query_concurrency.spec.ts +247 -0
- package/src/query_concurrency.ts +236 -0
- package/src/query_param_utils.ts +18 -0
- package/src/query_timeout.spec.ts +224 -0
- package/src/query_timeout.ts +178 -0
- package/src/server-old.ts +21 -1
- package/src/server.ts +61 -57
- package/src/service/connection.ts +8 -2
- package/src/service/db_utils.spec.ts +1 -1
- package/src/service/environment.ts +85 -4
- package/src/service/environment_admission.spec.ts +165 -1
- package/src/service/environment_store.spec.ts +103 -0
- package/src/service/environment_store.ts +98 -26
- package/src/service/filter_integration.spec.ts +110 -0
- package/src/service/given.ts +80 -0
- package/src/service/givens_integration.spec.ts +192 -0
- package/src/service/model.spec.ts +298 -3
- package/src/service/model.ts +362 -23
- package/src/service/model_limits.spec.ts +181 -0
- package/src/service/model_limits.ts +110 -0
- package/src/service/package.spec.ts +12 -6
- package/src/service/package.ts +263 -146
- package/src/service/package_worker_path.spec.ts +196 -0
- package/src/service/path_injection.spec.ts +39 -0
- package/src/stream_helpers.spec.ts +280 -0
- package/src/stream_helpers.ts +162 -0
- package/src/test_helpers/metrics_harness.ts +126 -0
- package/tests/integration/concurrent_package/concurrent_package.integration.spec.ts +280 -0
- package/dist/app/assets/HomePage-DwkH7OrS.js +0 -1
- package/dist/app/assets/index-U38AyjJL.js +0 -451
package/src/service/package.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as fs from "fs/promises";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
|
|
4
|
-
import { DuckDBConnection } from "@malloydata/db-duckdb";
|
|
5
4
|
import "@malloydata/db-duckdb/native";
|
|
6
5
|
import {
|
|
7
6
|
Connection,
|
|
@@ -10,19 +9,26 @@ import {
|
|
|
10
9
|
EmptyURLReader,
|
|
11
10
|
FixedConnectionMap,
|
|
12
11
|
MalloyConfig,
|
|
12
|
+
MalloyError,
|
|
13
13
|
SourceDef,
|
|
14
14
|
} from "@malloydata/malloy";
|
|
15
15
|
import { metrics } from "@opentelemetry/api";
|
|
16
16
|
import recursive from "recursive-readdir";
|
|
17
17
|
import { components } from "../api";
|
|
18
|
+
import { getPackageLoadPool } from "../package_load/package_load_pool";
|
|
18
19
|
import {
|
|
19
20
|
API_PREFIX,
|
|
20
21
|
MODEL_FILE_SUFFIX,
|
|
21
22
|
NOTEBOOK_FILE_SUFFIX,
|
|
22
23
|
PACKAGE_MANIFEST_NAME,
|
|
23
24
|
} from "../constants";
|
|
24
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
ModelCompilationError,
|
|
27
|
+
PackageNotFoundError,
|
|
28
|
+
ServiceUnavailableError,
|
|
29
|
+
} from "../errors";
|
|
25
30
|
import { formatDuration, logger } from "../logger";
|
|
31
|
+
import { assertSafeEnvironmentPath, safeJoinUnderRoot } from "../path_safety";
|
|
26
32
|
import { BuildManifest } from "../storage/DatabaseInterface";
|
|
27
33
|
import { ignoreDotfiles } from "../utils";
|
|
28
34
|
import { Model } from "./model";
|
|
@@ -85,6 +91,7 @@ export class Package {
|
|
|
85
91
|
packagePath: string,
|
|
86
92
|
environmentMalloyConfig: PackageConnectionInput,
|
|
87
93
|
): Promise<Package> {
|
|
94
|
+
assertSafeEnvironmentPath(packagePath);
|
|
88
95
|
const startTime = performance.now();
|
|
89
96
|
await Package.validatePackageManifestExistsOrThrowError(packagePath);
|
|
90
97
|
const manifestValidationTime = performance.now();
|
|
@@ -94,23 +101,12 @@ export class Package {
|
|
|
94
101
|
});
|
|
95
102
|
|
|
96
103
|
try {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
),
|
|
104
|
-
});
|
|
105
|
-
packageConfig.resource = `${API_PREFIX}/environments/${environmentName}/packages/${packageName}`;
|
|
106
|
-
|
|
107
|
-
const databases = await Package.readDatabases(packagePath);
|
|
108
|
-
const databasesTime = performance.now();
|
|
109
|
-
logger.info("Databases read completed", {
|
|
110
|
-
packageName,
|
|
111
|
-
databaseCount: databases.length,
|
|
112
|
-
duration: formatDuration(databasesTime - packageConfigTime),
|
|
113
|
-
});
|
|
104
|
+
// The MalloyConfig is always built on the main thread — it
|
|
105
|
+
// owns the live native connection handles the package needs
|
|
106
|
+
// to *serve queries* after load (workers can't share native
|
|
107
|
+
// handles across the V8 isolate boundary). The worker proxies
|
|
108
|
+
// non-duckdb connection lookups back through this MalloyConfig
|
|
109
|
+
// during compile.
|
|
114
110
|
const malloyConfig = Package.buildPackageMalloyConfig(
|
|
115
111
|
packagePath,
|
|
116
112
|
typeof environmentMalloyConfig === "function"
|
|
@@ -118,68 +114,29 @@ export class Package {
|
|
|
118
114
|
: () => Package.toMalloyConfig(environmentMalloyConfig),
|
|
119
115
|
);
|
|
120
116
|
|
|
121
|
-
|
|
122
|
-
packageName,
|
|
123
|
-
packagePath,
|
|
124
|
-
malloyConfig,
|
|
125
|
-
);
|
|
126
|
-
const modelsTime = performance.now();
|
|
127
|
-
logger.info("Models loaded", {
|
|
128
|
-
packageName,
|
|
129
|
-
modelCount: models.size,
|
|
130
|
-
duration: formatDuration(modelsTime - databasesTime),
|
|
131
|
-
});
|
|
132
|
-
for (const [modelPath, model] of models.entries()) {
|
|
133
|
-
const maybeModel = model as unknown as {
|
|
134
|
-
compilationError?: unknown;
|
|
135
|
-
};
|
|
136
|
-
if (maybeModel.compilationError) {
|
|
137
|
-
const err = maybeModel.compilationError;
|
|
138
|
-
const message =
|
|
139
|
-
err instanceof Error
|
|
140
|
-
? err.message
|
|
141
|
-
: `Unknown compilation error in ${modelPath}`;
|
|
142
|
-
|
|
143
|
-
logger.error("Model compilation failed", {
|
|
144
|
-
packageName,
|
|
145
|
-
modelPath,
|
|
146
|
-
error: message,
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
this.packageLoadHistogram.record(performance.now() - startTime, {
|
|
150
|
-
malloy_package_name: packageName,
|
|
151
|
-
status: "compilation_error",
|
|
152
|
-
});
|
|
153
|
-
throw err;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
const endTime = performance.now();
|
|
157
|
-
const executionTime = endTime - startTime;
|
|
158
|
-
this.packageLoadHistogram.record(executionTime, {
|
|
159
|
-
malloy_package_name: packageName,
|
|
160
|
-
status: "success",
|
|
161
|
-
});
|
|
162
|
-
logger.info(`Successfully loaded package ${packageName}`, {
|
|
163
|
-
packageName,
|
|
164
|
-
duration: formatDuration(executionTime),
|
|
165
|
-
});
|
|
166
|
-
return new Package(
|
|
117
|
+
return await Package.loadViaWorker(
|
|
167
118
|
environmentName,
|
|
168
119
|
packageName,
|
|
169
120
|
packagePath,
|
|
170
|
-
packageConfig,
|
|
171
|
-
databases,
|
|
172
|
-
models,
|
|
173
121
|
malloyConfig,
|
|
122
|
+
startTime,
|
|
123
|
+
manifestValidationTime,
|
|
174
124
|
);
|
|
175
125
|
} catch (error) {
|
|
176
126
|
logger.error(`Error loading package ${packageName}`, { error });
|
|
177
127
|
console.error(error);
|
|
178
128
|
const endTime = performance.now();
|
|
179
129
|
const executionTime = endTime - startTime;
|
|
130
|
+
const status =
|
|
131
|
+
error instanceof ModelCompilationError ||
|
|
132
|
+
error instanceof MalloyError
|
|
133
|
+
? "compilation_error"
|
|
134
|
+
: error instanceof ServiceUnavailableError
|
|
135
|
+
? "pool_unavailable"
|
|
136
|
+
: "error";
|
|
180
137
|
this.packageLoadHistogram.record(executionTime, {
|
|
181
138
|
malloy_package_name: packageName,
|
|
182
|
-
status
|
|
139
|
+
status,
|
|
183
140
|
});
|
|
184
141
|
// Clean up package directory on failure
|
|
185
142
|
try {
|
|
@@ -197,6 +154,141 @@ export class Package {
|
|
|
197
154
|
}
|
|
198
155
|
}
|
|
199
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Load the package via the package-load worker pool. The worker
|
|
159
|
+
* performs the CPU-bound bulk of the load off-thread (manifest
|
|
160
|
+
* read, every `.malloy` / `.malloynb` compile) and ships back a
|
|
161
|
+
* structured-clonable `LoadPackageOutcome`. Database probes
|
|
162
|
+
* (`.parquet` / `.csv`) run on the main thread, in parallel with
|
|
163
|
+
* the worker compile, against the package's existing DuckDB
|
|
164
|
+
* connection — they're async-IO-bound and don't compete with the
|
|
165
|
+
* worker for CPU.
|
|
166
|
+
*
|
|
167
|
+
* Pool-infrastructure failures (worker crash, RPC timeout, pool
|
|
168
|
+
* shutting down) are rewrapped as `ServiceUnavailableError` so
|
|
169
|
+
* the HTTP layer responds 503 (transient, retryable). Real compile
|
|
170
|
+
* errors (`MalloyError` / `ModelCompilationError`) propagate
|
|
171
|
+
* unchanged so they keep their 4xx mapping.
|
|
172
|
+
*/
|
|
173
|
+
private static async loadViaWorker(
|
|
174
|
+
environmentName: string,
|
|
175
|
+
packageName: string,
|
|
176
|
+
packagePath: string,
|
|
177
|
+
malloyConfig: MalloyConfig,
|
|
178
|
+
startTime: number,
|
|
179
|
+
manifestValidationTime: number,
|
|
180
|
+
): Promise<Package> {
|
|
181
|
+
const pool = getPackageLoadPool();
|
|
182
|
+
const dispatchTime = performance.now();
|
|
183
|
+
// Submit the worker job and run database probing on the main
|
|
184
|
+
// thread in parallel. We isolate the worker-job promise inside
|
|
185
|
+
// a wrapper so we can map pool-infrastructure failures (worker
|
|
186
|
+
// crash, RPC timeout, pool shutting down) to a 503 without
|
|
187
|
+
// accidentally re-mapping `readDatabases`'s own errors.
|
|
188
|
+
const workerOutcome = pool
|
|
189
|
+
.loadPackage({
|
|
190
|
+
packagePath,
|
|
191
|
+
packageName,
|
|
192
|
+
malloyConfig,
|
|
193
|
+
defaultConnectionName: "duckdb",
|
|
194
|
+
})
|
|
195
|
+
.catch((err: unknown) => {
|
|
196
|
+
// Compile errors surface in-band via
|
|
197
|
+
// `LoadPackageOutcome.models[i].compilationError`; if the
|
|
198
|
+
// pool itself rejects, it's an infra-side failure
|
|
199
|
+
// (shutting down, worker spawn failed, worker crashed,
|
|
200
|
+
// RPC timeout) and the client should retry. Real Malloy
|
|
201
|
+
// compile errors deserialised by the pool still carry
|
|
202
|
+
// their MalloyError / ModelCompilationError identity —
|
|
203
|
+
// let those bubble untouched so they keep their 4xx
|
|
204
|
+
// mapping in `errors.ts`.
|
|
205
|
+
const realError =
|
|
206
|
+
err instanceof Error
|
|
207
|
+
? err
|
|
208
|
+
: new Error(
|
|
209
|
+
`Package-load worker pool failure: ${String(err)}`,
|
|
210
|
+
);
|
|
211
|
+
if (
|
|
212
|
+
realError instanceof MalloyError ||
|
|
213
|
+
realError instanceof ModelCompilationError
|
|
214
|
+
) {
|
|
215
|
+
throw realError;
|
|
216
|
+
}
|
|
217
|
+
throw new ServiceUnavailableError(
|
|
218
|
+
`Package-load worker pool unavailable: ${realError.message}`,
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
const [outcome, databases] = await Promise.all([
|
|
222
|
+
workerOutcome,
|
|
223
|
+
Package.readDatabases(packagePath, malloyConfig),
|
|
224
|
+
]);
|
|
225
|
+
const workerDoneTime = performance.now();
|
|
226
|
+
logger.info("Package load via worker pool completed", {
|
|
227
|
+
packageName,
|
|
228
|
+
manifestValidationMs: dispatchTime - manifestValidationTime,
|
|
229
|
+
workerDurationMs: outcome.loadDurationMs,
|
|
230
|
+
dispatchOverheadMs:
|
|
231
|
+
workerDoneTime - dispatchTime - outcome.loadDurationMs,
|
|
232
|
+
modelCount: outcome.models.length,
|
|
233
|
+
databaseCount: databases.length,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Override the manifest-derived resource URI — the worker only
|
|
237
|
+
// returns name/description from publisher.json, but the rest of
|
|
238
|
+
// the API surface expects a `resource` field too.
|
|
239
|
+
const packageConfig: ApiPackage = {
|
|
240
|
+
name: outcome.packageMetadata.name,
|
|
241
|
+
description: outcome.packageMetadata.description,
|
|
242
|
+
resource: `${API_PREFIX}/environments/${environmentName}/packages/${packageName}`,
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Build live `Model`s from worker output. Any per-model compile
|
|
246
|
+
// failure aborts the load — matches the historical behaviour of
|
|
247
|
+
// `Package.create` failing the whole package on the first model
|
|
248
|
+
// error. (`Package.reloadAllModels` keeps the failed-model
|
|
249
|
+
// placeholders instead; that branch goes through a different
|
|
250
|
+
// hydration path.)
|
|
251
|
+
const models = new Map<string, Model>();
|
|
252
|
+
for (const sm of outcome.models) {
|
|
253
|
+
if (sm.compilationError) {
|
|
254
|
+
const err = Model.deserializeCompilationError(sm.compilationError);
|
|
255
|
+
logger.error("Model compilation failed", {
|
|
256
|
+
packageName,
|
|
257
|
+
modelPath: sm.modelPath,
|
|
258
|
+
error: err.message,
|
|
259
|
+
});
|
|
260
|
+
// The outer catch in Package.create records the metric +
|
|
261
|
+
// cleans the package directory.
|
|
262
|
+
throw err;
|
|
263
|
+
}
|
|
264
|
+
models.set(
|
|
265
|
+
sm.modelPath,
|
|
266
|
+
Model.fromSerialized(packageName, packagePath, malloyConfig, sm),
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const endTime = performance.now();
|
|
271
|
+
const executionTime = endTime - startTime;
|
|
272
|
+
this.packageLoadHistogram.record(executionTime, {
|
|
273
|
+
malloy_package_name: packageName,
|
|
274
|
+
status: "success",
|
|
275
|
+
});
|
|
276
|
+
logger.info(`Successfully loaded package ${packageName}`, {
|
|
277
|
+
packageName,
|
|
278
|
+
duration: formatDuration(executionTime),
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return new Package(
|
|
282
|
+
environmentName,
|
|
283
|
+
packageName,
|
|
284
|
+
packagePath,
|
|
285
|
+
packageConfig,
|
|
286
|
+
databases,
|
|
287
|
+
models,
|
|
288
|
+
malloyConfig,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
200
292
|
public getPackageName(): string {
|
|
201
293
|
return this.packageName;
|
|
202
294
|
}
|
|
@@ -231,6 +323,21 @@ export class Package {
|
|
|
231
323
|
return Array.from(this.models.keys());
|
|
232
324
|
}
|
|
233
325
|
|
|
326
|
+
/**
|
|
327
|
+
* Re-compile every model in the package against a new build
|
|
328
|
+
* manifest (called after a materialization build commits new
|
|
329
|
+
* physicalised tables). Runs through the package-load worker pool
|
|
330
|
+
* — same off-main-thread compile path as initial `Package.create`
|
|
331
|
+
* — so a reload of a large package can't block the K8s liveness
|
|
332
|
+
* probe.
|
|
333
|
+
*
|
|
334
|
+
* Unlike `Package.create`, a per-model compile failure here does
|
|
335
|
+
* NOT abort the reload: we keep the failed model as a placeholder
|
|
336
|
+
* (`Model.fromCompilationError`) in `this.models`, matching the
|
|
337
|
+
* historical reload semantics. Whole-pool failures (worker crash,
|
|
338
|
+
* timeout, pool shutting down) propagate as `ServiceUnavailableError`
|
|
339
|
+
* — the caller (manifest service) decides how to retry.
|
|
340
|
+
*/
|
|
234
341
|
public async reloadAllModels(
|
|
235
342
|
buildManifest: BuildManifest["entries"],
|
|
236
343
|
): Promise<void> {
|
|
@@ -240,20 +347,62 @@ export class Package {
|
|
|
240
347
|
modelCount: modelPaths.length,
|
|
241
348
|
manifestEntryCount: Object.keys(buildManifest).length,
|
|
242
349
|
});
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
350
|
+
|
|
351
|
+
const pool = getPackageLoadPool();
|
|
352
|
+
let outcome;
|
|
353
|
+
try {
|
|
354
|
+
outcome = await pool.loadPackage({
|
|
355
|
+
packagePath: this.packagePath,
|
|
356
|
+
packageName: this.packageName,
|
|
357
|
+
malloyConfig: this.malloyConfig,
|
|
358
|
+
defaultConnectionName: "duckdb",
|
|
359
|
+
buildManifest,
|
|
360
|
+
});
|
|
361
|
+
} catch (err) {
|
|
362
|
+
const realError =
|
|
363
|
+
err instanceof Error
|
|
364
|
+
? err
|
|
365
|
+
: new Error(`Package-load worker pool failure: ${String(err)}`);
|
|
366
|
+
if (
|
|
367
|
+
realError instanceof MalloyError ||
|
|
368
|
+
realError instanceof ModelCompilationError
|
|
369
|
+
) {
|
|
370
|
+
throw realError;
|
|
371
|
+
}
|
|
372
|
+
throw new ServiceUnavailableError(
|
|
373
|
+
`Package-load worker pool unavailable: ${realError.message}`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
254
377
|
const nextModels = new Map<string, Model>();
|
|
255
|
-
for (const
|
|
256
|
-
|
|
378
|
+
for (const sm of outcome.models) {
|
|
379
|
+
if (sm.compilationError) {
|
|
380
|
+
const err = Model.deserializeCompilationError(sm.compilationError);
|
|
381
|
+
logger.warn("Model compilation failed during reload", {
|
|
382
|
+
packageName: this.packageName,
|
|
383
|
+
modelPath: sm.modelPath,
|
|
384
|
+
error: err.message,
|
|
385
|
+
});
|
|
386
|
+
nextModels.set(
|
|
387
|
+
sm.modelPath,
|
|
388
|
+
Model.fromCompilationError(
|
|
389
|
+
this.packageName,
|
|
390
|
+
sm.modelPath,
|
|
391
|
+
sm.modelType,
|
|
392
|
+
err,
|
|
393
|
+
),
|
|
394
|
+
);
|
|
395
|
+
} else {
|
|
396
|
+
nextModels.set(
|
|
397
|
+
sm.modelPath,
|
|
398
|
+
Model.fromSerialized(
|
|
399
|
+
this.packageName,
|
|
400
|
+
this.packagePath,
|
|
401
|
+
this.malloyConfig,
|
|
402
|
+
sm,
|
|
403
|
+
),
|
|
404
|
+
);
|
|
405
|
+
}
|
|
257
406
|
}
|
|
258
407
|
this.models = nextModels;
|
|
259
408
|
}
|
|
@@ -316,20 +465,6 @@ export class Package {
|
|
|
316
465
|
);
|
|
317
466
|
}
|
|
318
467
|
|
|
319
|
-
private static async loadModels(
|
|
320
|
-
packageName: string,
|
|
321
|
-
packagePath: string,
|
|
322
|
-
malloyConfig: MalloyConfig,
|
|
323
|
-
): Promise<Map<string, Model>> {
|
|
324
|
-
const modelPaths = await Package.getModelPaths(packagePath);
|
|
325
|
-
const models = await Promise.all(
|
|
326
|
-
modelPaths.map((modelPath) =>
|
|
327
|
-
Model.create(packageName, packagePath, modelPath, malloyConfig),
|
|
328
|
-
),
|
|
329
|
-
);
|
|
330
|
-
return new Map(models.map((model) => [model.getPath(), model]));
|
|
331
|
-
}
|
|
332
|
-
|
|
333
468
|
private static buildPackageMalloyConfig(
|
|
334
469
|
packagePath: string,
|
|
335
470
|
getEnvironmentMalloyConfig: () => MalloyConfig,
|
|
@@ -379,31 +514,13 @@ export class Package {
|
|
|
379
514
|
return malloyConfig;
|
|
380
515
|
}
|
|
381
516
|
|
|
382
|
-
private static async getModelPaths(packagePath: string): Promise<string[]> {
|
|
383
|
-
let files = undefined;
|
|
384
|
-
try {
|
|
385
|
-
files = await recursive(packagePath, [ignoreDotfiles]);
|
|
386
|
-
} catch (error) {
|
|
387
|
-
logger.error(error);
|
|
388
|
-
throw new PackageNotFoundError(
|
|
389
|
-
`Package config for ${packagePath} does not exist.`,
|
|
390
|
-
);
|
|
391
|
-
}
|
|
392
|
-
return files
|
|
393
|
-
.map((fullPath: string) => {
|
|
394
|
-
return path.relative(packagePath, fullPath).replace(/\\/g, "/");
|
|
395
|
-
})
|
|
396
|
-
.filter(
|
|
397
|
-
(modelPath: string) =>
|
|
398
|
-
modelPath.endsWith(MODEL_FILE_SUFFIX) ||
|
|
399
|
-
modelPath.endsWith(NOTEBOOK_FILE_SUFFIX),
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
517
|
private static async validatePackageManifestExistsOrThrowError(
|
|
404
518
|
packagePath: string,
|
|
405
519
|
) {
|
|
406
|
-
const packageConfigPath =
|
|
520
|
+
const packageConfigPath = safeJoinUnderRoot(
|
|
521
|
+
packagePath,
|
|
522
|
+
PACKAGE_MANIFEST_NAME,
|
|
523
|
+
);
|
|
407
524
|
try {
|
|
408
525
|
await fs.stat(packageConfigPath);
|
|
409
526
|
} catch {
|
|
@@ -414,37 +531,32 @@ export class Package {
|
|
|
414
531
|
}
|
|
415
532
|
}
|
|
416
533
|
|
|
417
|
-
private static async readPackageConfig(
|
|
418
|
-
packagePath: string,
|
|
419
|
-
): Promise<ApiPackage> {
|
|
420
|
-
const packageConfigPath = path.join(packagePath, PACKAGE_MANIFEST_NAME);
|
|
421
|
-
const packageConfigContents = await fs.readFile(packageConfigPath);
|
|
422
|
-
// TODO: Validate package manifest. Define manifest type in public API.
|
|
423
|
-
const packageManifest = JSON.parse(packageConfigContents.toString());
|
|
424
|
-
return {
|
|
425
|
-
name: packageManifest.name,
|
|
426
|
-
description: packageManifest.description,
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
|
|
430
534
|
private static async readDatabases(
|
|
431
535
|
packagePath: string,
|
|
536
|
+
malloyConfig: MalloyConfig,
|
|
432
537
|
): Promise<ApiDatabase[]> {
|
|
538
|
+
const databasePaths = await Package.getDatabasePaths(packagePath);
|
|
539
|
+
if (databasePaths.length === 0) {
|
|
540
|
+
return [];
|
|
541
|
+
}
|
|
542
|
+
// Resolve the package's duckdb connection ONCE and reuse it for
|
|
543
|
+
// every schema/row-count probe in this package. Malloy caches the
|
|
544
|
+
// materialized connection on the MalloyConfig so the same instance
|
|
545
|
+
// will be returned to model compiles later in `Package.create`.
|
|
546
|
+
// This is the substantive optimization over the previous code:
|
|
547
|
+
// we go from `databasePaths.length` separate DuckDBConnections
|
|
548
|
+
// (each doing its own native init + extension load) to one.
|
|
549
|
+
const conn = await malloyConfig.connections.lookupConnection("duckdb");
|
|
433
550
|
return await Promise.all(
|
|
434
|
-
(
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
info: databaseInfo,
|
|
444
|
-
type: "embedded",
|
|
445
|
-
};
|
|
446
|
-
},
|
|
447
|
-
),
|
|
551
|
+
databasePaths.map(async (databasePath) => ({
|
|
552
|
+
path: databasePath,
|
|
553
|
+
info: await Package.getDatabaseInfo(
|
|
554
|
+
packagePath,
|
|
555
|
+
databasePath,
|
|
556
|
+
conn,
|
|
557
|
+
),
|
|
558
|
+
type: "embedded" as const,
|
|
559
|
+
})),
|
|
448
560
|
);
|
|
449
561
|
}
|
|
450
562
|
|
|
@@ -465,15 +577,20 @@ export class Package {
|
|
|
465
577
|
private static async getDatabaseInfo(
|
|
466
578
|
packagePath: string,
|
|
467
579
|
databasePath: string,
|
|
580
|
+
conn: Connection,
|
|
468
581
|
): Promise<ApiTableDescription> {
|
|
469
582
|
const fullPath = path.join(packagePath, databasePath);
|
|
470
583
|
|
|
471
584
|
// Create a DuckDB source then:
|
|
472
585
|
// 1. Load the model and get the table schema from model
|
|
473
586
|
// 2. Run a query to get the row count from the table
|
|
587
|
+
// ConnectionRuntime is cheap (just a wrapper), and creating one
|
|
588
|
+
// per call keeps each probe's compile state isolated. The
|
|
589
|
+
// expensive piece — the underlying DuckDBConnection — is shared
|
|
590
|
+
// across all probes via `conn` (resolved once in readDatabases).
|
|
474
591
|
const runtime = new ConnectionRuntime({
|
|
475
592
|
urlReader: new EmptyURLReader(),
|
|
476
|
-
connections: [
|
|
593
|
+
connections: [conn],
|
|
477
594
|
});
|
|
478
595
|
// Normalize path to use forward slashes for cross-platform compatibility
|
|
479
596
|
// DuckDB on Windows supports forward slashes, and this avoids escaping issues
|