@malloy-publisher/server 0.0.188 → 0.0.382-dev
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 +423 -60
- package/dist/app/assets/{HomePage-DsuUvSI_.js → HomePage-Dn3E4CuB.js} +1 -1
- package/dist/app/assets/{MainPage-DHWFkEN6.js → MainPage-BzB3yoqi.js} +1 -1
- package/dist/app/assets/{ModelPage-DNwcx1nE.js → ModelPage-C9O_sAXT.js} +1 -1
- package/dist/app/assets/{PackagePage-DSgz9G2V.js → PackagePage-DcxKEjBX.js} +1 -1
- package/dist/app/assets/{ProjectPage-CSdPosLV.js → ProjectPage-BDj307rF.js} +1 -1
- package/dist/app/assets/{RouteError-orw1RX8q.js → RouteError-DAShbVCG.js} +1 -1
- package/dist/app/assets/{WorkbookPage-Bp-BpGjL.js → WorkbookPage-Cs_XYEaB.js} +1 -1
- package/dist/app/assets/{core-B4ZYB7aS.es-8Zh0TkSr.js → core-CjeTkq8O.es-BqRc6yhC.js} +1 -1
- package/dist/app/assets/{index-BL2TJgTw.js → index-15BOvhp0.js} +4 -4
- package/dist/app/assets/{index-BWJkzsfl.js → index-Bb2jqquW.js} +1 -1
- package/dist/app/assets/{index-BefdHHMa.js → index-D68X76-7.js} +1 -1
- package/dist/app/assets/{index.umd-lY-87l4L.js → index.umd-DGBekgSu.js} +1 -1
- package/dist/app/index.html +1 -1
- package/dist/instrumentation.js +98 -77
- package/dist/server.js +1834 -450
- package/package.json +5 -3
- package/src/controller/connection.controller.ts +27 -20
- package/src/controller/manifest.controller.ts +29 -0
- package/src/controller/materialization.controller.ts +125 -0
- package/src/controller/model.controller.ts +0 -2
- package/src/controller/package.controller.ts +53 -2
- package/src/errors.ts +24 -0
- package/src/server.ts +196 -5
- package/src/service/manifest_service.spec.ts +201 -0
- package/src/service/manifest_service.ts +106 -0
- package/src/service/materialization_service.spec.ts +648 -0
- package/src/service/materialization_service.ts +929 -0
- package/src/service/materialized_table_gc.spec.ts +383 -0
- package/src/service/materialized_table_gc.ts +279 -0
- package/src/service/model.ts +25 -4
- package/src/service/package.ts +50 -0
- package/src/service/project_store.ts +21 -2
- package/src/service/quoting.ts +41 -0
- package/src/service/resolve_project.ts +13 -0
- package/src/storage/DatabaseInterface.ts +103 -1
- package/src/storage/{StorageManager.spec.ts → StorageManager.mock.ts} +9 -0
- package/src/storage/StorageManager.ts +119 -1
- package/src/storage/duckdb/DuckDBManifestStore.ts +70 -0
- package/src/storage/duckdb/DuckDBRepository.ts +99 -9
- package/src/storage/duckdb/ManifestRepository.ts +119 -0
- package/src/storage/duckdb/MaterializationRepository.ts +249 -0
- package/src/storage/duckdb/manifest_store.spec.ts +133 -0
- package/src/storage/duckdb/schema.ts +59 -1
- package/src/storage/ducklake/DuckLakeManifestStore.ts +146 -0
- package/tests/fixtures/persist-test/data/orders.csv +5 -0
- package/tests/fixtures/persist-test/persist_test.malloy +11 -0
- package/tests/fixtures/persist-test/publisher.json +5 -0
- package/tests/fixtures/publisher.config.json +15 -0
- package/tests/harness/rest_e2e.ts +68 -0
- package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +470 -0
- package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +2 -2
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import * as sinon from "sinon";
|
|
3
|
+
import {
|
|
4
|
+
BuildManifest,
|
|
5
|
+
ManifestEntry,
|
|
6
|
+
ManifestStore,
|
|
7
|
+
ResourceRepository,
|
|
8
|
+
} from "../storage/DatabaseInterface";
|
|
9
|
+
import { ManifestService } from "./manifest_service";
|
|
10
|
+
import { ProjectStore } from "./project_store";
|
|
11
|
+
|
|
12
|
+
function makeEntry(overrides: Partial<ManifestEntry> = {}): ManifestEntry {
|
|
13
|
+
return {
|
|
14
|
+
id: "entry-1",
|
|
15
|
+
projectId: "proj-1",
|
|
16
|
+
packageName: "pkg",
|
|
17
|
+
buildId: "build-abc",
|
|
18
|
+
tableName: "my_table",
|
|
19
|
+
sourceName: "my_source",
|
|
20
|
+
connectionName: "duckdb",
|
|
21
|
+
createdAt: new Date("2025-01-01"),
|
|
22
|
+
updatedAt: new Date("2025-01-01"),
|
|
23
|
+
...overrides,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createMocks() {
|
|
28
|
+
const sandbox = sinon.createSandbox();
|
|
29
|
+
|
|
30
|
+
const manifestStore: sinon.SinonStubbedInstance<ManifestStore> = {
|
|
31
|
+
getManifest: sandbox.stub(),
|
|
32
|
+
writeEntry: sandbox.stub(),
|
|
33
|
+
deleteEntry: sandbox.stub(),
|
|
34
|
+
listEntries: sandbox.stub(),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const repository = {
|
|
38
|
+
listManifestEntries: sandbox.stub(),
|
|
39
|
+
} as unknown as sinon.SinonStubbedInstance<ResourceRepository>;
|
|
40
|
+
|
|
41
|
+
const reloadAllModels = sandbox.stub().resolves();
|
|
42
|
+
|
|
43
|
+
const pkg = {
|
|
44
|
+
reloadAllModels,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const project = {
|
|
48
|
+
getPackage: sandbox.stub().resolves(pkg),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const projectStore = {
|
|
52
|
+
storageManager: {
|
|
53
|
+
getManifestStore: (_projectId?: string) => manifestStore,
|
|
54
|
+
getRepository: () => repository,
|
|
55
|
+
},
|
|
56
|
+
getProject: sandbox.stub().resolves(project),
|
|
57
|
+
} as unknown as ProjectStore;
|
|
58
|
+
|
|
59
|
+
const service = new ManifestService(projectStore);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
sandbox,
|
|
63
|
+
manifestStore,
|
|
64
|
+
repository,
|
|
65
|
+
projectStore,
|
|
66
|
+
project,
|
|
67
|
+
pkg,
|
|
68
|
+
reloadAllModels,
|
|
69
|
+
service,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe("ManifestService", () => {
|
|
74
|
+
let ctx: ReturnType<typeof createMocks>;
|
|
75
|
+
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
ctx = createMocks();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("getManifest", () => {
|
|
81
|
+
it("should delegate to the manifest store", async () => {
|
|
82
|
+
const manifest: BuildManifest = {
|
|
83
|
+
entries: { "build-abc": { tableName: "tbl" } },
|
|
84
|
+
strict: false,
|
|
85
|
+
};
|
|
86
|
+
ctx.manifestStore.getManifest.resolves(manifest);
|
|
87
|
+
|
|
88
|
+
const result = await ctx.service.getManifest("proj-1", "pkg");
|
|
89
|
+
|
|
90
|
+
expect(result).toEqual(manifest);
|
|
91
|
+
expect(ctx.manifestStore.getManifest.calledWith("proj-1", "pkg")).toBe(
|
|
92
|
+
true,
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("writeEntry", () => {
|
|
98
|
+
it("should delegate to the manifest store", async () => {
|
|
99
|
+
ctx.manifestStore.writeEntry.resolves();
|
|
100
|
+
|
|
101
|
+
await ctx.service.writeEntry(
|
|
102
|
+
"proj-1",
|
|
103
|
+
"pkg",
|
|
104
|
+
"build-abc",
|
|
105
|
+
"tbl",
|
|
106
|
+
"src",
|
|
107
|
+
"duckdb",
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
expect(ctx.manifestStore.writeEntry.calledOnce).toBe(true);
|
|
111
|
+
const args = ctx.manifestStore.writeEntry.firstCall.args;
|
|
112
|
+
expect(args[0]).toBe("proj-1");
|
|
113
|
+
expect(args[1]).toBe("pkg");
|
|
114
|
+
expect(args[2]).toBe("build-abc");
|
|
115
|
+
expect(args[3]).toBe("tbl");
|
|
116
|
+
expect(args[4]).toBe("src");
|
|
117
|
+
expect(args[5]).toBe("duckdb");
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("deleteEntry", () => {
|
|
122
|
+
it("should delegate to the manifest store", async () => {
|
|
123
|
+
ctx.manifestStore.deleteEntry.resolves();
|
|
124
|
+
|
|
125
|
+
await ctx.service.deleteEntry("proj-1", "entry-1");
|
|
126
|
+
|
|
127
|
+
expect(ctx.manifestStore.deleteEntry.calledOnce).toBe(true);
|
|
128
|
+
expect(ctx.manifestStore.deleteEntry.firstCall.args[0]).toBe(
|
|
129
|
+
"entry-1",
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("reloadManifest", () => {
|
|
135
|
+
it("should get manifest, reload it onto the package, and return it", async () => {
|
|
136
|
+
const manifest: BuildManifest = {
|
|
137
|
+
entries: { "build-abc": { tableName: "tbl" } },
|
|
138
|
+
strict: false,
|
|
139
|
+
};
|
|
140
|
+
ctx.manifestStore.getManifest.resolves(manifest);
|
|
141
|
+
|
|
142
|
+
const result = await ctx.service.reloadManifest(
|
|
143
|
+
"proj-1",
|
|
144
|
+
"pkg",
|
|
145
|
+
"my-project",
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
expect(result).toEqual(manifest);
|
|
149
|
+
expect(
|
|
150
|
+
(ctx.projectStore.getProject as sinon.SinonStub).calledWith(
|
|
151
|
+
"my-project",
|
|
152
|
+
false,
|
|
153
|
+
),
|
|
154
|
+
).toBe(true);
|
|
155
|
+
expect(ctx.project.getPackage.calledWith("pkg", false)).toBe(true);
|
|
156
|
+
expect(ctx.reloadAllModels.calledWith(manifest.entries)).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should return an empty manifest when no entries exist", async () => {
|
|
160
|
+
const emptyManifest: BuildManifest = {
|
|
161
|
+
entries: {},
|
|
162
|
+
strict: false,
|
|
163
|
+
};
|
|
164
|
+
ctx.manifestStore.getManifest.resolves(emptyManifest);
|
|
165
|
+
|
|
166
|
+
const result = await ctx.service.reloadManifest(
|
|
167
|
+
"proj-1",
|
|
168
|
+
"pkg",
|
|
169
|
+
"my-project",
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
expect(result.entries).toEqual({});
|
|
173
|
+
expect(ctx.reloadAllModels.calledWith({})).toBe(true);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("listEntries", () => {
|
|
178
|
+
it("should return entries from the manifest store", async () => {
|
|
179
|
+
const entries = [
|
|
180
|
+
makeEntry(),
|
|
181
|
+
makeEntry({ id: "entry-2", buildId: "build-def" }),
|
|
182
|
+
];
|
|
183
|
+
ctx.manifestStore.listEntries.resolves(entries);
|
|
184
|
+
|
|
185
|
+
const result = await ctx.service.listEntries("proj-1", "pkg");
|
|
186
|
+
|
|
187
|
+
expect(result).toEqual(entries);
|
|
188
|
+
expect(ctx.manifestStore.listEntries.calledWith("proj-1", "pkg")).toBe(
|
|
189
|
+
true,
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("should return empty array when no entries exist", async () => {
|
|
194
|
+
ctx.manifestStore.listEntries.resolves([]);
|
|
195
|
+
|
|
196
|
+
const result = await ctx.service.listEntries("proj-1", "pkg");
|
|
197
|
+
|
|
198
|
+
expect(result).toEqual([]);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { logger } from "../logger";
|
|
2
|
+
import {
|
|
3
|
+
BuildManifest,
|
|
4
|
+
ManifestEntry,
|
|
5
|
+
ManifestStore,
|
|
6
|
+
} from "../storage/DatabaseInterface";
|
|
7
|
+
import { ProjectStore } from "./project_store";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Manages build manifests that map source names to materialized table names.
|
|
11
|
+
*
|
|
12
|
+
* A build manifest is produced during materialization: each entry records which
|
|
13
|
+
* DuckDB table backs a given Malloy source.
|
|
14
|
+
*
|
|
15
|
+
* Two-phase lifecycle:
|
|
16
|
+
* 1. **Persist** – `writeEntry` stores individual entries to the DB as they
|
|
17
|
+
* are produced during a build.
|
|
18
|
+
* 2. **Reload** – `reloadManifest` reads the manifest from the DB and
|
|
19
|
+
* recompiles all models in the package so the Malloy Runtime resolves
|
|
20
|
+
* persist references to the materialized tables.
|
|
21
|
+
*
|
|
22
|
+
* All manifest operations delegate to the active {@link ManifestStore}, which
|
|
23
|
+
* is either the local DuckDB store (standalone) or DuckLake (orchestrated).
|
|
24
|
+
*/
|
|
25
|
+
export class ManifestService {
|
|
26
|
+
constructor(private projectStore: ProjectStore) {}
|
|
27
|
+
|
|
28
|
+
private manifestStoreFor(projectId: string): ManifestStore {
|
|
29
|
+
return this.projectStore.storageManager.getManifestStore(projectId);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async getManifest(
|
|
33
|
+
projectId: string,
|
|
34
|
+
packageName: string,
|
|
35
|
+
): Promise<BuildManifest> {
|
|
36
|
+
return this.manifestStoreFor(projectId).getManifest(
|
|
37
|
+
projectId,
|
|
38
|
+
packageName,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async writeEntry(
|
|
43
|
+
projectId: string,
|
|
44
|
+
packageName: string,
|
|
45
|
+
buildId: string,
|
|
46
|
+
tableName: string,
|
|
47
|
+
sourceName: string,
|
|
48
|
+
connectionName: string,
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
await this.manifestStoreFor(projectId).writeEntry(
|
|
51
|
+
projectId,
|
|
52
|
+
packageName,
|
|
53
|
+
buildId,
|
|
54
|
+
tableName,
|
|
55
|
+
sourceName,
|
|
56
|
+
connectionName,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async deleteEntry(projectId: string, entryId: string): Promise<void> {
|
|
61
|
+
await this.manifestStoreFor(projectId).deleteEntry(entryId);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Read the manifest from storage and reload it onto this worker's
|
|
66
|
+
* package so subsequent queries resolve persist references to
|
|
67
|
+
* materialized tables. This is what `POST .../manifest?action=reload` calls
|
|
68
|
+
* (used by orchestrated workers that need to pick up manifest state
|
|
69
|
+
* produced elsewhere). Despite the name, nothing is loaded *into*
|
|
70
|
+
* storage — the worker pulls the manifest down and recompiles.
|
|
71
|
+
*/
|
|
72
|
+
async reloadManifest(
|
|
73
|
+
projectId: string,
|
|
74
|
+
packageName: string,
|
|
75
|
+
projectName: string,
|
|
76
|
+
): Promise<BuildManifest> {
|
|
77
|
+
const manifest = await this.getManifest(projectId, packageName);
|
|
78
|
+
|
|
79
|
+
const project = await this.projectStore.getProject(projectName, false);
|
|
80
|
+
const pkg = await project.getPackage(packageName, false);
|
|
81
|
+
await pkg.reloadAllModels(manifest.entries);
|
|
82
|
+
|
|
83
|
+
logger.info("Reloaded manifest and recompiled models", {
|
|
84
|
+
projectId,
|
|
85
|
+
packageName,
|
|
86
|
+
entryCount: Object.keys(manifest.entries).length,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return manifest;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get the manifest entries from the active store for inspection.
|
|
94
|
+
* Routes through ManifestStore so DuckLake mode reads the shared
|
|
95
|
+
* catalog instead of the local DuckDB table.
|
|
96
|
+
*/
|
|
97
|
+
async listEntries(
|
|
98
|
+
projectId: string,
|
|
99
|
+
packageName: string,
|
|
100
|
+
): Promise<ManifestEntry[]> {
|
|
101
|
+
return this.manifestStoreFor(projectId).listEntries(
|
|
102
|
+
projectId,
|
|
103
|
+
packageName,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|