@malloy-publisher/server 0.0.192 → 0.0.193
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/dist/app/api-doc.yaml +522 -1
- package/dist/app/assets/{HomePage-H1OH-VW5.js → HomePage-Di9MU3lS.js} +1 -1
- package/dist/app/assets/{MainPage-GL06aMke.js → MainPage-yZQo2HSL.js} +1 -1
- package/dist/app/assets/{ModelPage-Crau5hgZ.js → ModelPage-Dx2mHWeT.js} +1 -1
- package/dist/app/assets/{PackagePage-CbubRhgE.js → PackagePage-Q386Py9t.js} +1 -1
- package/dist/app/assets/{ProjectPage-DUlJkYJ4.js → ProjectPage-WR7wPQB-.js} +1 -1
- package/dist/app/assets/{RouteError-DrNXNihc.js → RouteError-stRGU4aW.js} +1 -1
- package/dist/app/assets/{WorkbookPage-CBBv7n5U.js → WorkbookPage-D3iX0djH.js} +1 -1
- package/dist/app/assets/{core-Dzx75uJR.es-DwnFZnyO.js → core-QH4HZQVz.es-CqlQLZdl.js} +1 -1
- package/dist/app/assets/{index-d5rvmoZ7.js → index-CVHzPJwN.js} +119 -119
- package/dist/app/assets/{index-CzjyS9cx.js → index-DavAceYD.js} +50 -50
- package/dist/app/assets/{index-HHdhLUpv.js → index-Y3Y-VRna.js} +1 -1
- package/dist/app/assets/{index.umd-CetYIBQY.js → index.umd-Bp8OIhfV.js} +46 -46
- package/dist/app/index.html +1 -1
- package/dist/server.mjs +1389 -984
- package/package.json +10 -10
- package/src/controller/connection.controller.ts +102 -27
- package/src/dto/connection.dto.spec.ts +4 -0
- package/src/dto/connection.dto.ts +46 -2
- package/src/server.ts +201 -2
- package/src/service/connection.spec.ts +250 -4
- package/src/service/connection.ts +326 -473
- package/src/service/connection_config.ts +514 -0
- package/src/service/connection_service.spec.ts +50 -0
- package/src/service/connection_service.ts +125 -32
- 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
|
@@ -1,10 +1,66 @@
|
|
|
1
1
|
import { components } from "../api";
|
|
2
2
|
import { ConnectionNotFoundError, FrozenConfigError } from "../errors";
|
|
3
3
|
import { logger } from "../logger";
|
|
4
|
-
import {
|
|
4
|
+
import { buildProjectMalloyConfig } from "./connection";
|
|
5
5
|
import { ProjectStore } from "./project_store";
|
|
6
6
|
|
|
7
7
|
type ApiConnection = components["schemas"]["Connection"];
|
|
8
|
+
type ReleaseCallback = () => Promise<void>;
|
|
9
|
+
type ConnectionUpdateProject = {
|
|
10
|
+
runConnectionUpdateExclusive?: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
11
|
+
updateConnections?: (
|
|
12
|
+
nextMalloyConfig: ReturnType<typeof buildProjectMalloyConfig>,
|
|
13
|
+
apiConnections?: ApiConnection[],
|
|
14
|
+
afterPreviousRelease?: ReleaseCallback,
|
|
15
|
+
) => void;
|
|
16
|
+
deleteConnection?: (connectionName: string) => Promise<void>;
|
|
17
|
+
deleteDuckDBConnection?: (connectionName: string) => Promise<void>;
|
|
18
|
+
deleteDuckLakeConnection?: (connectionName: string) => Promise<void>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
async function runProjectConnectionUpdate<T>(
|
|
22
|
+
project: ConnectionUpdateProject,
|
|
23
|
+
fn: () => Promise<T>,
|
|
24
|
+
): Promise<T> {
|
|
25
|
+
if (project.runConnectionUpdateExclusive) {
|
|
26
|
+
return project.runConnectionUpdateExclusive(fn);
|
|
27
|
+
}
|
|
28
|
+
return fn();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function updateProjectConnections(
|
|
32
|
+
project: ConnectionUpdateProject,
|
|
33
|
+
nextMalloyConfig: ReturnType<typeof buildProjectMalloyConfig>,
|
|
34
|
+
afterPreviousRelease?: ReleaseCallback,
|
|
35
|
+
): void {
|
|
36
|
+
project.updateConnections?.(
|
|
37
|
+
nextMalloyConfig,
|
|
38
|
+
nextMalloyConfig.apiConnections,
|
|
39
|
+
afterPreviousRelease,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function buildDeletedConnectionCleanup(
|
|
44
|
+
project: ConnectionUpdateProject,
|
|
45
|
+
deletedConnection: ApiConnection,
|
|
46
|
+
connectionName: string,
|
|
47
|
+
): ReleaseCallback | undefined {
|
|
48
|
+
if (
|
|
49
|
+
deletedConnection.type === "duckdb" &&
|
|
50
|
+
typeof project.deleteDuckDBConnection === "function"
|
|
51
|
+
) {
|
|
52
|
+
return () => project.deleteDuckDBConnection!(connectionName);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (
|
|
56
|
+
deletedConnection.type === "ducklake" &&
|
|
57
|
+
typeof project.deleteDuckLakeConnection === "function"
|
|
58
|
+
) {
|
|
59
|
+
return () => project.deleteDuckLakeConnection!(connectionName);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
8
64
|
|
|
9
65
|
export class ConnectionService {
|
|
10
66
|
private projectStore: ProjectStore;
|
|
@@ -74,21 +130,21 @@ export class ConnectionService {
|
|
|
74
130
|
|
|
75
131
|
// Update in-memory connections
|
|
76
132
|
const project = await this.projectStore.getProject(projectName, false);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
await createProjectConnections(
|
|
133
|
+
await runProjectConnectionUpdate(project, async () => {
|
|
134
|
+
const existingConnections = project.listApiConnections();
|
|
135
|
+
const nextMalloyConfig = buildProjectMalloyConfig(
|
|
81
136
|
[...existingConnections, connection],
|
|
82
137
|
project.metadata.location || "",
|
|
83
138
|
);
|
|
84
139
|
|
|
85
|
-
|
|
140
|
+
await this.projectStore.addConnection(
|
|
141
|
+
connection,
|
|
142
|
+
dbProject.id,
|
|
143
|
+
repository,
|
|
144
|
+
);
|
|
86
145
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
dbProject.id,
|
|
90
|
-
repository,
|
|
91
|
-
);
|
|
146
|
+
updateProjectConnections(project, nextMalloyConfig);
|
|
147
|
+
});
|
|
92
148
|
|
|
93
149
|
logger.info(
|
|
94
150
|
`Successfully added connection "${connection.name}" to project "${projectName}"`,
|
|
@@ -117,31 +173,36 @@ export class ConnectionService {
|
|
|
117
173
|
|
|
118
174
|
// Update in-memory connections
|
|
119
175
|
const project = await this.projectStore.getProject(projectName, false);
|
|
120
|
-
|
|
176
|
+
await runProjectConnectionUpdate(project, async () => {
|
|
177
|
+
const existingConnections = project.listApiConnections();
|
|
121
178
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
179
|
+
const updatedConnection = {
|
|
180
|
+
...dbConnection.config,
|
|
181
|
+
...connection,
|
|
182
|
+
name: connectionName,
|
|
183
|
+
};
|
|
127
184
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
185
|
+
const updatedConnections = existingConnections.map((conn) =>
|
|
186
|
+
conn.name === connectionName ? updatedConnection : conn,
|
|
187
|
+
);
|
|
131
188
|
|
|
132
|
-
|
|
133
|
-
|
|
189
|
+
// Pass isUpdateConnectionRequest=true so the DuckLake wrapper
|
|
190
|
+
// re-attaches against the updated catalog/storage settings instead
|
|
191
|
+
// of trusting the prior generation's persisted attach state.
|
|
192
|
+
const nextMalloyConfig = buildProjectMalloyConfig(
|
|
134
193
|
updatedConnections,
|
|
135
194
|
project.metadata.location || "",
|
|
195
|
+
true,
|
|
136
196
|
);
|
|
137
197
|
|
|
138
|
-
|
|
198
|
+
await this.projectStore.updateConnection(
|
|
199
|
+
updatedConnection,
|
|
200
|
+
dbProject.id,
|
|
201
|
+
repository,
|
|
202
|
+
);
|
|
139
203
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
dbProject.id,
|
|
143
|
-
repository,
|
|
144
|
-
);
|
|
204
|
+
updateProjectConnections(project, nextMalloyConfig);
|
|
205
|
+
});
|
|
145
206
|
|
|
146
207
|
logger.info(
|
|
147
208
|
`Successfully updated connection "${connectionName}" in project "${projectName}"`,
|
|
@@ -169,10 +230,42 @@ export class ConnectionService {
|
|
|
169
230
|
|
|
170
231
|
// Update in-memory connections
|
|
171
232
|
const project = await this.projectStore.getProject(projectName, false);
|
|
172
|
-
await project
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
233
|
+
await runProjectConnectionUpdate(project, async () => {
|
|
234
|
+
if (typeof project.listApiConnections !== "function") {
|
|
235
|
+
if (typeof project.deleteConnection === "function") {
|
|
236
|
+
await project.deleteConnection(connectionName);
|
|
237
|
+
}
|
|
238
|
+
await repository.deleteConnection(dbConnection.id);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const deletedConnection =
|
|
243
|
+
"getApiConnection" in project &&
|
|
244
|
+
typeof project.getApiConnection === "function"
|
|
245
|
+
? project.getApiConnection(connectionName)
|
|
246
|
+
: dbConnection.config;
|
|
247
|
+
const updatedConnections = project
|
|
248
|
+
.listApiConnections()
|
|
249
|
+
.filter((connection) => connection.name !== connectionName);
|
|
250
|
+
const nextMalloyConfig = buildProjectMalloyConfig(
|
|
251
|
+
updatedConnections,
|
|
252
|
+
project.metadata.location || "",
|
|
253
|
+
);
|
|
254
|
+
const deleteConnectionFilesAfterRelease =
|
|
255
|
+
buildDeletedConnectionCleanup(
|
|
256
|
+
project,
|
|
257
|
+
deletedConnection,
|
|
258
|
+
connectionName,
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
await repository.deleteConnection(dbConnection.id);
|
|
262
|
+
|
|
263
|
+
updateProjectConnections(
|
|
264
|
+
project,
|
|
265
|
+
nextMalloyConfig,
|
|
266
|
+
deleteConnectionFilesAfterRelease,
|
|
267
|
+
);
|
|
268
|
+
});
|
|
176
269
|
|
|
177
270
|
logger.info(
|
|
178
271
|
`Successfully deleted connection "${connectionName}" from project "${projectName}"`,
|
|
@@ -480,10 +480,12 @@ describe("MaterializationService", () => {
|
|
|
480
480
|
dialectName: "duckdb",
|
|
481
481
|
runSQL,
|
|
482
482
|
} as unknown as Connection;
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
483
|
+
const pkg = {
|
|
484
|
+
getMalloyConnection: async (name: string): Promise<Connection> => {
|
|
485
|
+
if (name === "conn") return connection;
|
|
486
|
+
throw new Error(`unknown connection: ${name}`);
|
|
487
|
+
},
|
|
488
|
+
};
|
|
487
489
|
(ctx.projectStore.getProject as sinon.SinonStub).resolves({
|
|
488
490
|
getPackage: sinon.stub().resolves(pkg),
|
|
489
491
|
});
|
|
@@ -535,10 +537,12 @@ describe("MaterializationService", () => {
|
|
|
535
537
|
// a vanished "ghost_conn", which used to be impossible to tear down.
|
|
536
538
|
// `teardownPackage` must force-delete the row anyway so teardown
|
|
537
539
|
// can complete.
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
540
|
+
const pkg = {
|
|
541
|
+
getMalloyConnection: async (name: string): Promise<Connection> => {
|
|
542
|
+
if (name === "live_conn") return livingConn;
|
|
543
|
+
throw new Error(`unknown connection: ${name}`);
|
|
544
|
+
},
|
|
545
|
+
};
|
|
542
546
|
(ctx.projectStore.getProject as sinon.SinonStub).resolves({
|
|
543
547
|
getPackage: sinon.stub().resolves(pkg),
|
|
544
548
|
});
|
|
@@ -576,10 +580,12 @@ describe("MaterializationService", () => {
|
|
|
576
580
|
dialectName: "duckdb",
|
|
577
581
|
runSQL,
|
|
578
582
|
} as unknown as Connection;
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
+
const pkg = {
|
|
584
|
+
getMalloyConnection: async (name: string): Promise<Connection> => {
|
|
585
|
+
if (name === "conn") return connection;
|
|
586
|
+
throw new Error(`unknown connection: ${name}`);
|
|
587
|
+
},
|
|
588
|
+
};
|
|
583
589
|
(ctx.projectStore.getProject as sinon.SinonStub).resolves({
|
|
584
590
|
getPackage: sinon.stub().resolves(pkg),
|
|
585
591
|
});
|
|
@@ -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", () => {
|