@malloy-publisher/server 0.0.195 → 0.0.196
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 +213 -214
- package/dist/app/assets/EnvironmentPage-1j6QDWAy.js +1 -0
- package/dist/app/assets/HomePage-DMop21VG.js +1 -0
- package/dist/app/assets/MainPage-BbE8ETz1.js +2 -0
- package/dist/app/assets/ModelPage-D2jvfe3t.js +1 -0
- package/dist/app/assets/PackagePage-BbnhGoD3.js +1 -0
- package/dist/app/assets/{RouteError-DefbDO7F.js → RouteError-D3LGEZ3i.js} +1 -1
- package/dist/app/assets/WorkbookPage-DttVIj4u.js +1 -0
- package/dist/app/assets/{core-BrfQApxh.es-DnvCX4oH.js → core-w79IMXAG.es-Bd0UlzOL.js} +1 -1
- package/dist/app/assets/{index-Bu0ub036.js → index-5K9YjIxF.js} +117 -117
- package/dist/app/assets/{index-CkzK3JIl.js → index-C513UodQ.js} +1 -1
- package/dist/app/assets/{index-CoA6HIGS.js → index-DIgzgp69.js} +1 -1
- package/dist/app/assets/{index.umd-B6Ms2PpL.js → index.umd-BMeMPq_9.js} +1 -1
- package/dist/app/index.html +1 -1
- package/dist/server.mjs +1976 -1322
- package/package.json +2 -2
- package/publisher.config.json +2 -2
- package/src/config.spec.ts +181 -66
- package/src/config.ts +68 -47
- package/src/controller/compile.controller.ts +10 -7
- package/src/controller/connection.controller.ts +79 -58
- package/src/controller/database.controller.ts +10 -7
- package/src/controller/manifest.controller.ts +23 -14
- package/src/controller/materialization.controller.ts +14 -14
- package/src/controller/model.controller.ts +35 -20
- package/src/controller/package.controller.ts +83 -49
- package/src/controller/query.controller.ts +11 -8
- package/src/controller/watch-mode.controller.ts +35 -29
- package/src/errors.ts +2 -2
- package/src/mcp/error_messages.ts +2 -2
- package/src/mcp/handler_utils.ts +23 -20
- package/src/mcp/mcp_constants.ts +1 -1
- package/src/mcp/prompts/handlers.ts +3 -3
- package/src/mcp/prompts/prompt_service.ts +5 -5
- package/src/mcp/prompts/utils.ts +12 -12
- package/src/mcp/resource_metadata.ts +3 -3
- package/src/mcp/resources/environment_resource.ts +187 -0
- package/src/mcp/resources/model_resource.ts +19 -17
- package/src/mcp/resources/notebook_resource.ts +13 -13
- package/src/mcp/resources/package_resource.ts +30 -27
- package/src/mcp/resources/query_resource.ts +15 -10
- package/src/mcp/resources/source_resource.ts +10 -10
- package/src/mcp/resources/view_resource.ts +11 -11
- package/src/mcp/server.ts +16 -14
- package/src/mcp/tools/discovery_tools.ts +67 -49
- package/src/mcp/tools/execute_query_tool.ts +14 -14
- package/src/server-old.ts +1119 -0
- package/src/server.ts +191 -159
- package/src/service/connection.spec.ts +158 -133
- package/src/service/connection.ts +42 -39
- package/src/service/connection_config.spec.ts +13 -11
- package/src/service/connection_config.ts +28 -19
- package/src/service/connection_service.spec.ts +63 -43
- package/src/service/connection_service.ts +106 -89
- package/src/service/{project.ts → environment.ts} +92 -77
- package/src/service/{project_compile.spec.ts → environment_compile.spec.ts} +1 -1
- package/src/service/{project_store.spec.ts → environment_store.spec.ts} +99 -83
- package/src/service/{project_store.ts → environment_store.ts} +373 -327
- package/src/service/manifest_service.spec.ts +15 -15
- package/src/service/manifest_service.ts +26 -21
- package/src/service/materialization_service.spec.ts +93 -59
- package/src/service/materialization_service.ts +71 -62
- package/src/service/materialized_table_gc.spec.ts +15 -15
- package/src/service/materialized_table_gc.ts +3 -3
- package/src/service/model.ts +4 -4
- package/src/service/package.spec.ts +2 -2
- package/src/service/package.ts +23 -21
- package/src/service/resolve_environment.ts +15 -0
- package/src/storage/DatabaseInterface.ts +34 -25
- package/src/storage/StorageManager.mock.ts +3 -3
- package/src/storage/StorageManager.ts +64 -28
- package/src/storage/duckdb/ConnectionRepository.ts +13 -11
- package/src/storage/duckdb/DuckDBConnection.ts +1 -1
- package/src/storage/duckdb/DuckDBManifestStore.ts +6 -6
- package/src/storage/duckdb/DuckDBRepository.ts +47 -47
- package/src/storage/duckdb/{ProjectRepository.ts → EnvironmentRepository.ts} +35 -35
- package/src/storage/duckdb/ManifestRepository.ts +21 -20
- package/src/storage/duckdb/MaterializationRepository.ts +31 -28
- package/src/storage/duckdb/PackageRepository.ts +11 -11
- package/src/storage/duckdb/manifest_store.spec.ts +2 -2
- package/src/storage/duckdb/schema.ts +61 -20
- package/src/storage/ducklake/DuckLakeManifestStore.ts +20 -11
- package/tests/fixtures/publisher.config.json +1 -1
- package/tests/harness/e2e.ts +1 -1
- package/tests/harness/mcp_test_setup.ts +12 -24
- package/tests/harness/mocks.ts +10 -8
- package/tests/harness/rest_e2e.ts +2 -2
- package/tests/integration/legacy_routes/legacy_routes.integration.spec.ts +259 -0
- package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +4 -4
- package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +28 -49
- package/tests/integration/mcp/mcp_resource.integration.spec.ts +39 -47
- package/tests/integration/mcp/mcp_transport.integration.spec.ts +1 -1
- package/tests/unit/duckdb/attached_databases.test.ts +51 -33
- package/tests/unit/duckdb/legacy_schema_migration.test.ts +194 -0
- package/tests/unit/ducklake/ducklake.test.ts +24 -22
- package/tests/unit/mcp/prompt_happy.test.ts +8 -8
- package/dist/app/assets/HomePage-DbZS0N7G.js +0 -1
- package/dist/app/assets/MainPage-CBuWkbmr.js +0 -2
- package/dist/app/assets/ModelPage-Bt37smot.js +0 -1
- package/dist/app/assets/PackagePage-DLZe50WG.js +0 -1
- package/dist/app/assets/ProjectPage-FQTEPXP4.js +0 -1
- package/dist/app/assets/WorkbookPage-CkAo16ar.js +0 -1
- package/src/mcp/resources/project_resource.ts +0 -184
- package/src/service/resolve_project.ts +0 -13
package/src/service/package.ts
CHANGED
|
@@ -6,9 +6,9 @@ import "@malloydata/db-duckdb/native";
|
|
|
6
6
|
import {
|
|
7
7
|
Connection,
|
|
8
8
|
ConnectionRuntime,
|
|
9
|
+
contextOverlay,
|
|
9
10
|
EmptyURLReader,
|
|
10
11
|
FixedConnectionMap,
|
|
11
|
-
contextOverlay,
|
|
12
12
|
MalloyConfig,
|
|
13
13
|
SourceDef,
|
|
14
14
|
} from "@malloydata/malloy";
|
|
@@ -32,8 +32,8 @@ type ApiNotebook = components["schemas"]["Notebook"];
|
|
|
32
32
|
export type ApiPackage = components["schemas"]["Package"];
|
|
33
33
|
type ApiColumn = components["schemas"]["Column"];
|
|
34
34
|
type ApiTableDescription = components["schemas"]["TableDescription"];
|
|
35
|
-
// A thunk lets callers pass a live reference to the *current*
|
|
36
|
-
// MalloyConfig so the package wrapper resolves
|
|
35
|
+
// A thunk lets callers pass a live reference to the *current* environment
|
|
36
|
+
// MalloyConfig so the package wrapper resolves environment connections against the
|
|
37
37
|
// generation that's active at lookup time, not the one that was current when
|
|
38
38
|
// the package was first loaded.
|
|
39
39
|
type PackageConnectionInput =
|
|
@@ -43,7 +43,7 @@ type PackageConnectionInput =
|
|
|
43
43
|
|
|
44
44
|
const ENABLE_LIST_MODEL_COMPILATION = true;
|
|
45
45
|
export class Package {
|
|
46
|
-
private
|
|
46
|
+
private environmentName: string;
|
|
47
47
|
private packageName: string;
|
|
48
48
|
private packageMetadata: ApiPackage;
|
|
49
49
|
private databases: ApiDatabase[];
|
|
@@ -60,7 +60,7 @@ export class Package {
|
|
|
60
60
|
);
|
|
61
61
|
|
|
62
62
|
constructor(
|
|
63
|
-
|
|
63
|
+
environmentName: string,
|
|
64
64
|
packageName: string,
|
|
65
65
|
packagePath: string,
|
|
66
66
|
packageMetadata: ApiPackage,
|
|
@@ -68,7 +68,7 @@ export class Package {
|
|
|
68
68
|
models: Map<string, Model>,
|
|
69
69
|
malloyConfig: MalloyConfig = new MalloyConfig({ connections: {} }),
|
|
70
70
|
) {
|
|
71
|
-
this.
|
|
71
|
+
this.environmentName = environmentName;
|
|
72
72
|
this.packageName = packageName;
|
|
73
73
|
this.packagePath = packagePath;
|
|
74
74
|
this.packageMetadata = packageMetadata;
|
|
@@ -78,10 +78,10 @@ export class Package {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
static async create(
|
|
81
|
-
|
|
81
|
+
environmentName: string,
|
|
82
82
|
packageName: string,
|
|
83
83
|
packagePath: string,
|
|
84
|
-
|
|
84
|
+
environmentMalloyConfig: PackageConnectionInput,
|
|
85
85
|
): Promise<Package> {
|
|
86
86
|
const startTime = performance.now();
|
|
87
87
|
await Package.validatePackageManifestExistsOrThrowError(packagePath);
|
|
@@ -100,7 +100,7 @@ export class Package {
|
|
|
100
100
|
packageConfigTime - manifestValidationTime,
|
|
101
101
|
),
|
|
102
102
|
});
|
|
103
|
-
packageConfig.resource = `${API_PREFIX}/
|
|
103
|
+
packageConfig.resource = `${API_PREFIX}/environments/${environmentName}/packages/${packageName}`;
|
|
104
104
|
|
|
105
105
|
const databases = await Package.readDatabases(packagePath);
|
|
106
106
|
const databasesTime = performance.now();
|
|
@@ -111,9 +111,9 @@ export class Package {
|
|
|
111
111
|
});
|
|
112
112
|
const malloyConfig = Package.buildPackageMalloyConfig(
|
|
113
113
|
packagePath,
|
|
114
|
-
typeof
|
|
115
|
-
?
|
|
116
|
-
: () => Package.toMalloyConfig(
|
|
114
|
+
typeof environmentMalloyConfig === "function"
|
|
115
|
+
? environmentMalloyConfig
|
|
116
|
+
: () => Package.toMalloyConfig(environmentMalloyConfig),
|
|
117
117
|
);
|
|
118
118
|
|
|
119
119
|
const models = await Package.loadModels(
|
|
@@ -162,7 +162,7 @@ export class Package {
|
|
|
162
162
|
duration: formatDuration(executionTime),
|
|
163
163
|
});
|
|
164
164
|
return new Package(
|
|
165
|
-
|
|
165
|
+
environmentName,
|
|
166
166
|
packageName,
|
|
167
167
|
packagePath,
|
|
168
168
|
packageConfig,
|
|
@@ -283,7 +283,7 @@ export class Package {
|
|
|
283
283
|
}
|
|
284
284
|
}
|
|
285
285
|
return {
|
|
286
|
-
|
|
286
|
+
environmentName: this.environmentName,
|
|
287
287
|
path: modelPath,
|
|
288
288
|
packageName: this.packageName,
|
|
289
289
|
error,
|
|
@@ -305,7 +305,7 @@ export class Package {
|
|
|
305
305
|
error = this.models.get(modelPath)?.getNotebookError();
|
|
306
306
|
}
|
|
307
307
|
return {
|
|
308
|
-
|
|
308
|
+
environmentName: this.environmentName,
|
|
309
309
|
packageName: this.packageName,
|
|
310
310
|
path: modelPath,
|
|
311
311
|
error: error?.message,
|
|
@@ -330,7 +330,7 @@ export class Package {
|
|
|
330
330
|
|
|
331
331
|
private static buildPackageMalloyConfig(
|
|
332
332
|
packagePath: string,
|
|
333
|
-
|
|
333
|
+
getEnvironmentMalloyConfig: () => MalloyConfig,
|
|
334
334
|
): MalloyConfig {
|
|
335
335
|
const malloyConfig = new MalloyConfig(
|
|
336
336
|
{
|
|
@@ -351,10 +351,12 @@ export class Package {
|
|
|
351
351
|
if (!name || name === "duckdb") {
|
|
352
352
|
return base.lookupConnection(name);
|
|
353
353
|
}
|
|
354
|
-
// Resolve against the *current*
|
|
355
|
-
// connection-generation swap on
|
|
354
|
+
// Resolve against the *current* environment MalloyConfig so a
|
|
355
|
+
// connection-generation swap on Environment propagates without a
|
|
356
356
|
// package reload.
|
|
357
|
-
return
|
|
357
|
+
return getEnvironmentMalloyConfig().connections.lookupConnection(
|
|
358
|
+
name,
|
|
359
|
+
);
|
|
358
360
|
},
|
|
359
361
|
}));
|
|
360
362
|
|
|
@@ -495,8 +497,8 @@ export class Package {
|
|
|
495
497
|
this.packageName = name;
|
|
496
498
|
}
|
|
497
499
|
|
|
498
|
-
public
|
|
499
|
-
this.
|
|
500
|
+
public setEnvironmentName(environmentName: string) {
|
|
501
|
+
this.environmentName = environmentName;
|
|
500
502
|
}
|
|
501
503
|
|
|
502
504
|
public setPackageMetadata(packageMetadata: ApiPackage) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { EnvironmentNotFoundError } from "../errors";
|
|
2
|
+
import { ResourceRepository } from "../storage/DatabaseInterface";
|
|
3
|
+
|
|
4
|
+
export async function resolveEnvironmentId(
|
|
5
|
+
repository: ResourceRepository,
|
|
6
|
+
environmentName: string,
|
|
7
|
+
): Promise<string> {
|
|
8
|
+
const dbEnvironment = await repository.getEnvironmentByName(environmentName);
|
|
9
|
+
if (!dbEnvironment) {
|
|
10
|
+
throw new EnvironmentNotFoundError(
|
|
11
|
+
`Environment '${environmentName}' not found`,
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
return dbEnvironment.id;
|
|
15
|
+
}
|
|
@@ -9,20 +9,26 @@ export interface DatabaseConnection {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export interface ResourceRepository {
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
): Promise<
|
|
19
|
-
|
|
20
|
-
|
|
12
|
+
// Environments
|
|
13
|
+
listEnvironments(): Promise<Environment[]>;
|
|
14
|
+
getEnvironmentById(id: string): Promise<Environment | null>;
|
|
15
|
+
getEnvironmentByName(name: string): Promise<Environment | null>;
|
|
16
|
+
createEnvironment(
|
|
17
|
+
environment: Omit<Environment, "id" | "createdAt" | "updatedAt">,
|
|
18
|
+
): Promise<Environment>;
|
|
19
|
+
updateEnvironment(
|
|
20
|
+
id: string,
|
|
21
|
+
updates: Partial<Environment>,
|
|
22
|
+
): Promise<Environment>;
|
|
23
|
+
deleteEnvironment(id: string): Promise<void>;
|
|
21
24
|
|
|
22
25
|
// Packages
|
|
23
|
-
listPackages(
|
|
26
|
+
listPackages(environmentId: string): Promise<Package[]>;
|
|
24
27
|
getPackageById(id: string): Promise<Package | null>;
|
|
25
|
-
getPackageByName(
|
|
28
|
+
getPackageByName(
|
|
29
|
+
environmentId: string,
|
|
30
|
+
name: string,
|
|
31
|
+
): Promise<Package | null>;
|
|
26
32
|
createPackage(
|
|
27
33
|
pkg: Omit<Package, "id" | "createdAt" | "updatedAt">,
|
|
28
34
|
): Promise<Package>;
|
|
@@ -30,10 +36,10 @@ export interface ResourceRepository {
|
|
|
30
36
|
deletePackage(id: string): Promise<void>;
|
|
31
37
|
|
|
32
38
|
// Connections
|
|
33
|
-
listConnections(
|
|
39
|
+
listConnections(environmentId: string): Promise<Connection[]>;
|
|
34
40
|
getConnectionById(id: string): Promise<Connection | null>;
|
|
35
41
|
getConnectionByName(
|
|
36
|
-
|
|
42
|
+
environmentId: string,
|
|
37
43
|
name: string,
|
|
38
44
|
): Promise<Connection | null>;
|
|
39
45
|
createConnection(
|
|
@@ -47,17 +53,17 @@ export interface ResourceRepository {
|
|
|
47
53
|
|
|
48
54
|
// Materializations
|
|
49
55
|
listMaterializations(
|
|
50
|
-
|
|
56
|
+
environmentId: string,
|
|
51
57
|
packageName: string,
|
|
52
58
|
options?: { limit?: number; offset?: number },
|
|
53
59
|
): Promise<Materialization[]>;
|
|
54
60
|
getMaterializationById(id: string): Promise<Materialization | null>;
|
|
55
61
|
getActiveMaterialization(
|
|
56
|
-
|
|
62
|
+
environmentId: string,
|
|
57
63
|
packageName: string,
|
|
58
64
|
): Promise<Materialization | null>;
|
|
59
65
|
createMaterialization(
|
|
60
|
-
|
|
66
|
+
environmentId: string,
|
|
61
67
|
packageName: string,
|
|
62
68
|
status?: MaterializationStatus,
|
|
63
69
|
metadata?: Record<string, unknown> | null,
|
|
@@ -75,7 +81,7 @@ export interface ResourceRepository {
|
|
|
75
81
|
deleteMaterialization(id: string): Promise<void>;
|
|
76
82
|
// Build Manifests
|
|
77
83
|
listManifestEntries(
|
|
78
|
-
|
|
84
|
+
environmentId: string,
|
|
79
85
|
packageName: string,
|
|
80
86
|
): Promise<ManifestEntry[]>;
|
|
81
87
|
upsertManifestEntry(
|
|
@@ -84,7 +90,7 @@ export interface ResourceRepository {
|
|
|
84
90
|
deleteManifestEntry(id: string): Promise<void>;
|
|
85
91
|
}
|
|
86
92
|
|
|
87
|
-
export interface
|
|
93
|
+
export interface Environment {
|
|
88
94
|
id: string;
|
|
89
95
|
name: string;
|
|
90
96
|
path: string;
|
|
@@ -96,7 +102,7 @@ export interface Project {
|
|
|
96
102
|
|
|
97
103
|
export interface Package {
|
|
98
104
|
id: string;
|
|
99
|
-
|
|
105
|
+
environmentId: string;
|
|
100
106
|
name: string;
|
|
101
107
|
description?: string;
|
|
102
108
|
manifestPath: string;
|
|
@@ -107,7 +113,7 @@ export interface Package {
|
|
|
107
113
|
|
|
108
114
|
export interface Connection {
|
|
109
115
|
id: string;
|
|
110
|
-
|
|
116
|
+
environmentId: string;
|
|
111
117
|
name: string;
|
|
112
118
|
type: "bigquery" | "postgres" | "duckdb" | "mysql" | "snowflake" | "trino";
|
|
113
119
|
config: Record<string, unknown>;
|
|
@@ -124,7 +130,7 @@ export type MaterializationStatus =
|
|
|
124
130
|
|
|
125
131
|
export interface Materialization {
|
|
126
132
|
id: string;
|
|
127
|
-
|
|
133
|
+
environmentId: string;
|
|
128
134
|
packageName: string;
|
|
129
135
|
status: MaterializationStatus;
|
|
130
136
|
startedAt: Date | null;
|
|
@@ -137,7 +143,7 @@ export interface Materialization {
|
|
|
137
143
|
|
|
138
144
|
export interface ManifestEntry {
|
|
139
145
|
id: string;
|
|
140
|
-
|
|
146
|
+
environmentId: string;
|
|
141
147
|
packageName: string;
|
|
142
148
|
buildId: string;
|
|
143
149
|
tableName: string;
|
|
@@ -163,9 +169,12 @@ export interface BuildManifest {
|
|
|
163
169
|
* orchestrated mode swaps in a DuckLakeManifestStore.
|
|
164
170
|
*/
|
|
165
171
|
export interface ManifestStore {
|
|
166
|
-
getManifest(
|
|
172
|
+
getManifest(
|
|
173
|
+
environmentId: string,
|
|
174
|
+
packageName: string,
|
|
175
|
+
): Promise<BuildManifest>;
|
|
167
176
|
writeEntry(
|
|
168
|
-
|
|
177
|
+
environmentId: string,
|
|
169
178
|
packageName: string,
|
|
170
179
|
buildId: string,
|
|
171
180
|
tableName: string,
|
|
@@ -174,7 +183,7 @@ export interface ManifestStore {
|
|
|
174
183
|
): Promise<void>;
|
|
175
184
|
deleteEntry(id: string): Promise<void>;
|
|
176
185
|
listEntries(
|
|
177
|
-
|
|
186
|
+
environmentId: string,
|
|
178
187
|
packageName: string,
|
|
179
188
|
): Promise<ManifestEntry[]>;
|
|
180
189
|
}
|
|
@@ -7,12 +7,12 @@ export class StorageManager {
|
|
|
7
7
|
|
|
8
8
|
getRepository() {
|
|
9
9
|
return {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
listEnvironments: async (): Promise<unknown[]> => [],
|
|
11
|
+
createEnvironment: async (data: MockData): Promise<MockData> => ({
|
|
12
12
|
id: "test-id",
|
|
13
13
|
...data,
|
|
14
14
|
}),
|
|
15
|
-
|
|
15
|
+
updateEnvironment: async (
|
|
16
16
|
id: string,
|
|
17
17
|
data: MockData,
|
|
18
18
|
): Promise<MockData> => ({
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as crypto from "crypto";
|
|
1
2
|
import { logger } from "../logger";
|
|
2
3
|
import {
|
|
3
4
|
DatabaseConnection,
|
|
@@ -42,10 +43,23 @@ function escapeSQL(value: string): string {
|
|
|
42
43
|
return value.replace(/'/g, "''");
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
function configKey(c: DuckLakeManifestConfig): string {
|
|
47
|
+
return `${c.catalogUrl}|${c.dataPath}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function catalogNameForConfig(c: DuckLakeManifestConfig): string {
|
|
51
|
+
const hash = crypto
|
|
52
|
+
.createHash("sha256")
|
|
53
|
+
.update(configKey(c))
|
|
54
|
+
.digest("hex")
|
|
55
|
+
.slice(0, 8);
|
|
56
|
+
return `manifest_lake_${hash}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
/**
|
|
46
|
-
* Manages the storage backend (DuckDB, Postgres, etc.) and per-
|
|
47
|
-
* manifest stores.
|
|
48
|
-
* the default DuckDB manifest store.
|
|
60
|
+
* Manages the storage backend (DuckDB, Postgres, etc.) and per-environment
|
|
61
|
+
* manifest stores. Environments without `materializationStorage` config use
|
|
62
|
+
* the default DuckDB manifest store. Environments with the config get a
|
|
49
63
|
* DuckLake-backed store attached lazily on first access.
|
|
50
64
|
*/
|
|
51
65
|
export class StorageManager {
|
|
@@ -54,11 +68,15 @@ export class StorageManager {
|
|
|
54
68
|
private repository: ResourceRepository | null = null;
|
|
55
69
|
private defaultManifestStore: ManifestStore | null = null;
|
|
56
70
|
|
|
57
|
-
/** Per-
|
|
58
|
-
private
|
|
71
|
+
/** Per-environment DuckLake manifest stores, keyed by environmentId. */
|
|
72
|
+
private environmentManifestStores = new Map<string, ManifestStore>();
|
|
59
73
|
|
|
60
|
-
/**
|
|
61
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Tracks attached DuckLake catalogs as `configKey -> catalogName`. Each
|
|
76
|
+
* unique materializationStorage config gets its own ATTACHment under a
|
|
77
|
+
* deterministic catalog name, so multiple configs can coexist on one worker.
|
|
78
|
+
*/
|
|
79
|
+
private attachedCatalogs = new Map<string, string>();
|
|
62
80
|
|
|
63
81
|
private config: StorageConfig;
|
|
64
82
|
|
|
@@ -104,33 +122,45 @@ export class StorageManager {
|
|
|
104
122
|
}
|
|
105
123
|
|
|
106
124
|
/**
|
|
107
|
-
* Lazily initializes a DuckLake manifest store for
|
|
108
|
-
*
|
|
109
|
-
*
|
|
125
|
+
* Lazily initializes a DuckLake manifest store for an environment.
|
|
126
|
+
*
|
|
127
|
+
* One shared catalog per materializationStorage config: every environment
|
|
128
|
+
* pointing at the same (catalogUrl, dataPath) shares one `build_manifests`
|
|
129
|
+
* table inside it, partitioned by `environment_id` (set to the environment's name
|
|
130
|
+
* so it's stable across worker replicas — required for cross-pod manifest
|
|
131
|
+
* visibility in orchestrated mode). Different configs (e.g. different
|
|
132
|
+
* orgs) attach as separate catalogs under distinct deterministic aliases.
|
|
110
133
|
*/
|
|
111
|
-
async
|
|
112
|
-
|
|
134
|
+
async initializeDuckLakeForEnvironment(
|
|
135
|
+
environmentId: string,
|
|
136
|
+
environmentName: string,
|
|
113
137
|
config: DuckLakeManifestConfig,
|
|
114
138
|
): Promise<void> {
|
|
115
139
|
if (!this.duckDbConnection) {
|
|
116
140
|
throw new Error("Storage not initialized. Call initialize() first.");
|
|
117
141
|
}
|
|
118
142
|
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
if (!
|
|
143
|
+
const key = configKey(config);
|
|
144
|
+
let catalogName = this.attachedCatalogs.get(key);
|
|
145
|
+
if (!catalogName) {
|
|
146
|
+
// Catalog name derived from the config so multiple configs can coexist as
|
|
147
|
+
// separate ATTACHments without colliding on the name.
|
|
148
|
+
catalogName = catalogNameForConfig(config);
|
|
122
149
|
await this.attachDuckLakeCatalog(config, catalogName);
|
|
150
|
+
this.attachedCatalogs.set(key, catalogName);
|
|
123
151
|
}
|
|
124
152
|
|
|
125
153
|
const store = new DuckLakeManifestStore(
|
|
126
154
|
this.duckDbConnection,
|
|
127
155
|
catalogName,
|
|
156
|
+
environmentName,
|
|
128
157
|
);
|
|
129
158
|
await store.bootstrapSchema();
|
|
130
159
|
|
|
131
|
-
this.
|
|
132
|
-
logger.info("DuckLake manifest store initialized for
|
|
133
|
-
|
|
160
|
+
this.environmentManifestStores.set(environmentId, store);
|
|
161
|
+
logger.info("DuckLake manifest store initialized for environment", {
|
|
162
|
+
environmentId,
|
|
163
|
+
environmentName,
|
|
134
164
|
catalogName,
|
|
135
165
|
});
|
|
136
166
|
}
|
|
@@ -155,7 +185,14 @@ export class StorageManager {
|
|
|
155
185
|
config.dataPath.startsWith("s3://");
|
|
156
186
|
|
|
157
187
|
let attachCmd = `ATTACH 'ducklake:${escapedCatalogUrl}' AS ${catalogName}`;
|
|
158
|
-
const attachOpts: string[] = [
|
|
188
|
+
const attachOpts: string[] = [
|
|
189
|
+
`DATA_PATH '${escapedDataPath}'`,
|
|
190
|
+
// The manifest table is small relational metadata (one row per build).
|
|
191
|
+
// Set a high inlining limit so writes always land transactionally in
|
|
192
|
+
// the postgres catalog rather than as parquet files in object storage,
|
|
193
|
+
// sidestepping object-storage auth issues entirely for this path.
|
|
194
|
+
"DATA_INLINING_ROW_LIMIT 100000",
|
|
195
|
+
];
|
|
159
196
|
if (isCloudStorage) {
|
|
160
197
|
attachOpts.push("OVERRIDE_DATA_PATH true");
|
|
161
198
|
}
|
|
@@ -163,8 +200,6 @@ export class StorageManager {
|
|
|
163
200
|
|
|
164
201
|
logger.info(`Attaching DuckLake manifest catalog: ${attachCmd}`);
|
|
165
202
|
await connection.run(attachCmd);
|
|
166
|
-
|
|
167
|
-
this.attachedCatalogs.add(catalogName);
|
|
168
203
|
}
|
|
169
204
|
|
|
170
205
|
getRepository(): ResourceRepository {
|
|
@@ -175,15 +210,16 @@ export class StorageManager {
|
|
|
175
210
|
}
|
|
176
211
|
|
|
177
212
|
/**
|
|
178
|
-
* Returns the manifest store for
|
|
213
|
+
* Returns the manifest store for an environment. If the environment has a
|
|
179
214
|
* DuckLake store configured, returns that; otherwise returns the
|
|
180
215
|
* default DuckDB-backed store.
|
|
181
216
|
*/
|
|
182
|
-
getManifestStore(
|
|
183
|
-
if (
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
217
|
+
getManifestStore(environmentId?: string): ManifestStore {
|
|
218
|
+
if (environmentId) {
|
|
219
|
+
const environmentStore =
|
|
220
|
+
this.environmentManifestStores.get(environmentId);
|
|
221
|
+
if (environmentStore) {
|
|
222
|
+
return environmentStore;
|
|
187
223
|
}
|
|
188
224
|
}
|
|
189
225
|
if (!this.defaultManifestStore) {
|
|
@@ -199,7 +235,7 @@ export class StorageManager {
|
|
|
199
235
|
this.duckDbConnection = null;
|
|
200
236
|
this.repository = null;
|
|
201
237
|
this.defaultManifestStore = null;
|
|
202
|
-
this.
|
|
238
|
+
this.environmentManifestStores.clear();
|
|
203
239
|
this.attachedCatalogs.clear();
|
|
204
240
|
}
|
|
205
241
|
}
|
|
@@ -12,11 +12,11 @@ export class ConnectionRepository {
|
|
|
12
12
|
return new Date();
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
async listConnections(
|
|
15
|
+
async listConnections(environmentId: string): Promise<Connection[]> {
|
|
16
16
|
try {
|
|
17
17
|
const rows = await this.db.all<Record<string, unknown>>(
|
|
18
|
-
"SELECT * FROM connections WHERE
|
|
19
|
-
[
|
|
18
|
+
"SELECT * FROM connections WHERE environment_id = ? ORDER BY name",
|
|
19
|
+
[environmentId],
|
|
20
20
|
);
|
|
21
21
|
return rows.map(this.mapToConnection);
|
|
22
22
|
} catch (err: unknown) {
|
|
@@ -35,12 +35,12 @@ export class ConnectionRepository {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
async getConnectionByName(
|
|
38
|
-
|
|
38
|
+
environmentId: string,
|
|
39
39
|
name: string,
|
|
40
40
|
): Promise<Connection | null> {
|
|
41
41
|
const row = await this.db.get<Record<string, unknown>>(
|
|
42
|
-
"SELECT * FROM connections WHERE
|
|
43
|
-
[
|
|
42
|
+
"SELECT * FROM connections WHERE environment_id = ? AND name = ?",
|
|
43
|
+
[environmentId, name],
|
|
44
44
|
);
|
|
45
45
|
return row ? this.mapToConnection(row) : null;
|
|
46
46
|
}
|
|
@@ -55,11 +55,11 @@ export class ConnectionRepository {
|
|
|
55
55
|
const configJson = JSON.stringify(connection.config);
|
|
56
56
|
|
|
57
57
|
await this.db.run(
|
|
58
|
-
`INSERT INTO connections (id,
|
|
58
|
+
`INSERT INTO connections (id, environment_id, name, type, config, created_at, updated_at)
|
|
59
59
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
60
60
|
[
|
|
61
61
|
id,
|
|
62
|
-
connection.
|
|
62
|
+
connection.environmentId,
|
|
63
63
|
connection.name,
|
|
64
64
|
connection.type,
|
|
65
65
|
configJson,
|
|
@@ -123,14 +123,16 @@ export class ConnectionRepository {
|
|
|
123
123
|
await this.db.run("DELETE FROM connections WHERE id = ?", [id]);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
async
|
|
127
|
-
await this.db.run("DELETE FROM connections WHERE
|
|
126
|
+
async deleteConnectionsByEnvironmentId(id: string): Promise<void> {
|
|
127
|
+
await this.db.run("DELETE FROM connections WHERE environment_id = ?", [
|
|
128
|
+
id,
|
|
129
|
+
]);
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
private mapToConnection(row: Record<string, unknown>): Connection {
|
|
131
133
|
return {
|
|
132
134
|
id: row.id as string,
|
|
133
|
-
|
|
135
|
+
environmentId: row.environment_id as string,
|
|
134
136
|
name: row.name as string,
|
|
135
137
|
type: row.type as Connection["type"],
|
|
136
138
|
config: JSON.parse(row.config as string),
|
|
@@ -93,7 +93,7 @@ export class DuckDBConnection implements DatabaseConnection {
|
|
|
93
93
|
return this.mutex.runExclusive(async () => {
|
|
94
94
|
return new Promise<boolean>((resolve) => {
|
|
95
95
|
this.connection!.all(
|
|
96
|
-
"SELECT name FROM sqlite_master WHERE type='table' AND name='
|
|
96
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='environments'",
|
|
97
97
|
(err, rows) => {
|
|
98
98
|
if (err) {
|
|
99
99
|
resolve(false);
|
|
@@ -22,11 +22,11 @@ export class DuckDBManifestStore implements ManifestStore {
|
|
|
22
22
|
* reference has no manifest entry (e.g. before the first materialization).
|
|
23
23
|
*/
|
|
24
24
|
async getManifest(
|
|
25
|
-
|
|
25
|
+
environmentId: string,
|
|
26
26
|
packageName: string,
|
|
27
27
|
): Promise<BuildManifest> {
|
|
28
28
|
const entries = await this.repository.listManifestEntries(
|
|
29
|
-
|
|
29
|
+
environmentId,
|
|
30
30
|
packageName,
|
|
31
31
|
);
|
|
32
32
|
const manifest: BuildManifest = {
|
|
@@ -40,7 +40,7 @@ export class DuckDBManifestStore implements ManifestStore {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
async writeEntry(
|
|
43
|
-
|
|
43
|
+
environmentId: string,
|
|
44
44
|
packageName: string,
|
|
45
45
|
buildId: string,
|
|
46
46
|
tableName: string,
|
|
@@ -48,7 +48,7 @@ export class DuckDBManifestStore implements ManifestStore {
|
|
|
48
48
|
connectionName: string,
|
|
49
49
|
): Promise<void> {
|
|
50
50
|
await this.repository.upsertManifestEntry({
|
|
51
|
-
|
|
51
|
+
environmentId,
|
|
52
52
|
packageName,
|
|
53
53
|
buildId,
|
|
54
54
|
tableName,
|
|
@@ -62,9 +62,9 @@ export class DuckDBManifestStore implements ManifestStore {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
async listEntries(
|
|
65
|
-
|
|
65
|
+
environmentId: string,
|
|
66
66
|
packageName: string,
|
|
67
67
|
): Promise<ManifestEntry[]> {
|
|
68
|
-
return this.repository.listManifestEntries(
|
|
68
|
+
return this.repository.listManifestEntries(environmentId, packageName);
|
|
69
69
|
}
|
|
70
70
|
}
|