@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.
Files changed (52) hide show
  1. package/dist/app/api-doc.yaml +423 -60
  2. package/dist/app/assets/{HomePage-DsuUvSI_.js → HomePage-Dn3E4CuB.js} +1 -1
  3. package/dist/app/assets/{MainPage-DHWFkEN6.js → MainPage-BzB3yoqi.js} +1 -1
  4. package/dist/app/assets/{ModelPage-DNwcx1nE.js → ModelPage-C9O_sAXT.js} +1 -1
  5. package/dist/app/assets/{PackagePage-DSgz9G2V.js → PackagePage-DcxKEjBX.js} +1 -1
  6. package/dist/app/assets/{ProjectPage-CSdPosLV.js → ProjectPage-BDj307rF.js} +1 -1
  7. package/dist/app/assets/{RouteError-orw1RX8q.js → RouteError-DAShbVCG.js} +1 -1
  8. package/dist/app/assets/{WorkbookPage-Bp-BpGjL.js → WorkbookPage-Cs_XYEaB.js} +1 -1
  9. package/dist/app/assets/{core-B4ZYB7aS.es-8Zh0TkSr.js → core-CjeTkq8O.es-BqRc6yhC.js} +1 -1
  10. package/dist/app/assets/{index-BL2TJgTw.js → index-15BOvhp0.js} +4 -4
  11. package/dist/app/assets/{index-BWJkzsfl.js → index-Bb2jqquW.js} +1 -1
  12. package/dist/app/assets/{index-BefdHHMa.js → index-D68X76-7.js} +1 -1
  13. package/dist/app/assets/{index.umd-lY-87l4L.js → index.umd-DGBekgSu.js} +1 -1
  14. package/dist/app/index.html +1 -1
  15. package/dist/instrumentation.js +98 -77
  16. package/dist/server.js +1834 -450
  17. package/package.json +5 -3
  18. package/src/controller/connection.controller.ts +27 -20
  19. package/src/controller/manifest.controller.ts +29 -0
  20. package/src/controller/materialization.controller.ts +125 -0
  21. package/src/controller/model.controller.ts +0 -2
  22. package/src/controller/package.controller.ts +53 -2
  23. package/src/errors.ts +24 -0
  24. package/src/server.ts +196 -5
  25. package/src/service/manifest_service.spec.ts +201 -0
  26. package/src/service/manifest_service.ts +106 -0
  27. package/src/service/materialization_service.spec.ts +648 -0
  28. package/src/service/materialization_service.ts +929 -0
  29. package/src/service/materialized_table_gc.spec.ts +383 -0
  30. package/src/service/materialized_table_gc.ts +279 -0
  31. package/src/service/model.ts +25 -4
  32. package/src/service/package.ts +50 -0
  33. package/src/service/project_store.ts +21 -2
  34. package/src/service/quoting.ts +41 -0
  35. package/src/service/resolve_project.ts +13 -0
  36. package/src/storage/DatabaseInterface.ts +103 -1
  37. package/src/storage/{StorageManager.spec.ts → StorageManager.mock.ts} +9 -0
  38. package/src/storage/StorageManager.ts +119 -1
  39. package/src/storage/duckdb/DuckDBManifestStore.ts +70 -0
  40. package/src/storage/duckdb/DuckDBRepository.ts +99 -9
  41. package/src/storage/duckdb/ManifestRepository.ts +119 -0
  42. package/src/storage/duckdb/MaterializationRepository.ts +249 -0
  43. package/src/storage/duckdb/manifest_store.spec.ts +133 -0
  44. package/src/storage/duckdb/schema.ts +59 -1
  45. package/src/storage/ducklake/DuckLakeManifestStore.ts +146 -0
  46. package/tests/fixtures/persist-test/data/orders.csv +5 -0
  47. package/tests/fixtures/persist-test/persist_test.malloy +11 -0
  48. package/tests/fixtures/persist-test/publisher.json +5 -0
  49. package/tests/fixtures/publisher.config.json +15 -0
  50. package/tests/harness/rest_e2e.ts +68 -0
  51. package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +470 -0
  52. package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +2 -2
