@malloy-publisher/server 0.0.196-dev → 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 +1954 -1318
- package/package.json +1 -1
- 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 -85
- package/src/service/{project_store.ts → environment_store.ts} +368 -326
- 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 +2 -2
- 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 +24 -23
- 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 +14 -14
- package/tests/fixtures/publisher.config.json +1 -1
- package/tests/harness/e2e.ts +1 -1
- package/tests/harness/mcp_test_setup.ts +1 -1
- 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 +27 -48
- package/tests/integration/mcp/mcp_resource.integration.spec.ts +26 -35
- 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
|
@@ -1,73 +1,73 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Connection,
|
|
3
|
+
Environment,
|
|
3
4
|
ManifestEntry,
|
|
4
5
|
Materialization,
|
|
5
6
|
MaterializationStatus,
|
|
6
7
|
Package,
|
|
7
|
-
Project,
|
|
8
8
|
ResourceRepository,
|
|
9
9
|
} from "../DatabaseInterface";
|
|
10
10
|
import { ConnectionRepository } from "./ConnectionRepository";
|
|
11
11
|
import { DuckDBConnection } from "./DuckDBConnection";
|
|
12
|
+
import { EnvironmentRepository } from "./EnvironmentRepository";
|
|
12
13
|
import { ManifestRepository } from "./ManifestRepository";
|
|
13
14
|
import { MaterializationRepository } from "./MaterializationRepository";
|
|
14
15
|
import { PackageRepository } from "./PackageRepository";
|
|
15
|
-
import { ProjectRepository } from "./ProjectRepository";
|
|
16
16
|
|
|
17
17
|
export class DuckDBRepository implements ResourceRepository {
|
|
18
|
-
private
|
|
18
|
+
private environmentRepo: EnvironmentRepository;
|
|
19
19
|
private packageRepo: PackageRepository;
|
|
20
20
|
private connectionRepo: ConnectionRepository;
|
|
21
21
|
private materializationRepo: MaterializationRepository;
|
|
22
22
|
private manifestRepo: ManifestRepository;
|
|
23
23
|
|
|
24
24
|
constructor(public db: DuckDBConnection) {
|
|
25
|
-
this.
|
|
25
|
+
this.environmentRepo = new EnvironmentRepository(db);
|
|
26
26
|
this.packageRepo = new PackageRepository(db);
|
|
27
27
|
this.connectionRepo = new ConnectionRepository(db);
|
|
28
28
|
this.materializationRepo = new MaterializationRepository(db);
|
|
29
29
|
this.manifestRepo = new ManifestRepository(db);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
// ====================
|
|
32
|
+
// ==================== ENVIRONMENTS ====================
|
|
33
33
|
|
|
34
|
-
async
|
|
35
|
-
return this.
|
|
34
|
+
async listEnvironments(): Promise<Environment[]> {
|
|
35
|
+
return this.environmentRepo.listEnvironments();
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
async
|
|
39
|
-
return this.
|
|
38
|
+
async getEnvironmentById(id: string): Promise<Environment | null> {
|
|
39
|
+
return this.environmentRepo.getEnvironmentById(id);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
async
|
|
43
|
-
return this.
|
|
42
|
+
async getEnvironmentByName(name: string): Promise<Environment | null> {
|
|
43
|
+
return this.environmentRepo.getEnvironmentByName(name);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
async
|
|
47
|
-
|
|
48
|
-
): Promise<
|
|
49
|
-
return this.
|
|
46
|
+
async createEnvironment(
|
|
47
|
+
environment: Omit<Environment, "id" | "createdAt" | "updatedAt">,
|
|
48
|
+
): Promise<Environment> {
|
|
49
|
+
return this.environmentRepo.createEnvironment(environment);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
async
|
|
52
|
+
async updateEnvironment(
|
|
53
53
|
id: string,
|
|
54
|
-
updates: Partial<
|
|
55
|
-
): Promise<
|
|
56
|
-
return this.
|
|
54
|
+
updates: Partial<Environment>,
|
|
55
|
+
): Promise<Environment> {
|
|
56
|
+
return this.environmentRepo.updateEnvironment(id, updates);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
async
|
|
60
|
-
await this.manifestRepo.
|
|
61
|
-
await this.materializationRepo.
|
|
62
|
-
await this.connectionRepo.
|
|
63
|
-
await this.packageRepo.
|
|
64
|
-
await this.
|
|
59
|
+
async deleteEnvironment(id: string): Promise<void> {
|
|
60
|
+
await this.manifestRepo.deleteEntriesByEnvironmentId(id);
|
|
61
|
+
await this.materializationRepo.deleteByEnvironmentId(id);
|
|
62
|
+
await this.connectionRepo.deleteConnectionsByEnvironmentId(id);
|
|
63
|
+
await this.packageRepo.deletePackagesByEnvironmentId(id);
|
|
64
|
+
await this.environmentRepo.deleteEnvironment(id);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// ==================== PACKAGES ====================
|
|
68
68
|
|
|
69
|
-
async listPackages(
|
|
70
|
-
return this.packageRepo.listPackages(
|
|
69
|
+
async listPackages(environmentId: string): Promise<Package[]> {
|
|
70
|
+
return this.packageRepo.listPackages(environmentId);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
async getPackageById(id: string): Promise<Package | null> {
|
|
@@ -75,10 +75,10 @@ export class DuckDBRepository implements ResourceRepository {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
async getPackageByName(
|
|
78
|
-
|
|
78
|
+
environmentId: string,
|
|
79
79
|
name: string,
|
|
80
80
|
): Promise<Package | null> {
|
|
81
|
-
return this.packageRepo.getPackageByName(
|
|
81
|
+
return this.packageRepo.getPackageByName(environmentId, name);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
async createPackage(
|
|
@@ -98,25 +98,25 @@ export class DuckDBRepository implements ResourceRepository {
|
|
|
98
98
|
const pkg = await this.packageRepo.getPackageById(id);
|
|
99
99
|
if (pkg) {
|
|
100
100
|
await this.manifestRepo.deleteEntriesByPackage(
|
|
101
|
-
pkg.
|
|
101
|
+
pkg.environmentId,
|
|
102
102
|
pkg.name,
|
|
103
103
|
);
|
|
104
104
|
await this.materializationRepo.deleteByPackage(
|
|
105
|
-
pkg.
|
|
105
|
+
pkg.environmentId,
|
|
106
106
|
pkg.name,
|
|
107
107
|
);
|
|
108
108
|
}
|
|
109
109
|
await this.packageRepo.deletePackage(id);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
async
|
|
113
|
-
return this.packageRepo.
|
|
112
|
+
async deletePackagesByEnvironmentId(id: string): Promise<void> {
|
|
113
|
+
return this.packageRepo.deletePackagesByEnvironmentId(id);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
// ==================== CONNECTIONS ====================
|
|
117
117
|
|
|
118
|
-
async listConnections(
|
|
119
|
-
return this.connectionRepo.listConnections(
|
|
118
|
+
async listConnections(environmentId: string): Promise<Connection[]> {
|
|
119
|
+
return this.connectionRepo.listConnections(environmentId);
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
async getConnectionById(id: string): Promise<Connection | null> {
|
|
@@ -124,10 +124,10 @@ export class DuckDBRepository implements ResourceRepository {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
async getConnectionByName(
|
|
127
|
-
|
|
127
|
+
environmentId: string,
|
|
128
128
|
name: string,
|
|
129
129
|
): Promise<Connection | null> {
|
|
130
|
-
return this.connectionRepo.getConnectionByName(
|
|
130
|
+
return this.connectionRepo.getConnectionByName(environmentId, name);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
async createConnection(
|
|
@@ -147,18 +147,18 @@ export class DuckDBRepository implements ResourceRepository {
|
|
|
147
147
|
return this.connectionRepo.deleteConnection(id);
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
async
|
|
151
|
-
return this.connectionRepo.
|
|
150
|
+
async deleteConnectionsByEnvironmentId(id: string): Promise<void> {
|
|
151
|
+
return this.connectionRepo.deleteConnectionsByEnvironmentId(id);
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
// ==================== MATERIALIZATIONS ====================
|
|
155
155
|
|
|
156
156
|
async listMaterializations(
|
|
157
|
-
|
|
157
|
+
environmentId: string,
|
|
158
158
|
packageName: string,
|
|
159
159
|
options?: { limit?: number; offset?: number },
|
|
160
160
|
): Promise<Materialization[]> {
|
|
161
|
-
return this.materializationRepo.list(
|
|
161
|
+
return this.materializationRepo.list(environmentId, packageName, options);
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
async getMaterializationById(id: string): Promise<Materialization | null> {
|
|
@@ -166,20 +166,20 @@ export class DuckDBRepository implements ResourceRepository {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
async getActiveMaterialization(
|
|
169
|
-
|
|
169
|
+
environmentId: string,
|
|
170
170
|
packageName: string,
|
|
171
171
|
): Promise<Materialization | null> {
|
|
172
|
-
return this.materializationRepo.getActive(
|
|
172
|
+
return this.materializationRepo.getActive(environmentId, packageName);
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
async createMaterialization(
|
|
176
|
-
|
|
176
|
+
environmentId: string,
|
|
177
177
|
packageName: string,
|
|
178
178
|
status: MaterializationStatus = "PENDING",
|
|
179
179
|
metadata: Record<string, unknown> | null = null,
|
|
180
180
|
): Promise<Materialization> {
|
|
181
181
|
return this.materializationRepo.create(
|
|
182
|
-
|
|
182
|
+
environmentId,
|
|
183
183
|
packageName,
|
|
184
184
|
status,
|
|
185
185
|
metadata,
|
|
@@ -206,10 +206,10 @@ export class DuckDBRepository implements ResourceRepository {
|
|
|
206
206
|
// ==================== BUILD MANIFESTS ====================
|
|
207
207
|
|
|
208
208
|
async listManifestEntries(
|
|
209
|
-
|
|
209
|
+
environmentId: string,
|
|
210
210
|
packageName: string,
|
|
211
211
|
): Promise<ManifestEntry[]> {
|
|
212
|
-
return this.manifestRepo.listEntries(
|
|
212
|
+
return this.manifestRepo.listEntries(environmentId, packageName);
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
async upsertManifestEntry(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Environment } from "../DatabaseInterface";
|
|
2
2
|
import { DuckDBConnection } from "./DuckDBConnection";
|
|
3
3
|
|
|
4
|
-
export class
|
|
4
|
+
export class EnvironmentRepository {
|
|
5
5
|
constructor(private db: DuckDBConnection) {}
|
|
6
6
|
|
|
7
7
|
private generateId(): string {
|
|
@@ -12,85 +12,85 @@ export class ProjectRepository {
|
|
|
12
12
|
return new Date();
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
async
|
|
15
|
+
async listEnvironments(): Promise<Environment[]> {
|
|
16
16
|
const rows = await this.db.all<Record<string, unknown>>(
|
|
17
|
-
"SELECT * FROM
|
|
17
|
+
"SELECT * FROM environments ORDER BY name",
|
|
18
18
|
);
|
|
19
|
-
return rows.map(this.
|
|
19
|
+
return rows.map(this.mapToEnvironment);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
async
|
|
22
|
+
async getEnvironmentById(id: string): Promise<Environment | null> {
|
|
23
23
|
const row = await this.db.get<Record<string, unknown>>(
|
|
24
|
-
"SELECT * FROM
|
|
24
|
+
"SELECT * FROM environments WHERE id = ?",
|
|
25
25
|
[id],
|
|
26
26
|
);
|
|
27
|
-
return row ? this.
|
|
27
|
+
return row ? this.mapToEnvironment(row) : null;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
async
|
|
30
|
+
async getEnvironmentByName(name: string): Promise<Environment | null> {
|
|
31
31
|
const row = await this.db.get<Record<string, unknown>>(
|
|
32
|
-
"SELECT * FROM
|
|
32
|
+
"SELECT * FROM environments WHERE name = ?",
|
|
33
33
|
[name],
|
|
34
34
|
);
|
|
35
|
-
return row ? this.
|
|
35
|
+
return row ? this.mapToEnvironment(row) : null;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
async
|
|
39
|
-
|
|
40
|
-
): Promise<
|
|
38
|
+
async createEnvironment(
|
|
39
|
+
environment: Omit<Environment, "id" | "createdAt" | "updatedAt">,
|
|
40
|
+
): Promise<Environment> {
|
|
41
41
|
const id = this.generateId();
|
|
42
42
|
const now = this.now();
|
|
43
43
|
|
|
44
44
|
const params = [
|
|
45
45
|
id,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
environment.name,
|
|
47
|
+
environment.path,
|
|
48
|
+
environment.description || null,
|
|
49
|
+
environment.metadata ? JSON.stringify(environment.metadata) : null,
|
|
50
50
|
now.toISOString(),
|
|
51
51
|
now.toISOString(),
|
|
52
52
|
];
|
|
53
53
|
|
|
54
54
|
try {
|
|
55
55
|
await this.db.run(
|
|
56
|
-
`INSERT INTO
|
|
56
|
+
`INSERT INTO environments (id, name, path, description, metadata, created_at, updated_at)
|
|
57
57
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
58
58
|
params,
|
|
59
59
|
);
|
|
60
60
|
|
|
61
61
|
return {
|
|
62
62
|
id,
|
|
63
|
-
...
|
|
63
|
+
...environment,
|
|
64
64
|
createdAt: now,
|
|
65
65
|
updatedAt: now,
|
|
66
66
|
};
|
|
67
67
|
} catch (err: unknown) {
|
|
68
68
|
const error = err as Error;
|
|
69
|
-
// If unique constraint violation, return existing
|
|
69
|
+
// If unique constraint violation, return existing environment
|
|
70
70
|
if (
|
|
71
71
|
error.message?.includes("UNIQUE") ||
|
|
72
72
|
error.message?.includes("Constraint")
|
|
73
73
|
) {
|
|
74
74
|
const existing = await this.db.get<Record<string, unknown>>(
|
|
75
|
-
"SELECT * FROM
|
|
76
|
-
[
|
|
75
|
+
"SELECT * FROM environments WHERE name = ?",
|
|
76
|
+
[environment.name],
|
|
77
77
|
);
|
|
78
78
|
if (existing) {
|
|
79
|
-
console.log("Returning existing
|
|
80
|
-
return this.
|
|
79
|
+
console.log("Returning existing environment");
|
|
80
|
+
return this.mapToEnvironment(existing);
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
throw error;
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
async
|
|
87
|
+
async updateEnvironment(
|
|
88
88
|
id: string,
|
|
89
|
-
updates: Partial<
|
|
90
|
-
): Promise<
|
|
91
|
-
const existing = await this.
|
|
89
|
+
updates: Partial<Environment>,
|
|
90
|
+
): Promise<Environment> {
|
|
91
|
+
const existing = await this.getEnvironmentById(id);
|
|
92
92
|
if (!existing) {
|
|
93
|
-
throw new Error(`
|
|
93
|
+
throw new Error(`Environment with id ${id} not found`);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
const now = this.now();
|
|
@@ -121,18 +121,18 @@ export class ProjectRepository {
|
|
|
121
121
|
params.push(id);
|
|
122
122
|
|
|
123
123
|
await this.db.run(
|
|
124
|
-
`UPDATE
|
|
124
|
+
`UPDATE environments SET ${setClauses.join(", ")} WHERE id = ?`,
|
|
125
125
|
params,
|
|
126
126
|
);
|
|
127
127
|
|
|
128
|
-
return this.
|
|
128
|
+
return this.getEnvironmentById(id) as Promise<Environment>;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
async
|
|
132
|
-
await this.db.run("DELETE FROM
|
|
131
|
+
async deleteEnvironment(id: string): Promise<void> {
|
|
132
|
+
await this.db.run("DELETE FROM environments WHERE id = ?", [id]);
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
private
|
|
135
|
+
private mapToEnvironment(row: Record<string, unknown>): Environment {
|
|
136
136
|
return {
|
|
137
137
|
id: row.id as string,
|
|
138
138
|
name: row.name as string,
|
|
@@ -6,7 +6,7 @@ import { DuckDBConnection } from "./DuckDBConnection";
|
|
|
6
6
|
*
|
|
7
7
|
* A build manifest records the materialized table produced by a specific build
|
|
8
8
|
* of a package, linking it back to the Malloy source and connection that
|
|
9
|
-
* generated it. Manifests are keyed by (
|
|
9
|
+
* generated it. Manifests are keyed by (environment, package, build) and enable
|
|
10
10
|
* downstream consumers to discover which physical tables correspond to a given
|
|
11
11
|
* package version.
|
|
12
12
|
*/
|
|
@@ -19,32 +19,32 @@ export class ManifestRepository {
|
|
|
19
19
|
|
|
20
20
|
/** Returns all manifest entries for a package, most recent first. */
|
|
21
21
|
async listEntries(
|
|
22
|
-
|
|
22
|
+
environmentId: string,
|
|
23
23
|
packageName: string,
|
|
24
24
|
): Promise<ManifestEntry[]> {
|
|
25
25
|
const rows = await this.db.all<Record<string, unknown>>(
|
|
26
|
-
"SELECT * FROM build_manifests WHERE
|
|
27
|
-
[
|
|
26
|
+
"SELECT * FROM build_manifests WHERE environment_id = ? AND package_name = ? ORDER BY created_at DESC",
|
|
27
|
+
[environmentId, packageName],
|
|
28
28
|
);
|
|
29
29
|
return rows.map(this.mapToEntry);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/** Looks up the manifest entry for a specific build, or null if none exists. */
|
|
33
33
|
async getEntryByBuildId(
|
|
34
|
-
|
|
34
|
+
environmentId: string,
|
|
35
35
|
packageName: string,
|
|
36
36
|
buildId: string,
|
|
37
37
|
): Promise<ManifestEntry | null> {
|
|
38
38
|
const row = await this.db.get<Record<string, unknown>>(
|
|
39
|
-
"SELECT * FROM build_manifests WHERE
|
|
40
|
-
[
|
|
39
|
+
"SELECT * FROM build_manifests WHERE environment_id = ? AND package_name = ? AND build_id = ?",
|
|
40
|
+
[environmentId, packageName, buildId],
|
|
41
41
|
);
|
|
42
42
|
return row ? this.mapToEntry(row) : null;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* Inserts a new manifest entry, or updates the existing one if a row with
|
|
47
|
-
* the same (
|
|
47
|
+
* the same (environment, package, build) triple already exists. Uses INSERT ON
|
|
48
48
|
* CONFLICT to avoid the TOCTOU race of SELECT-then-INSERT/UPDATE.
|
|
49
49
|
*/
|
|
50
50
|
async upsertEntry(
|
|
@@ -55,9 +55,9 @@ export class ManifestRepository {
|
|
|
55
55
|
const iso = now.toISOString();
|
|
56
56
|
|
|
57
57
|
const rows = await this.db.all<Record<string, unknown>>(
|
|
58
|
-
`INSERT INTO build_manifests (id,
|
|
58
|
+
`INSERT INTO build_manifests (id, environment_id, package_name, build_id, table_name, source_name, connection_name, created_at, updated_at)
|
|
59
59
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
60
|
-
ON CONFLICT (
|
|
60
|
+
ON CONFLICT (environment_id, package_name, build_id)
|
|
61
61
|
DO UPDATE SET table_name = EXCLUDED.table_name,
|
|
62
62
|
source_name = EXCLUDED.source_name,
|
|
63
63
|
connection_name = EXCLUDED.connection_name,
|
|
@@ -65,7 +65,7 @@ export class ManifestRepository {
|
|
|
65
65
|
RETURNING *`,
|
|
66
66
|
[
|
|
67
67
|
id,
|
|
68
|
-
entry.
|
|
68
|
+
entry.environmentId,
|
|
69
69
|
entry.packageName,
|
|
70
70
|
entry.buildId,
|
|
71
71
|
entry.tableName,
|
|
@@ -84,21 +84,22 @@ export class ManifestRepository {
|
|
|
84
84
|
await this.db.run("DELETE FROM build_manifests WHERE id = ?", [id]);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
/** Removes all manifest entries belonging to
|
|
88
|
-
async
|
|
89
|
-
await this.db.run(
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
/** Removes all manifest entries belonging to an environment (used on environment deletion). */
|
|
88
|
+
async deleteEntriesByEnvironmentId(environmentId: string): Promise<void> {
|
|
89
|
+
await this.db.run(
|
|
90
|
+
"DELETE FROM build_manifests WHERE environment_id = ?",
|
|
91
|
+
[environmentId],
|
|
92
|
+
);
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
/** Removes all manifest entries for a specific package. */
|
|
95
96
|
async deleteEntriesByPackage(
|
|
96
|
-
|
|
97
|
+
environmentId: string,
|
|
97
98
|
packageName: string,
|
|
98
99
|
): Promise<void> {
|
|
99
100
|
await this.db.run(
|
|
100
|
-
"DELETE FROM build_manifests WHERE
|
|
101
|
-
[
|
|
101
|
+
"DELETE FROM build_manifests WHERE environment_id = ? AND package_name = ?",
|
|
102
|
+
[environmentId, packageName],
|
|
102
103
|
);
|
|
103
104
|
}
|
|
104
105
|
|
|
@@ -106,7 +107,7 @@ export class ManifestRepository {
|
|
|
106
107
|
private mapToEntry(row: Record<string, unknown>): ManifestEntry {
|
|
107
108
|
return {
|
|
108
109
|
id: row.id as string,
|
|
109
|
-
|
|
110
|
+
environmentId: row.environment_id as string,
|
|
110
111
|
packageName: row.package_name as string,
|
|
111
112
|
buildId: row.build_id as string,
|
|
112
113
|
tableName: row.table_name as string,
|
|
@@ -7,19 +7,19 @@ const TERMINAL_STATUSES: ReadonlySet<MaterializationStatus> = new Set([
|
|
|
7
7
|
"CANCELLED",
|
|
8
8
|
]);
|
|
9
9
|
|
|
10
|
-
function activeKeyFor(
|
|
11
|
-
return `${
|
|
10
|
+
function activeKeyFor(environmentId: string, packageName: string): string {
|
|
11
|
+
return `${environmentId}|${packageName}`;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Thrown when an atomic insert loses a race on (
|
|
15
|
+
* Thrown when an atomic insert loses a race on (environment, package) active
|
|
16
16
|
* materialization. Surfaced separately from a generic DB error so the service
|
|
17
17
|
* layer can translate to `MaterializationConflictError`.
|
|
18
18
|
*/
|
|
19
19
|
export class DuplicateActiveMaterializationError extends Error {
|
|
20
|
-
constructor(
|
|
20
|
+
constructor(environmentId: string, packageName: string) {
|
|
21
21
|
super(
|
|
22
|
-
`Active materialization already exists for (${
|
|
22
|
+
`Active materialization already exists for (${environmentId}, ${packageName})`,
|
|
23
23
|
);
|
|
24
24
|
}
|
|
25
25
|
}
|
|
@@ -27,7 +27,7 @@ export class DuplicateActiveMaterializationError extends Error {
|
|
|
27
27
|
/**
|
|
28
28
|
* DuckDB-backed repository for package materializations.
|
|
29
29
|
*
|
|
30
|
-
* A Materialization tracks a single build run for
|
|
30
|
+
* A Materialization tracks a single build run for an (environment, package) pair
|
|
31
31
|
* through its lifecycle: PENDING -> RUNNING -> SUCCESS | FAILED | CANCELLED.
|
|
32
32
|
*/
|
|
33
33
|
export class MaterializationRepository {
|
|
@@ -42,13 +42,13 @@ export class MaterializationRepository {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
async list(
|
|
45
|
-
|
|
45
|
+
environmentId: string,
|
|
46
46
|
packageName: string,
|
|
47
47
|
options?: { limit?: number; offset?: number },
|
|
48
48
|
): Promise<Materialization[]> {
|
|
49
49
|
let sql =
|
|
50
|
-
"SELECT * FROM materializations WHERE
|
|
51
|
-
const params: unknown[] = [
|
|
50
|
+
"SELECT * FROM materializations WHERE environment_id = ? AND package_name = ? ORDER BY created_at DESC";
|
|
51
|
+
const params: unknown[] = [environmentId, packageName];
|
|
52
52
|
if (options?.limit !== undefined) {
|
|
53
53
|
sql += " LIMIT ?";
|
|
54
54
|
params.push(options.limit);
|
|
@@ -70,18 +70,18 @@ export class MaterializationRepository {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
async getActive(
|
|
73
|
-
|
|
73
|
+
environmentId: string,
|
|
74
74
|
packageName: string,
|
|
75
75
|
): Promise<Materialization | null> {
|
|
76
76
|
const row = await this.db.get<Record<string, unknown>>(
|
|
77
|
-
"SELECT * FROM materializations WHERE
|
|
78
|
-
[
|
|
77
|
+
"SELECT * FROM materializations WHERE environment_id = ? AND package_name = ? AND status IN ('PENDING', 'RUNNING')",
|
|
78
|
+
[environmentId, packageName],
|
|
79
79
|
);
|
|
80
80
|
return row ? this.mapRow(row) : null;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
async create(
|
|
84
|
-
|
|
84
|
+
environmentId: string,
|
|
85
85
|
packageName: string,
|
|
86
86
|
status: MaterializationStatus = "PENDING",
|
|
87
87
|
metadata: Record<string, unknown> | null = null,
|
|
@@ -91,21 +91,21 @@ export class MaterializationRepository {
|
|
|
91
91
|
const iso = now.toISOString();
|
|
92
92
|
// Set active_key iff the row is in a non-terminal state. The unique
|
|
93
93
|
// index on active_key makes the race-free conditional insert: a second
|
|
94
|
-
// concurrent create on the same (
|
|
94
|
+
// concurrent create on the same (environment, package) fails here rather
|
|
95
95
|
// than in a check-then-write window.
|
|
96
96
|
const activeKey = TERMINAL_STATUSES.has(status)
|
|
97
97
|
? null
|
|
98
|
-
: activeKeyFor(
|
|
98
|
+
: activeKeyFor(environmentId, packageName);
|
|
99
99
|
const metadataJson = metadata ? JSON.stringify(metadata) : null;
|
|
100
100
|
|
|
101
101
|
try {
|
|
102
102
|
const rows = await this.db.all<Record<string, unknown>>(
|
|
103
|
-
`INSERT INTO materializations (id,
|
|
103
|
+
`INSERT INTO materializations (id, environment_id, package_name, status, active_key, metadata, created_at, updated_at)
|
|
104
104
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
105
105
|
RETURNING *`,
|
|
106
106
|
[
|
|
107
107
|
id,
|
|
108
|
-
|
|
108
|
+
environmentId,
|
|
109
109
|
packageName,
|
|
110
110
|
status,
|
|
111
111
|
activeKey,
|
|
@@ -118,7 +118,7 @@ export class MaterializationRepository {
|
|
|
118
118
|
} catch (err) {
|
|
119
119
|
if (isUniqueViolation(err, "idx_materializations_active_key")) {
|
|
120
120
|
throw new DuplicateActiveMaterializationError(
|
|
121
|
-
|
|
121
|
+
environmentId,
|
|
122
122
|
packageName,
|
|
123
123
|
);
|
|
124
124
|
}
|
|
@@ -146,11 +146,13 @@ export class MaterializationRepository {
|
|
|
146
146
|
// Clear active_key on any transition to a terminal state; set it on
|
|
147
147
|
// any transition to a non-terminal state. The unique index
|
|
148
148
|
// guarantees we can never end up with two active rows for the same
|
|
149
|
-
// (
|
|
149
|
+
// (environment, package).
|
|
150
150
|
if (TERMINAL_STATUSES.has(updates.status)) {
|
|
151
151
|
setClauses.push(`active_key = NULL`);
|
|
152
152
|
} else {
|
|
153
|
-
setClauses.push(
|
|
153
|
+
setClauses.push(
|
|
154
|
+
`active_key = environment_id || '|' || package_name`,
|
|
155
|
+
);
|
|
154
156
|
}
|
|
155
157
|
}
|
|
156
158
|
if (updates.startedAt !== undefined) {
|
|
@@ -188,10 +190,11 @@ export class MaterializationRepository {
|
|
|
188
190
|
return updated;
|
|
189
191
|
}
|
|
190
192
|
|
|
191
|
-
async
|
|
192
|
-
await this.db.run(
|
|
193
|
-
|
|
194
|
-
|
|
193
|
+
async deleteByEnvironmentId(environmentId: string): Promise<void> {
|
|
194
|
+
await this.db.run(
|
|
195
|
+
"DELETE FROM materializations WHERE environment_id = ?",
|
|
196
|
+
[environmentId],
|
|
197
|
+
);
|
|
195
198
|
}
|
|
196
199
|
|
|
197
200
|
async deleteById(id: string): Promise<void> {
|
|
@@ -199,12 +202,12 @@ export class MaterializationRepository {
|
|
|
199
202
|
}
|
|
200
203
|
|
|
201
204
|
async deleteByPackage(
|
|
202
|
-
|
|
205
|
+
environmentId: string,
|
|
203
206
|
packageName: string,
|
|
204
207
|
): Promise<void> {
|
|
205
208
|
await this.db.run(
|
|
206
|
-
"DELETE FROM materializations WHERE
|
|
207
|
-
[
|
|
209
|
+
"DELETE FROM materializations WHERE environment_id = ? AND package_name = ?",
|
|
210
|
+
[environmentId, packageName],
|
|
208
211
|
);
|
|
209
212
|
}
|
|
210
213
|
|
|
@@ -220,7 +223,7 @@ export class MaterializationRepository {
|
|
|
220
223
|
|
|
221
224
|
return {
|
|
222
225
|
id: row.id as string,
|
|
223
|
-
|
|
226
|
+
environmentId: row.environment_id as string,
|
|
224
227
|
packageName: row.package_name as string,
|
|
225
228
|
status: row.status as MaterializationStatus,
|
|
226
229
|
startedAt: row.started_at ? new Date(row.started_at as string) : null,
|