@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.
Files changed (75) hide show
  1. package/build.ts +30 -1
  2. package/dist/app/api-doc.yaml +127 -111
  3. package/dist/app/assets/{EnvironmentPage-C7rtH4mC.js → EnvironmentPage-CgKNjySu.js} +1 -1
  4. package/dist/app/assets/HomePage-BPIpMBjW.js +1 -0
  5. package/dist/app/assets/{MainPage-D38LtZDV.js → MainPage-CAwb8U82.js} +2 -2
  6. package/dist/app/assets/{ModelPage-DOol8Mz7.js → ModelPage-C0Uevsw9.js} +1 -1
  7. package/dist/app/assets/{PackagePage-0tgzA_kO.js → PackagePage-Cu-u9k1g.js} +1 -1
  8. package/dist/app/assets/{RouteError-BaMsOSly.js → RouteError-DVwPh2Ql.js} +1 -1
  9. package/dist/app/assets/{WorkbookPage-Cx4SePkx.js → WorkbookPage-DW38R2Zv.js} +1 -1
  10. package/dist/app/assets/{core-CbsC6R_Y.es-Cwf6asf3.js → core-C0vCMRDQ.es-D_ytHhjS.js} +10 -10
  11. package/dist/app/assets/{index-DL6BZTuw.js → index-BGdcKsFF.js} +1 -1
  12. package/dist/app/assets/{index-DNofXMxi.js → index-CTx4v4_3.js} +1 -1
  13. package/dist/app/assets/index-DE6d5jEy.js +452 -0
  14. package/dist/app/assets/{index.umd-B68wGGkM.js → index.umd-C1Mi1uRm.js} +1 -1
  15. package/dist/app/index.html +1 -1
  16. package/dist/instrumentation.mjs +57 -36
  17. package/dist/package_load_worker.mjs +12213 -0
  18. package/dist/server.mjs +4198 -3648
  19. package/package.json +2 -3
  20. package/src/config.spec.ts +246 -0
  21. package/src/config.ts +121 -1
  22. package/src/constants.ts +84 -1
  23. package/src/controller/compile.controller.ts +3 -1
  24. package/src/controller/connection.controller.spec.ts +803 -0
  25. package/src/controller/connection.controller.ts +207 -20
  26. package/src/controller/model.controller.ts +19 -1
  27. package/src/controller/query.controller.ts +22 -6
  28. package/src/controller/watch-mode.controller.ts +11 -2
  29. package/src/errors.spec.ts +44 -0
  30. package/src/errors.ts +34 -0
  31. package/src/health.spec.ts +90 -0
  32. package/src/health.ts +88 -45
  33. package/src/heap_check.spec.ts +144 -0
  34. package/src/heap_check.ts +144 -0
  35. package/src/instrumentation.ts +50 -0
  36. package/src/mcp/handler_utils.ts +14 -0
  37. package/src/mcp/tools/execute_query_tool.ts +52 -10
  38. package/src/oom_guards.integration.spec.ts +261 -0
  39. package/src/package_load/package_load_pool.spec.ts +252 -0
  40. package/src/package_load/package_load_pool.ts +920 -0
  41. package/src/package_load/package_load_worker.ts +980 -0
  42. package/src/package_load/protocol.ts +336 -0
  43. package/src/path_safety.ts +9 -3
  44. package/src/query_cap_metrics.spec.ts +89 -0
  45. package/src/query_cap_metrics.ts +115 -0
  46. package/src/query_concurrency.spec.ts +247 -0
  47. package/src/query_concurrency.ts +236 -0
  48. package/src/query_param_utils.ts +18 -0
  49. package/src/query_timeout.spec.ts +224 -0
  50. package/src/query_timeout.ts +178 -0
  51. package/src/server-old.ts +21 -1
  52. package/src/server.ts +61 -57
  53. package/src/service/connection.ts +8 -2
  54. package/src/service/db_utils.spec.ts +1 -1
  55. package/src/service/environment.ts +85 -4
  56. package/src/service/environment_admission.spec.ts +165 -1
  57. package/src/service/environment_store.spec.ts +103 -0
  58. package/src/service/environment_store.ts +98 -26
  59. package/src/service/filter_integration.spec.ts +110 -0
  60. package/src/service/given.ts +80 -0
  61. package/src/service/givens_integration.spec.ts +192 -0
  62. package/src/service/model.spec.ts +298 -3
  63. package/src/service/model.ts +362 -23
  64. package/src/service/model_limits.spec.ts +181 -0
  65. package/src/service/model_limits.ts +110 -0
  66. package/src/service/package.spec.ts +12 -6
  67. package/src/service/package.ts +263 -146
  68. package/src/service/package_worker_path.spec.ts +196 -0
  69. package/src/service/path_injection.spec.ts +39 -0
  70. package/src/stream_helpers.spec.ts +280 -0
  71. package/src/stream_helpers.ts +162 -0
  72. package/src/test_helpers/metrics_harness.ts +126 -0
  73. package/tests/integration/concurrent_package/concurrent_package.integration.spec.ts +280 -0
  74. package/dist/app/assets/HomePage-DwkH7OrS.js +0 -1
  75. package/dist/app/assets/index-U38AyjJL.js +0 -451
@@ -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 { PackageNotFoundError } from "../errors";
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
- const packageConfig = await Package.readPackageConfig(packagePath);
98
- const packageConfigTime = performance.now();
99
- logger.info("Package config read completed", {
100
- packageName,
101
- duration: formatDuration(
102
- packageConfigTime - manifestValidationTime,
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
- const models = await Package.loadModels(
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: "error",
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
- const reloaded = await Promise.all(
244
- modelPaths.map((modelPath) =>
245
- Model.create(
246
- this.packageName,
247
- this.packagePath,
248
- modelPath,
249
- this.malloyConfig,
250
- { buildManifest },
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 model of reloaded) {
256
- nextModels.set(model.getPath(), model);
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 = path.join(packagePath, PACKAGE_MANIFEST_NAME);
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
- (await Package.getDatabasePaths(packagePath)).map(
435
- async (databasePath) => {
436
- const databaseInfo = await Package.getDatabaseInfo(
437
- packagePath,
438
- databasePath,
439
- );
440
-
441
- return {
442
- path: databasePath,
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: [new DuckDBConnection("duckdb")],
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