@malloy-publisher/server 0.0.192 → 0.0.194

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 (44) hide show
  1. package/build.ts +1 -0
  2. package/dist/app/api-doc.yaml +558 -1
  3. package/dist/app/assets/{HomePage-H1OH-VW5.js → HomePage-DbZS0N7G.js} +1 -1
  4. package/dist/app/assets/MainPage-CBuWkbmr.js +2 -0
  5. package/dist/app/assets/{ModelPage-Crau5hgZ.js → ModelPage-Bt37smot.js} +1 -1
  6. package/dist/app/assets/{PackagePage-CbubRhgE.js → PackagePage-DLZe50WG.js} +1 -1
  7. package/dist/app/assets/{ProjectPage-DUlJkYJ4.js → ProjectPage-FQTEPXP4.js} +1 -1
  8. package/dist/app/assets/{RouteError-DrNXNihc.js → RouteError-DefbDO7F.js} +1 -1
  9. package/dist/app/assets/{WorkbookPage-CBBv7n5U.js → WorkbookPage-CkAo16ar.js} +1 -1
  10. package/dist/app/assets/{core-Dzx75uJR.es-DwnFZnyO.js → core-BrfQApxh.es-DnvCX4oH.js} +14 -14
  11. package/dist/app/assets/index-5eLCcNmP.css +1 -0
  12. package/dist/app/assets/{index-d5rvmoZ7.js → index-Bu0ub036.js} +119 -119
  13. package/dist/app/assets/index-CkzK3JIl.js +40 -0
  14. package/dist/app/assets/index-CoA6HIGS.js +1742 -0
  15. package/dist/app/assets/{index.umd-CetYIBQY.js → index.umd-B6Ms2PpL.js} +46 -46
  16. package/dist/app/index.html +2 -2
  17. package/dist/server.mjs +1529 -985
  18. package/package.json +11 -10
  19. package/src/config.ts +7 -2
  20. package/src/controller/connection.controller.ts +102 -27
  21. package/src/dto/connection.dto.spec.ts +55 -0
  22. package/src/dto/connection.dto.ts +87 -2
  23. package/src/server.ts +201 -2
  24. package/src/service/connection.spec.ts +250 -4
  25. package/src/service/connection.ts +328 -473
  26. package/src/service/connection_config.spec.ts +123 -0
  27. package/src/service/connection_config.ts +562 -0
  28. package/src/service/connection_service.spec.ts +50 -0
  29. package/src/service/connection_service.ts +125 -32
  30. package/src/service/db_utils.spec.ts +161 -0
  31. package/src/service/db_utils.ts +131 -0
  32. package/src/service/materialization_service.spec.ts +18 -12
  33. package/src/service/materialization_service.ts +54 -7
  34. package/src/service/model.ts +24 -27
  35. package/src/service/package.spec.ts +125 -1
  36. package/src/service/package.ts +86 -44
  37. package/src/service/project.ts +172 -94
  38. package/src/service/project_store.spec.ts +72 -0
  39. package/src/service/project_store.ts +98 -81
  40. package/tests/unit/duckdb/attached_databases.test.ts +1 -19
  41. package/dist/app/assets/MainPage-GL06aMke.js +0 -2
  42. package/dist/app/assets/index-CMlGQMcl.css +0 -1
  43. package/dist/app/assets/index-CzjyS9cx.js +0 -1276
  44. package/dist/app/assets/index-HHdhLUpv.js +0 -676
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  BuildGraph,
3
3
  Connection as MalloyConnection,
4
+ MalloyConfig,
4
5
  PersistSource,
5
6
  } from "@malloydata/malloy";
6
7
  import { Manifest } from "@malloydata/malloy";
@@ -46,6 +47,34 @@ export function stagingSuffix(buildId: string): string {
46
47
  return `_${buildId.substring(0, STAGING_BUILD_ID_LEN)}`;
47
48
  }
48
49
 
