@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.
- package/build.ts +1 -0
- package/dist/app/api-doc.yaml +558 -1
- package/dist/app/assets/{HomePage-H1OH-VW5.js → HomePage-DbZS0N7G.js} +1 -1
- package/dist/app/assets/MainPage-CBuWkbmr.js +2 -0
- package/dist/app/assets/{ModelPage-Crau5hgZ.js → ModelPage-Bt37smot.js} +1 -1
- package/dist/app/assets/{PackagePage-CbubRhgE.js → PackagePage-DLZe50WG.js} +1 -1
- package/dist/app/assets/{ProjectPage-DUlJkYJ4.js → ProjectPage-FQTEPXP4.js} +1 -1
- package/dist/app/assets/{RouteError-DrNXNihc.js → RouteError-DefbDO7F.js} +1 -1
- package/dist/app/assets/{WorkbookPage-CBBv7n5U.js → WorkbookPage-CkAo16ar.js} +1 -1
- package/dist/app/assets/{core-Dzx75uJR.es-DwnFZnyO.js → core-BrfQApxh.es-DnvCX4oH.js} +14 -14
- package/dist/app/assets/index-5eLCcNmP.css +1 -0
- package/dist/app/assets/{index-d5rvmoZ7.js → index-Bu0ub036.js} +119 -119
- package/dist/app/assets/index-CkzK3JIl.js +40 -0
- package/dist/app/assets/index-CoA6HIGS.js +1742 -0
- package/dist/app/assets/{index.umd-CetYIBQY.js → index.umd-B6Ms2PpL.js} +46 -46
- package/dist/app/index.html +2 -2
- package/dist/server.mjs +1529 -985
- package/package.json +11 -10
- package/src/config.ts +7 -2
- package/src/controller/connection.controller.ts +102 -27
- package/src/dto/connection.dto.spec.ts +55 -0
- package/src/dto/connection.dto.ts +87 -2
- package/src/server.ts +201 -2
- package/src/service/connection.spec.ts +250 -4
- package/src/service/connection.ts +328 -473
- package/src/service/connection_config.spec.ts +123 -0
- package/src/service/connection_config.ts +562 -0
- package/src/service/connection_service.spec.ts +50 -0
- package/src/service/connection_service.ts +125 -32
- package/src/service/db_utils.spec.ts +161 -0
- package/src/service/db_utils.ts +131 -0
- package/src/service/materialization_service.spec.ts +18 -12
- package/src/service/materialization_service.ts +54 -7
- package/src/service/model.ts +24 -27
- package/src/service/package.spec.ts +125 -1
- package/src/service/package.ts +86 -44
- package/src/service/project.ts +172 -94
- package/src/service/project_store.spec.ts +72 -0
- package/src/service/project_store.ts +98 -81
- package/tests/unit/duckdb/attached_databases.test.ts +1 -19
- package/dist/app/assets/MainPage-GL06aMke.js +0 -2
- package/dist/app/assets/index-CMlGQMcl.css +0 -1
- package/dist/app/assets/index-CzjyS9cx.js +0 -1276
- 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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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 {
|
|
784
|
+
return {
|
|
785
|
+
graphs: allGraphs,
|
|
786
|
+
sources: allSources,
|
|
787
|
+
connectionDigests,
|
|
788
|
+
connections,
|
|
789
|
+
};
|
|
743
790
|
}
|
|
744
791
|
|
|
745
792
|
/**
|
package/src/service/model.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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
|
-
|
|
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
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
|
733
|
-
|
|
734
|
-
|
|
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", () => {
|
package/src/service/package.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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 {
|