@@ -41,6 +41,7 @@ import {
41
41
  ModelNotFoundError,
42
42
  } from "../errors";
43
43
  import { logger } from "../logger";
44
+ import { BuildManifest } from "../storage/DatabaseInterface";
44
45
  import { URL_READER } from "../utils";
45
46
  import {
46
47
  buildFilterClause,
@@ -158,11 +159,17 @@ export class Model {
158
159
  packagePath: string,
159
160
  modelPath: string,
160
161
  connections: Map<string, Connection>,
162
+ options?: { buildManifest?: BuildManifest["entries"] },
161
163
  ): Promise<Model> {
162
164
  // getModelRuntime might throw a ModelNotFoundError. It's the callers responsibility
163
165
  // to pass a valid model path or handle the error.
164
166
  const { runtime, modelURL, importBaseURL, dataStyles, modelType } =
165
- await Model.getModelRuntime(packagePath, modelPath, connections);
167
+ await Model.getModelRuntime(
168
+ packagePath,
169
+ modelPath,
170
+ connections,
171
+ options,
172
+ );
166
173
 
167
174
  try {
168
175
  const { modelMaterializer, runnableNotebookCells } =
@@ -291,7 +298,7 @@ export class Model {
291
298
  }
292
299
 
293
300
  public getQueries(): ApiQuery[] | undefined {
294
- return this.sources;
301
+ return this.queries;
295
302
  }
296
303
 
297
304
  public async getModel(): Promise<ApiCompiledModel> {
@@ -662,6 +669,7 @@ export class Model {
662
669
  packagePath: string,
663
670
  modelPath: string,
664
671
  connections: Map<string, Connection>,
672
+ options?: { buildManifest?: BuildManifest["entries"] },
665
673
  ): Promise<{
666
674
  runtime: Runtime;
667
675
  modelURL: URL;
@@ -701,10 +709,23 @@ export class Model {
701
709
  `SET FILE_SEARCH_PATH='${workingDirectory}';`,
702
710
  );
703
711
 
704
- const runtime = new Runtime({
712
+ const runtimeOptions: {
713
+ urlReader: typeof urlReader;
714
+ connections: FixedConnectionMap;
715
+ buildManifest?: BuildManifest;
716
+ } = {
705
717
  urlReader,
706
718
  connections: new FixedConnectionMap(connections, "duckdb"),
707
- });
719
+ };
720
+
721
+ if (options?.buildManifest) {
722
+ runtimeOptions.buildManifest = {
723
+ entries: options.buildManifest,
724
+ strict: false,
725
+ };
726
+ }
727
+
728
+ const runtime = new Runtime(runtimeOptions);
708
729
  const dataStyles = urlReader.getHackyAccumulatedDataStyles();
709
730
  return { runtime, modelURL, importBaseURL, dataStyles, modelType };
710
731
  }
@@ -19,6 +19,7 @@ import {
19
19
  } from "../constants";
20
20
  import { PackageNotFoundError } from "../errors";
21
21
  import { formatDuration, logger } from "../logger";
22
+ import { BuildManifest } from "../storage/DatabaseInterface";
22
23
  import { Model } from "./model";
23
24
 
24
25
  type ApiDatabase = components["schemas"]["Database"];
@@ -189,6 +190,10 @@ export class Package {
189
190
  return this.packageName;
190
191
  }
191
192
 
193
+ public getPackagePath(): string {
194
+ return this.packagePath;
195
+ }
196
+
192
197
  public getPackageMetadata(): ApiPackage {
193
198
  return this.packageMetadata;
194
199
  }
@@ -201,6 +206,51 @@ export class Package {
201
206
  return this.models.get(modelPath);
202
207
  }
203
208
 
209
+ public getModelPaths(): string[] {
210
+ return Array.from(this.models.keys());
211
+ }
212
+
213
+ /**
214
+ * Recompile every model in the package with the given build manifest
215
+ * so queries resolve persist references to materialized tables.
216
+ *
217
+ * Builds a fresh map off to the side and swaps it in at the end. If any
218
+ * recompile fails the whole call rejects before the swap and the live
219
+ * `this.models` reference remains untouched — no half-loaded state is
220
+ * ever observable to concurrent readers.
221
+ */
222
+ public async reloadAllModels(
223
+ buildManifest: BuildManifest["entries"],
224
+ ): Promise<void> {
225
+ const modelPaths = Array.from(this.models.keys());
226
+ logger.info("Reloading all models with build manifest", {
227
+ packageName: this.packageName,
228
+ modelCount: modelPaths.length,
229
+ manifestEntryCount: Object.keys(buildManifest).length,
230
+ });
231
+
232
+ const reloaded = await Promise.all(
233
+ modelPaths.map((modelPath) =>
234
+ Model.create(
235
+ this.packageName,
236
+ this.packagePath,
237
+ modelPath,
238
+ this.connections,
239
+ { buildManifest },
240
+ ),
241
+ ),
242
+ );
243
+ const nextModels = new Map<string, Model>();
244
+ for (const model of reloaded) {
245
+ nextModels.set(model.getPath(), model);
246
+ }
247
+ this.models = nextModels;
248
+ }
249
+
250
+ public getConnections(): Map<string, Connection> {
251
+ return this.connections;
252
+ }
253
+
204
254
  public getMalloyConnection(connectionName: string): Connection {
205
255
  const connection = this.connections.get(connectionName);
206
256
  if (!connection) {
@@ -320,6 +320,7 @@ export class ProjectStore {
320
320
  };
321
321
  const existingProject = await repository.getProjectByName(projectName);
322
322
 
323
+ let dbProject: { id: string; name: string };
323
324
  if (existingProject) {
324
325
  const updateData = {
325
326
  description: projectDescription,
@@ -327,10 +328,27 @@ export class ProjectStore {
327
328
  };
328
329
 
329
330
  await repository.updateProject(existingProject.id, updateData);
330
- return { id: existingProject.id, name: projectName };
331
+ dbProject = { id: existingProject.id, name: projectName };
331
332
  } else {
332
- return await repository.createProject(projectData);
333
+ dbProject = await repository.createProject(projectData);
333
334
  }
335
+
336
+ // Initialize DuckLake manifest storage if configured on the project.
337
+ const materializationStorage = project.metadata
338
+ ?.materializationStorage as
339
+ | { catalogUrl?: string; dataPath?: string }
340
+ | undefined;
341
+ if (
342
+ materializationStorage?.catalogUrl &&
343
+ materializationStorage?.dataPath
344
+ ) {
345
+ await this.storageManager.initializeDuckLakeForProject(dbProject.id, {
346
+ catalogUrl: materializationStorage.catalogUrl,
347
+ dataPath: materializationStorage.dataPath,
348
+ });
349
+ }
350
+
351
+ return dbProject;
334
352
  }
335
353
 
336
354
  private async addPackages(
@@ -917,6 +935,7 @@ export class ProjectStore {
917
935
  private isLocalPath(location: string) {
918
936
  return (
919
937
  location.startsWith("./") ||
938
+ location.startsWith("../") ||
920
939
  location.startsWith("~/") ||
921
940
  location.startsWith("/") ||
922
941
  path.isAbsolute(location)
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Minimal identifier-quoting surface. Every `Dialect` in `@malloydata/malloy`
3
+ * implements this; we accept the duck type so tests can inject a fake without
4
+ * instantiating a full dialect.
5
+ */
6
+ export interface Quoter {
7
+ quoteTablePath(seg: string): string;
8
+ }
9
+
10
+ /**
11
+ * Quote a potentially schema-qualified table path (e.g. "schema.table")
12
+ * by quoting each segment individually with the dialect's quoteTablePath.
13
+ */
14
+ export function quoteTablePath(path: string, dialect: Quoter): string {
15
+ return path
16
+ .split(".")
17
+ .map((seg) => dialect.quoteTablePath(seg))
18
+ .join(".");
19
+ }
20
+
21
+ /**
22
+ * Split a possibly schema-qualified table name into its schema prefix
23
+ * (including the trailing dot) and the bare table name.
24
+ *
25
+ * Examples:
26
+ * "my_schema.my_table" -> { schemaPrefix: "my_schema.", bareName: "my_table" }
27
+ * "my_table" -> { schemaPrefix: "", bareName: "my_table" }
28
+ */
29
+ export function splitTablePath(tableName: string): {
30
+ schemaPrefix: string;
31
+ bareName: string;
32
+ } {
33
+ const lastDot = tableName.lastIndexOf(".");
34
+ if (lastDot >= 0) {
35
+ return {
36
+ schemaPrefix: tableName.substring(0, lastDot + 1),
37
+ bareName: tableName.substring(lastDot + 1),
38
+ };
39
+ }
40
+ return { schemaPrefix: "", bareName: tableName };
41
+ }
@@ -0,0 +1,13 @@
1
+ import { ProjectNotFoundError } from "../errors";
2
+ import { ResourceRepository } from "../storage/DatabaseInterface";
3
+
4
+ export async function resolveProjectId(
5
+ repository: ResourceRepository,
6
+ projectName: string,
7
+ ): Promise<string> {
8
+ const dbProject = await repository.getProjectByName(projectName);
9
+ if (!dbProject) {
10
+ throw new ProjectNotFoundError(`Project '${projectName}' not found`);
11
+ }
12
+ return dbProject.id;
13
+ }
@@ -34,7 +34,7 @@ export interface ResourceRepository {
34
34
  getConnectionById(id: string): Promise<Connection | null>;
35
35
  getConnectionByName(
36
36
  projectId: string,
37
- id: string,
37
+ name: string,
38
38
  ): Promise<Connection | null>;
39
39
  createConnection(
40
40
  connection: Omit<Connection, "id" | "createdAt" | "updatedAt">,
@@ -44,6 +44,44 @@ export interface ResourceRepository {
44
44
  updates: Partial<Connection>,
45
45
  ): Promise<Connection>;
46
46
  deleteConnection(id: string): Promise<void>;
47
+
48
+ // Materializations
49
+ listMaterializations(
50
+ projectId: string,
51
+ packageName: string,
52
+ options?: { limit?: number; offset?: number },
53
+ ): Promise<Materialization[]>;
54
+ getMaterializationById(id: string): Promise<Materialization | null>;
55
+ getActiveMaterialization(
56
+ projectId: string,
57
+ packageName: string,
58
+ ): Promise<Materialization | null>;
59
+ createMaterialization(
60
+ projectId: string,
61
+ packageName: string,
62
+ status?: MaterializationStatus,
63
+ metadata?: Record<string, unknown> | null,
64
+ ): Promise<Materialization>;
65
+ updateMaterialization(
66
+ id: string,
67
+ updates: {
68
+ status?: MaterializationStatus;
69
+ startedAt?: Date;
70
+ completedAt?: Date;
71
+ error?: string | null;
72
+ metadata?: Record<string, unknown> | null;
73
+ },
74
+ ): Promise<Materialization>;
75
+ deleteMaterialization(id: string): Promise<void>;
76
+ // Build Manifests
77
+ listManifestEntries(
78
+ projectId: string,
79
+ packageName: string,
80
+ ): Promise<ManifestEntry[]>;
81
+ upsertManifestEntry(
82
+ entry: Omit<ManifestEntry, "id" | "createdAt" | "updatedAt">,
83
+ ): Promise<ManifestEntry>;
84
+ deleteManifestEntry(id: string): Promise<void>;
47
85
  }
48
86
 
49
87
  export interface Project {
@@ -76,3 +114,67 @@ export interface Connection {
76
114
  createdAt: Date;
77
115
  updatedAt: Date;
78
116
  }
117
+
118
+ export type MaterializationStatus =
119
+ | "PENDING"
120
+ | "RUNNING"
121
+ | "SUCCESS"
122
+ | "FAILED"
123
+ | "CANCELLED";
124
+
125
+ export interface Materialization {
126
+ id: string;
127
+ projectId: string;
128
+ packageName: string;
129
+ status: MaterializationStatus;
130
+ startedAt: Date | null;
131
+ completedAt: Date | null;
132
+ error: string | null;
133
+ metadata: Record<string, unknown> | null;
134
+ createdAt: Date;
135
+ updatedAt: Date;
136
+ }
137
+
138
+ export interface ManifestEntry {
139
+ id: string;
140
+ projectId: string;
141
+ packageName: string;
142
+ buildId: string;
143
+ tableName: string;
144
+ sourceName: string;
145
+ connectionName: string;
146
+ createdAt: Date;
147
+ updatedAt: Date;
148
+ }
149
+
150
+ // ==================== MANIFEST STORE ====================
151
+
152
+ export interface BuildManifestEntry {
153
+ tableName: string;
154
+ }
155
+
156
+ export interface BuildManifest {
157
+ entries: Record<string, BuildManifestEntry>;
158
+ strict?: boolean;
159
+ }
160
+
161
+ /**
162
+ * Abstraction for manifest storage. Standalone mode uses DuckDB;
163
+ * orchestrated mode swaps in a DuckLakeManifestStore.
164
+ */
165
+ export interface ManifestStore {
166
+ getManifest(projectId: string, packageName: string): Promise<BuildManifest>;
167
+ writeEntry(
168
+ projectId: string,
169
+ packageName: string,
170
+ buildId: string,
171
+ tableName: string,
172
+ sourceName: string,
173
+ connectionName: string,
174
+ ): Promise<void>;
175
+ deleteEntry(id: string): Promise<void>;
176
+ listEntries(
177
+ projectId: string,
178
+ packageName: string,
179
+ ): Promise<ManifestEntry[]>;
180
+ }
@@ -47,4 +47,13 @@ export class StorageManager {
47
47
  deleteConnection: async (): Promise<void> => {},
48
48
  };
49
49
  }
50
+
51
+ getManifestStore() {
52
+ return {
53
+ getManifest: async () => ({ entries: {}, strict: false }),
54
+ writeEntry: async () => {},
55
+ deleteEntry: async () => {},
56
+ listEntries: async () => [],
57
+ };
58
+ }
50
59
  }
@@ -1,11 +1,22 @@
1
1
  import { logger } from "../logger";
2
- import { DatabaseConnection, ResourceRepository } from "./DatabaseInterface";
2
+ import {
3
+ DatabaseConnection,
4
+ ManifestStore,
5
+ ResourceRepository,
6
+ } from "./DatabaseInterface";
3
7
  import { DuckDBConnection } from "./duckdb/DuckDBConnection";
8
+ import { DuckDBManifestStore } from "./duckdb/DuckDBManifestStore";
4
9
  import { DuckDBRepository } from "./duckdb/DuckDBRepository";
5
10
  import { initializeSchema } from "./duckdb/schema";
11
+ import { DuckLakeManifestStore } from "./ducklake/DuckLakeManifestStore";
6
12
 
7
13
  export type StorageType = "duckdb" | "postgres" | "mysql";
8
14
 
15
+ export interface DuckLakeManifestConfig {
16
+ catalogUrl: string;
17
+ dataPath: string;
18
+ }
19
+
9
20
  export interface StorageConfig {
10
21
  type: StorageType;
11
22
  duckdb?: {
@@ -27,9 +38,28 @@ export interface StorageConfig {
27
38
  };
28
39
  }
29
40
 
41
+ function escapeSQL(value: string): string {
42
+ return value.replace(/'/g, "''");
43
+ }
44
+
45
+ /**
46
+ * Manages the storage backend (DuckDB, Postgres, etc.) and per-project
47
+ * manifest stores. Projects without `materializationStorage` config use
48
+ * the default DuckDB manifest store. Projects with the config get a
49
+ * DuckLake-backed store attached lazily on first access.
50
+ */
30
51
  export class StorageManager {
31
52
  private connection: DatabaseConnection | null = null;
53
+ private duckDbConnection: DuckDBConnection | null = null;
32
54
  private repository: ResourceRepository | null = null;
55
+ private defaultManifestStore: ManifestStore | null = null;
56
+
57
+ /** Per-project DuckLake manifest stores, keyed by projectId. */
58
+ private projectManifestStores = new Map<string, ManifestStore>();
59
+
60
+ /** Tracks which catalogs have been attached to avoid duplicate ATTACHes. */
61
+ private attachedCatalogs = new Set<string>();
62
+
33
63
  private config: StorageConfig;
34
64
 
35
65
  constructor(config: StorageConfig) {
@@ -68,7 +98,73 @@ export class StorageManager {
68
98
  await initializeSchema(connection, reinit);
69
99
 
70
100
  this.connection = connection;
101
+ this.duckDbConnection = connection;
71
102
  this.repository = new DuckDBRepository(connection);
103
+ this.defaultManifestStore = new DuckDBManifestStore(this.repository);
104
+ }
105
+
106
+ /**
107
+ * Lazily initializes a DuckLake manifest store for a project.
108
+ * The DuckLake catalog is attached to the shared DuckDB connection
109
+ * and persists for the lifetime of the process.
110
+ */
111
+ async initializeDuckLakeForProject(
112
+ projectId: string,
113
+ config: DuckLakeManifestConfig,
114
+ ): Promise<void> {
115
+ if (!this.duckDbConnection) {
116
+ throw new Error("Storage not initialized. Call initialize() first.");
117
+ }
118
+
119
+ const catalogName = `manifest_lake_${projectId.replace(/[^a-zA-Z0-9_]/g, "_")}`;
120
+
121
+ if (!this.attachedCatalogs.has(catalogName)) {
122
+ await this.attachDuckLakeCatalog(config, catalogName);
123
+ }
124
+
125
+ const store = new DuckLakeManifestStore(
126
+ this.duckDbConnection,
127
+ catalogName,
128
+ );
129
+ await store.bootstrapSchema();
130
+
131
+ this.projectManifestStores.set(projectId, store);
132
+ logger.info("DuckLake manifest store initialized for project", {
133
+ projectId,
134
+ catalogName,
135
+ });
136
+ }
137
+
138
+ private async attachDuckLakeCatalog(
139
+ config: DuckLakeManifestConfig,
140
+ catalogName: string,
141
+ ): Promise<void> {
142
+ const connection = this.duckDbConnection!;
143
+
144
+ await connection.run("INSTALL ducklake; LOAD ducklake;");
145
+
146
+ const isPostgres = config.catalogUrl.startsWith("postgres:");
147
+ if (isPostgres) {
148
+ await connection.run("INSTALL postgres; LOAD postgres;");
149
+ }
150
+
151
+ const escapedCatalogUrl = escapeSQL(config.catalogUrl);
152
+ const escapedDataPath = escapeSQL(config.dataPath);
153
+ const isCloudStorage =
154
+ config.dataPath.startsWith("gs://") ||
155
+ config.dataPath.startsWith("s3://");
156
+
157
+ let attachCmd = `ATTACH 'ducklake:${escapedCatalogUrl}' AS ${catalogName}`;
158
+ const attachOpts: string[] = [`DATA_PATH '${escapedDataPath}'`];
159
+ if (isCloudStorage) {
160
+ attachOpts.push("OVERRIDE_DATA_PATH true");
161
+ }
162
+ attachCmd += ` (${attachOpts.join(", ")});`;
163
+
164
+ logger.info(`Attaching DuckLake manifest catalog: ${attachCmd}`);
165
+ await connection.run(attachCmd);
166
+
167
+ this.attachedCatalogs.add(catalogName);
72
168
  }
73
169
 
74
170
  getRepository(): ResourceRepository {
@@ -78,11 +174,33 @@ export class StorageManager {
78
174
  return this.repository;
79
175
  }
80
176
 
177
+ /**
178
+ * Returns the manifest store for a project. If the project has a
179
+ * DuckLake store configured, returns that; otherwise returns the
180
+ * default DuckDB-backed store.
181
+ */
182
+ getManifestStore(projectId?: string): ManifestStore {
183
+ if (projectId) {
184
+ const projectStore = this.projectManifestStores.get(projectId);
185
+ if (projectStore) {
186
+ return projectStore;
187
+ }
188
+ }
189
+ if (!this.defaultManifestStore) {
190
+ throw new Error("Storage not initialized. Call initialize() first.");
191
+ }
192
+ return this.defaultManifestStore;
193
+ }
194
+
81
195
  async close(): Promise<void> {
82
196
  if (this.connection) {
83
197
  await this.connection.close();
84
198
  this.connection = null;
199
+ this.duckDbConnection = null;
85
200
  this.repository = null;
201
+ this.defaultManifestStore = null;
202
+ this.projectManifestStores.clear();
203
+ this.attachedCatalogs.clear();
86
204
  }
87
205
  }
88
206
 
@@ -0,0 +1,70 @@
1
+ import {
2
+ BuildManifest,
3
+ ManifestEntry,
4
+ ManifestStore,
5
+ ResourceRepository,
6
+ } from "../DatabaseInterface";
7
+
8
+ /**
9
+ * DuckDB-backed ManifestStore that delegates to the local ResourceRepository.
10
+ *
11
+ * In standalone mode this is the active store. Orchestrated deployments swap
12
+ * in a DuckLake-backed implementation that reads/writes manifests through the
13
+ * shared catalog instead of the local DuckDB build_manifests table.
14
+ */
15
+ export class DuckDBManifestStore implements ManifestStore {
16
+ constructor(private repository: ResourceRepository) {}
17
+
18
+ /**
19
+ * Assembles a {@link BuildManifest} by folding all manifest rows for the
20
+ * given package into a build-ID-keyed map. `strict: false` lets the Malloy
21
+ * runtime fall back to executing the underlying query when a persist
22
+ * reference has no manifest entry (e.g. before the first materialization).
23
+ */
24
+ async getManifest(
25
+ projectId: string,
26
+ packageName: string,
27
+ ): Promise<BuildManifest> {
28
+ const entries = await this.repository.listManifestEntries(
29
+ projectId,
30
+ packageName,
31
+ );
32
+ const manifest: BuildManifest = {
33
+ entries: {},
34
+ strict: false,
35
+ };
36
+ for (const entry of entries) {
37
+ manifest.entries[entry.buildId] = { tableName: entry.tableName };
38
+ }
39
+ return manifest;
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.repository.upsertManifestEntry({
51
+ projectId,
52
+ packageName,
53
+ buildId,
54
+ tableName,
55
+ sourceName,
56
+ connectionName,
57
+ });
58
+ }
59
+
60
+ async deleteEntry(id: string): Promise<void> {
61
+ await this.repository.deleteManifestEntry(id);
62
+ }
63
+
64
+ async listEntries(
65
+ projectId: string,
66
+ packageName: string,
67
+ ): Promise<ManifestEntry[]> {
68
+ return this.repository.listManifestEntries(projectId, packageName);
69
+ }
70
+ }