50
+ /**
51
+ * Resolve a Map<name, Connection> for just the names a materialization
52
+ * step is about to touch. The package's MalloyConfig caches each lookup,
53
+ * so subsequent calls with overlapping names are cheap. A failed lookup
54
+ * is logged and the name is omitted from the result — downstream code
55
+ * (`forceDeleteRowOnMissingConnection` in teardown, or the explicit
56
+ * "connection X not found" check in build) handles a missing entry.
57
+ */
58
+ async function resolvePackageConnections(
59
+ pkg: { getMalloyConnection(name: string): Promise<MalloyConnection> },
60
+ names: Iterable<string>,
61
+ ): Promise<Map<string, MalloyConnection>> {
62
+ const map = new Map<string, MalloyConnection>();
63
+ const seen = new Set<string>();
64
+ for (const name of names) {
65
+ if (!name || seen.has(name)) continue;
66
+ seen.add(name);
67
+ try {
68
+ map.set(name, await pkg.getMalloyConnection(name));
69
+ } catch (err) {
70
+ logger.warn(`Failed to resolve connection ${name}`, {
71
+ error: err instanceof Error ? err.message : String(err),
72
+ });
73
+ }
74
+ }
75
+ return map;
76
+ }
77
+
49
78
  /**
50
79
  * Build a stable key for a `(connectionName, tableName)` pair.
51
80
  * Used to check whether a persist target was created by a previous build.
@@ -494,13 +523,17 @@ export class MaterializationService {
494
523
 
495
524
  const project = await this.projectStore.getProject(projectName, false);
496
525
  const pkg = await project.getPackage(packageName, false);
497
- const connections = pkg.getConnections();
498
526
 
499
527
  const entries = await this.manifestService.listEntries(
500
528
  projectId,
501
529
  packageName,
502
530
  );
503
531
 
532
+ const connections = await resolvePackageConnections(
533
+ pkg,
534
+ entries.map((e) => e.connectionName),
535
+ );
536
+
504
537
  // `forceDeleteRowOnMissingConnection`: teardown is the one place
505
538
  // where we'd rather lose the manifest row than leave it pointing at
506
539
  // a vanished connection. We also deliberately omit `liveTables`:
@@ -560,7 +593,9 @@ export class MaterializationService {
560
593
  );
561
594
 
562
595
  // ── STEP 2: COMPILE & PLAN ─────────────────────────────────────
563
- const { graphs, sources, connectionDigests } =
596
+ // `connections` is built lazily from the connection names the plan
597
+ // actually targets — no upfront ATTACH on every project connection.
598
+ const { graphs, sources, connectionDigests, connections } =
564
599
  await this.compilePackageBuildPlan(pkg, signal);
565
600
 
566
601
  if (graphs.length === 0) {
@@ -569,7 +604,6 @@ export class MaterializationService {
569
604
  }
570
605
 
571
606
  // ── STEP 3: BUILD ──────────────────────────────────────────────
572
- const connections = pkg.getConnections();
573
607
  let sourcesBuilt = 0;
574
608
  let sourcesSkipped = 0;
575
609
  const sourceResults: Record<string, unknown>[] = [];
@@ -646,13 +680,15 @@ export class MaterializationService {
646
680
  pkg: {
647
681
  getModelPaths(): string[];
648
682
  getPackagePath(): string;
649
- getConnections(): Map<string, MalloyConnection>;
683
+ getMalloyConfig(): MalloyConfig;
684
+ getMalloyConnection(name: string): Promise<MalloyConnection>;
650
685
  },
651
686
  signal: AbortSignal,
652
687
  ): Promise<{
653
688
  graphs: BuildGraph[];
654
689
  sources: Record<string, PersistSource>;
655
690
  connectionDigests: Record<string, string>;
691
+ connections: Map<string, MalloyConnection>;
656
692
  }> {
657
693
  const modelPaths = pkg.getModelPaths();
658
694
  const allGraphs: BuildGraph[] = [];
@@ -665,7 +701,7 @@ export class MaterializationService {
665
701
  await Model.getModelRuntime(
666
702
  pkg.getPackagePath(),
667
703
  modelPath,
668
- pkg.getConnections(),
704
+ pkg.getMalloyConfig(),
669
705
  );
670
706
 
671
707
  const modelMaterializer = runtime.loadModel(modelURL, {
@@ -730,7 +766,13 @@ export class MaterializationService {
730
766
  tableOwners.set(key, sourceID);
731
767
  }
732
768
 
733
- const connections = pkg.getConnections();
769
+ // Resolve only the connections this build plan actually targets;
770
+ // the package's MalloyConfig caches each lookup so the build phase
771
+ // sees the same Connection instance and avoids re-resolving.
772
+ const connections = await resolvePackageConnections(
773
+ pkg,
774
+ allGraphs.map((g) => g.connectionName),
775
+ );
734
776
  const connectionDigests: Record<string, string> = {};
735
777
  for (const graph of allGraphs) {
736
778
  const conn = connections.get(graph.connectionName);
@@ -739,7 +781,12 @@ export class MaterializationService {
739
781
  }
740
782
  }
741
783
 
742
- return { graphs: allGraphs, sources: allSources, connectionDigests };
784
+ return {
785
+ graphs: allGraphs,
786
+ sources: allSources,
787
+ connectionDigests,
788
+ connections,
789
+ };
743
790
  }
744
791
 
745
792
  /**
@@ -1,10 +1,10 @@
1
- import { DuckDBConnection } from "@malloydata/db-duckdb";
2
1
  import {
3
2
  Annotation,
4
3
  API,
5
4
  Connection,
6
5
  FixedConnectionMap,
7
6
  isSourceDef,
7
+ MalloyConfig,
8
8
  MalloyError,
9
9
  ModelDef,
10
10
  modelDefToModelInfo,
@@ -27,7 +27,6 @@ import { DataStyles } from "@malloydata/render";
27
27
  import { metrics } from "@opentelemetry/api";
28
28
  import * as fs from "fs/promises";
29
29
  import * as path from "path";
30
- import { fileURLToPath } from "url";
31
30
  import { components } from "../api";
32
31
  import {
33
32
  MODEL_FILE_SUFFIX,
@@ -72,6 +71,7 @@ const MALLOY_VERSION = (
72
71
  ).version;
73
72
 
74
73
  export type ModelType = "model" | "notebook";
74
+ type ModelConnectionInput = MalloyConfig | Map<string, Connection>;
75
75
 
76
76
  interface RunnableNotebookCell {
77
77
  type: "code" | "markdown";
@@ -162,7 +162,7 @@ export class Model {
162
162
  packageName: string,
163
163
  packagePath: string,
164
164
  modelPath: string,
165
- connections: Map<string, Connection>,
165
+ malloyConfig: ModelConnectionInput,
166
166
  options?: { buildManifest?: BuildManifest["entries"] },
167
167
  ): Promise<Model> {
168
168
  // getModelRuntime might throw a ModelNotFoundError. It's the callers responsibility
@@ -171,7 +171,7 @@ export class Model {
171
171
  await Model.getModelRuntime(
172
172
  packagePath,
173
173
  modelPath,
174
- connections,
174
+ malloyConfig,
175
175
  options,
176
176
  );
177
177
 
@@ -672,7 +672,7 @@ export class Model {
672
672
  static async getModelRuntime(
673
673
  packagePath: string,
674
674
  modelPath: string,
675
- connections: Map<string, Connection>,
675
+ malloyConfig: ModelConnectionInput,
676
676
  options?: { buildManifest?: BuildManifest["entries"] },
677
677
  ): Promise<{
678
678
  runtime: Runtime;
@@ -703,35 +703,32 @@ export class Model {
703
703
 
704
704
  const modelURL = new URL(`file://${fullModelPath}`);
705
705
  const baseUrl = new URL(".", modelURL);
706
- const fileUrl = new URL(baseUrl.pathname, "file:");
707
- const workingDirectory = fileURLToPath(fileUrl);
708
706
  const importBaseURL = new URL(baseUrl.pathname + "/", "file:");
709
707
  const urlReader = new HackyDataStylesAccumulator(URL_READER);
710
708
 
711
- const duckdbConnection = connections.get("duckdb") as DuckDBConnection;
712
- await duckdbConnection.runSQL(
713
- `SET FILE_SEARCH_PATH='${workingDirectory}';`,
714
- );
715
-
716
- const runtimeOptions: {
717
- urlReader: typeof urlReader;
718
- connections: FixedConnectionMap;
719
- buildManifest?: BuildManifest;
720
- } = {
709
+ // Request runtimes borrow the cached package MalloyConfig. The package
710
+ // owns release; callers must not release this runtime per request.
711
+ const runtime = new Runtime({
721
712
  urlReader,
722
- connections: new FixedConnectionMap(connections, "duckdb"),
723
- };
713
+ config: Model.toMalloyConfig(malloyConfig),
714
+ buildManifest: options?.buildManifest
715
+ ? { entries: options.buildManifest, strict: false }
716
+ : undefined,
717
+ });
718
+ const dataStyles = urlReader.getHackyAccumulatedDataStyles();
719
+ return { runtime, modelURL, importBaseURL, dataStyles, modelType };
720
+ }
724
721
 
725
- if (options?.buildManifest) {
726
- runtimeOptions.buildManifest = {
727
- entries: options.buildManifest,
728
- strict: false,
729
- };
722
+ private static toMalloyConfig(input: ModelConnectionInput): MalloyConfig {
723
+ if (input instanceof MalloyConfig) {
724
+ return input;
730
725
  }
731
726
 
732
- const runtime = new Runtime(runtimeOptions);
733
- const dataStyles = urlReader.getHackyAccumulatedDataStyles();
734
- return { runtime, modelURL, importBaseURL, dataStyles, modelType };
727
+ const malloyConfig = new MalloyConfig({ connections: {} });
728
+ malloyConfig.wrapConnections(
729
+ () => new FixedConnectionMap(input, "duckdb"),
730
+ );
731
+ return malloyConfig;
735
732
  }
736
733
 
737
734
  private static getQueries(
@@ -1,7 +1,7 @@
1
1
  import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
2
  import { Stats } from "fs";
3
3
  import fs from "fs/promises";
4
- import { join } from "path";
4
+ import { join, resolve } from "path";
5
5
  import sinon from "sinon";
6
6
  import { PackageNotFoundError } from "../errors";
7
7
  import { Model } from "./model";
@@ -155,6 +155,130 @@ describe("service/package", () => {
155
155
  },
156
156
  { timeout: 20000 },
157
157
  );
158
+
159
+ it(
160
+ "uses package-root-relative DuckDB file paths for nested models",
161
+ async () => {
162
+ await fs.mkdir(join(testPackageDirectory, "data"), {
163
+ recursive: true,
164
+ });
165
+ await fs.mkdir(join(testPackageDirectory, "models"), {
166
+ recursive: true,
167
+ });
168
+ await fs.writeFile(
169
+ join(testPackageDirectory, "data", "root.csv"),
170
+ "name,value\nalpha,1\nbeta,2\n",
171
+ );
172
+ await fs.writeFile(
173
+ join(testPackageDirectory, "models", "root_read.malloy"),
174
+ [
175
+ "source: rows is duckdb.table('data/root.csv')",
176
+ "query: row_count is rows -> { aggregate: c is count() }",
177
+ ].join("\n"),
178
+ );
179
+
180
+ const absolutePackageDirectory = resolve(testPackageDirectory);
181
+ const packageInstance = await Package.create(
182
+ "testProject",
183
+ "testPackage",
184
+ absolutePackageDirectory,
185
+ new Map(),
186
+ );
187
+ try {
188
+ const rootModel = packageInstance.getModel(
189
+ "models/root_read.malloy",
190
+ );
191
+ expect(rootModel).toBeDefined();
192
+ const rootResults = await rootModel!.getQueryResults(
193
+ undefined,
194
+ "row_count",
195
+ );
196
+ expect(
197
+ (rootResults.compactResult as { c: number }[])[0]?.c,
198
+ ).toBe(2);
199
+ } finally {
200
+ await packageInstance.getMalloyConfig().releaseConnections();
201
+ }
202
+ },
203
+ { timeout: 20000 },
204
+ );
205
+
206
+ it(
207
+ "does not resolve DuckDB file paths relative to the model directory",
208
+ async () => {
209
+ await fs.mkdir(join(testPackageDirectory, "models"), {
210
+ recursive: true,
211
+ });
212
+ await fs.writeFile(
213
+ join(testPackageDirectory, "models", "sibling.csv"),
214
+ "name,value\nnested,3\n",
215
+ );
216
+ await fs.writeFile(
217
+ join(testPackageDirectory, "models", "sibling_read.malloy"),
218
+ [
219
+ "source: rows is duckdb.table('./sibling.csv')",
220
+ "query: row_count is rows -> { aggregate: c is count() }",
221
+ ].join("\n"),
222
+ );
223
+
224
+ await expect(
225
+ Package.create(
226
+ "testProject",
227
+ "testPackage",
228
+ resolve(testPackageDirectory),
229
+ new Map(),
230
+ ),
231
+ ).rejects.toThrow();
232
+ },
233
+ { timeout: 20000 },
234
+ );
235
+
236
+ it(
237
+ "does not treat package-root paths as filesystem isolation",
238
+ async () => {
239
+ const outsidePath = resolve(
240
+ testPackageDirectory,
241
+ "..",
242
+ "outside-package.csv",
243
+ );
244
+ await fs.mkdir(join(testPackageDirectory, "models"), {
245
+ recursive: true,
246
+ });
247
+ await fs.writeFile(outsidePath, "name,value\noutside,9\n");
248
+ await fs.writeFile(
249
+ join(testPackageDirectory, "models", "outside_read.malloy"),
250
+ [
251
+ "source: rows is duckdb.table('../outside-package.csv')",
252
+ "query: row_count is rows -> { aggregate: c is count() }",
253
+ ].join("\n"),
254
+ );
255
+
256
+ let packageInstance: Package | undefined;
257
+ try {
258
+ packageInstance = await Package.create(
259
+ "testProject",
260
+ "testPackage",
261
+ resolve(testPackageDirectory),
262
+ new Map(),
263
+ );
264
+ const outsideModel = packageInstance.getModel(
265
+ "models/outside_read.malloy",
266
+ );
267
+ expect(outsideModel).toBeDefined();
268
+ const outsideResults = await outsideModel!.getQueryResults(
269
+ undefined,
270
+ "row_count",
271
+ );
272
+ expect(
273
+ (outsideResults.compactResult as { c: number }[])[0]?.c,
274
+ ).toBe(1);
275
+ } finally {
276
+ await packageInstance?.getMalloyConfig().releaseConnections();
277
+ await fs.rm(outsidePath, { force: true });
278
+ }
279
+ },
280
+ { timeout: 20000 },
281
+ );
158
282
  });
159
283
 
160
284
  describe("listModels", () => {
@@ -2,10 +2,14 @@ import * as fs from "fs/promises";
2
2
  import * as path from "path";
3
3
 
4
4
  import { DuckDBConnection } from "@malloydata/db-duckdb";
5
+ import "@malloydata/db-duckdb/native";
5
6
  import {
6
7
  Connection,
7
8
  ConnectionRuntime,
8
9
  EmptyURLReader,
10
+ FixedConnectionMap,
11
+ contextOverlay,
12
+ MalloyConfig,
9
13
  SourceDef,
10
14
  } from "@malloydata/malloy";
11
15
  import { metrics } from "@opentelemetry/api";
@@ -28,6 +32,14 @@ type ApiNotebook = components["schemas"]["Notebook"];
28
32
  export type ApiPackage = components["schemas"]["Package"];
29
33
  type ApiColumn = components["schemas"]["Column"];
30
34
  type ApiTableDescription = components["schemas"]["TableDescription"];
35
+ // A thunk lets callers pass a live reference to the *current* project
36
+ // MalloyConfig so the package wrapper resolves project connections against the
37
+ // generation that's active at lookup time, not the one that was current when
38
+ // the package was first loaded.
39
+ type PackageConnectionInput =
40
+ | MalloyConfig
41
+ | Map<string, Connection>
42
+ | (() => MalloyConfig);
31
43
 
32
44
  const ENABLE_LIST_MODEL_COMPILATION = true;
33
45
  export class Package {
@@ -37,7 +49,7 @@ export class Package {
37
49
  private databases: ApiDatabase[];
38
50
  private models: Map<string, Model> = new Map();
39
51
  private packagePath: string;
40
- private connections: Map<string, Connection> = new Map();
52
+ private malloyConfig: MalloyConfig;
41
53
  private static meter = metrics.getMeter("publisher");
42
54
  private static packageLoadHistogram = this.meter.createHistogram(
43
55
  "malloy_package_load_duration",
@@ -54,7 +66,7 @@ export class Package {
54
66
  packageMetadata: ApiPackage,
55
67
  databases: ApiDatabase[],
56
68
  models: Map<string, Model>,
57
- connections: Map<string, Connection> = new Map(),
69
+ malloyConfig: MalloyConfig = new MalloyConfig({ connections: {} }),
58
70
  ) {
59
71
  this.projectName = projectName;
60
72
  this.packageName = packageName;
@@ -62,14 +74,14 @@ export class Package {
62
74
  this.packageMetadata = packageMetadata;
63
75
  this.databases = databases;
64
76
  this.models = models;
65
- this.connections = connections;
77
+ this.malloyConfig = malloyConfig;
66
78
  }
67
79
 
68
80
  static async create(
69
81
  projectName: string,
70
82
  packageName: string,
71
83
  packagePath: string,
72
- projectConnections: Map<string, Connection>,
84
+ projectMalloyConfig: PackageConnectionInput,
73
85
  ): Promise<Package> {
74
86
  const startTime = performance.now();
75
87
  await Package.validatePackageManifestExistsOrThrowError(packagePath);
@@ -97,20 +109,17 @@ export class Package {
97
109
  databaseCount: databases.length,
98
110
  duration: formatDuration(databasesTime - packageConfigTime),
99
111
  });
100
- const connections = new Map<string, Connection>(projectConnections);
101
-
102
- // Add a duckdb connection for the package.
103
- const duckdbConnection = new DuckDBConnection(
104
- "duckdb",
105
- ":memory:",
112
+ const malloyConfig = Package.buildPackageMalloyConfig(
106
113
  packagePath,
114
+ typeof projectMalloyConfig === "function"
115
+ ? projectMalloyConfig
116
+ : () => Package.toMalloyConfig(projectMalloyConfig),
107
117
  );
108
- connections.set("duckdb", duckdbConnection);
109
118
 
110
119
  const models = await Package.loadModels(
111
120
  packageName,
112
121
  packagePath,
113
- connections,
122
+ malloyConfig,
114
123
  );
115
124
  const modelsTime = performance.now();
116
125
  logger.info("Models loaded", {
@@ -159,7 +168,7 @@ export class Package {
159
168
  packageConfig,
160
169
  databases,
161
170
  models,
162
- connections,
171
+ malloyConfig,
163
172
  );
164
173
  } catch (error) {
165
174
  logger.error(`Error loading package ${packageName}`, { error });
@@ -190,10 +199,6 @@ export class Package {
190
199
  return this.packageName;
191
200
  }
192
201
 
193
- public getPackagePath(): string {
194
- return this.packagePath;
195
- }
196
-
197
202
  public getPackageMetadata(): ApiPackage {
198
203
  return this.packageMetadata;
199
204
  }
@@ -206,19 +211,24 @@ export class Package {
206
211
  return this.models.get(modelPath);
207
212
  }
208
213
 
214
+ public async getMalloyConnection(
215
+ connectionName: string,
216
+ ): Promise<Connection> {
217
+ return this.malloyConfig.connections.lookupConnection(connectionName);
218
+ }
219
+
220
+ public getMalloyConfig(): MalloyConfig {
221
+ return this.malloyConfig;
222
+ }
223
+
224
+ public getPackagePath(): string {
225
+ return this.packagePath;
226
+ }
227
+
209
228
  public getModelPaths(): string[] {
210
229
  return Array.from(this.models.keys());
211
230
  }
212
231
 
213
- /**
214
- * Recompile every model in the package with the given build manifest
215
- * so queries resolve persist references to materialized tables.
216
- *
217
- * Builds a fresh map off to the side and swaps it in at the end. If any
218
- * recompile fails the whole call rejects before the swap and the live
219
- * `this.models` reference remains untouched — no half-loaded state is
220
- * ever observable to concurrent readers.
221
- */
222
232
  public async reloadAllModels(
223
233
  buildManifest: BuildManifest["entries"],
224
234
  ): Promise<void> {
@@ -228,14 +238,13 @@ export class Package {
228
238
  modelCount: modelPaths.length,
229
239
  manifestEntryCount: Object.keys(buildManifest).length,
230
240
  });
231
-
232
241
  const reloaded = await Promise.all(
233
242
  modelPaths.map((modelPath) =>
234
243
  Model.create(
235
244
  this.packageName,
236
245
  this.packagePath,
237
246
  modelPath,
238
- this.connections,
247
+ this.malloyConfig,
239
248
  { buildManifest },
240
249
  ),
241
250
  ),
@@ -247,20 +256,6 @@ export class Package {
247
256
  this.models = nextModels;
248
257
  }
249
258
 
250
- public getConnections(): Map<string, Connection> {
251
- return this.connections;
252
- }
253
-
254
- public getMalloyConnection(connectionName: string): Connection {
255
- const connection = this.connections.get(connectionName);
256
- if (!connection) {
257
- throw new Error(
258
- `Connection ${connectionName} not found in package ${this.packageName}`,
259
- );
260
- }
261
- return connection;
262
- }
263
-
264
259
  public async getModelFileText(modelPath: string): Promise<string> {
265
260
  const model = this.getModel(modelPath);
266
261
  if (!model) {
@@ -322,17 +317,64 @@ export class Package {
322
317
  private static async loadModels(
323
318
  packageName: string,
324
319
  packagePath: string,
325
- connections: Map<string, Connection>,
320
+ malloyConfig: MalloyConfig,
326
321
  ): Promise<Map<string, Model>> {
327
322
  const modelPaths = await Package.getModelPaths(packagePath);
328
323
  const models = await Promise.all(
329
324
  modelPaths.map((modelPath) =>
330
- Model.create(packageName, packagePath, modelPath, connections),
325
+ Model.create(packageName, packagePath, modelPath, malloyConfig),
331
326
  ),
332
327
  );
333
328
  return new Map(models.map((model) => [model.getPath(), model]));
334
329
  }
335
330
 
331
+ private static buildPackageMalloyConfig(
332
+ packagePath: string,
333
+ getProjectMalloyConfig: () => MalloyConfig,
334
+ ): MalloyConfig {
335
+ const malloyConfig = new MalloyConfig(
336
+ {
337
+ connections: {
338
+ duckdb: {
339
+ is: "duckdb",
340
+ databasePath: ":memory:",
341
+ },
342
+ },
343
+ },
344
+ {
345
+ config: contextOverlay({ rootDirectory: packagePath }),
346
+ },
347
+ );
348
+
349
+ malloyConfig.wrapConnections((base) => ({
350
+ lookupConnection: async (name?: string) => {
351
+ if (!name || name === "duckdb") {
352
+ return base.lookupConnection(name);
353
+ }
354
+ // Resolve against the *current* project MalloyConfig so a
355
+ // connection-generation swap on Project propagates without a
356
+ // package reload.
357
+ return getProjectMalloyConfig().connections.lookupConnection(name);
358
+ },
359
+ }));
360
+
361
+ return malloyConfig;
362
+ }
363
+
364
+ private static toMalloyConfig(
365
+ input: MalloyConfig | Map<string, Connection>,
366
+ ): MalloyConfig {
367
+ if (input instanceof MalloyConfig) {
368
+ return input;
369
+ }
370
+
371
+ const malloyConfig = new MalloyConfig({ connections: {} });
372
+ malloyConfig.wrapConnections(
373
+ () => new FixedConnectionMap(input, "duckdb"),
374
+ );
375
+ return malloyConfig;
376
+ }
377
+
336
378
  private static async getModelPaths(packagePath: string): Promise<string[]> {
337
379
  let files = undefined;
338
380
  try {