@malloy-publisher/server 0.0.204 → 0.0.206

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 (100) hide show
  1. package/build.ts +10 -1
  2. package/dist/app/api-doc.yaml +494 -397
  3. package/dist/app/assets/{EnvironmentPage-CX06cjOF.js → EnvironmentPage-BYwBeC2F.js} +1 -1
  4. package/dist/app/assets/HomePage-ivu4vdpj.js +1 -0
  5. package/dist/app/assets/{MainPage-nUJ9YatG.js → MainPage-B2DnHEDU.js} +2 -2
  6. package/dist/app/assets/MaterializationsPage-BZEuwF9P.js +1 -0
  7. package/dist/app/assets/ModelPage-Dpu3bfPg.js +1 -0
  8. package/dist/app/assets/{PackagePage-BaEVdEAG.js → PackagePage-B8PwcRHt.js} +1 -1
  9. package/dist/app/assets/{RouteError-BShQjZio.js → RouteError-BhbywAeC.js} +1 -1
  10. package/dist/app/assets/{WorkbookPage-CBn6ZjJW.js → WorkbookPage-C-JXsJG0.js} +1 -1
  11. package/dist/app/assets/{core-DECXYL4E.es-OaRfXwuQ.js → core-pPlPr7jK.es-CNEOlxKB.js} +1 -1
  12. package/dist/app/assets/{index-BLfPC1gy.js → index-BHEm8Egc.js} +1 -1
  13. package/dist/app/assets/{index-Dy3YhAZQ.js → index-BsvDrV14.js} +1 -1
  14. package/dist/app/assets/{index-DqiJ0bWp.js → index-ChR1fKR2.js} +134 -134
  15. package/dist/app/assets/{index.umd-DAN9K8yC.js → index.umd-BVLPYNuj.js} +1 -1
  16. package/dist/app/index.html +1 -1
  17. package/dist/instrumentation.mjs +18 -8
  18. package/dist/package_load_worker.mjs +19 -2
  19. package/dist/runtime/publisher.js +318 -0
  20. package/dist/server.mjs +1703 -1443
  21. package/package.json +5 -4
  22. package/scripts/bake-duckdb-extensions.js +104 -0
  23. package/src/constants.ts +12 -0
  24. package/src/controller/materialization.controller.ts +79 -35
  25. package/src/controller/package.controller.spec.ts +179 -0
  26. package/src/controller/package.controller.ts +60 -73
  27. package/src/controller/watch-mode.controller.ts +176 -46
  28. package/src/dto/package.dto.ts +16 -1
  29. package/src/errors.spec.ts +33 -0
  30. package/src/errors.ts +18 -0
  31. package/src/health.spec.ts +34 -1
  32. package/src/instrumentation.ts +33 -17
  33. package/src/materialization_metrics.ts +66 -0
  34. package/src/mcp/error_messages.spec.ts +35 -0
  35. package/src/mcp/error_messages.ts +14 -1
  36. package/src/mcp/handler_utils.ts +12 -0
  37. package/src/package_load/package_load_pool.ts +7 -1
  38. package/src/package_load/package_load_worker.ts +44 -4
  39. package/src/package_load/protocol.ts +7 -1
  40. package/src/runtime/publisher.js +318 -0
  41. package/src/server-old.ts +7 -149
  42. package/src/server.ts +488 -190
  43. package/src/service/authorize_integration.spec.ts +163 -2
  44. package/src/service/compile_authorize.spec.ts +85 -0
  45. package/src/service/environment.ts +270 -12
  46. package/src/service/environment_store.spec.ts +0 -81
  47. package/src/service/environment_store.ts +142 -34
  48. package/src/service/explore_visibility.spec.ts +434 -0
  49. package/src/service/exports_probe.spec.ts +107 -0
  50. package/src/service/manifest_loader.spec.ts +99 -0
  51. package/src/service/manifest_loader.ts +95 -0
  52. package/src/service/materialization_service.spec.ts +324 -512
  53. package/src/service/materialization_service.ts +816 -656
  54. package/src/service/model.ts +444 -13
  55. package/src/service/package.spec.ts +14 -2
  56. package/src/service/package.ts +271 -20
  57. package/src/service/package_rollback.spec.ts +190 -0
  58. package/src/service/package_worker_path.spec.ts +223 -0
  59. package/src/service/query_boundary.spec.ts +470 -0
  60. package/src/storage/DatabaseInterface.ts +35 -57
  61. package/src/storage/StorageManager.mock.ts +0 -9
  62. package/src/storage/StorageManager.ts +7 -290
  63. package/src/storage/duckdb/DuckDBConnection.ts +70 -124
  64. package/src/storage/duckdb/DuckDBRepository.ts +2 -35
  65. package/src/storage/duckdb/MaterializationRepository.ts +52 -27
  66. package/src/storage/duckdb/schema.ts +4 -20
  67. package/tests/fixtures/authorize-compile/model.malloy +9 -0
  68. package/tests/fixtures/authorize-compile/publisher.json +4 -0
  69. package/tests/fixtures/html-pages-nopublic/model.malloy +1 -0
  70. package/tests/fixtures/html-pages-nopublic/publisher.json +5 -0
  71. package/tests/fixtures/html-pages-test/data.csv +3 -0
  72. package/tests/fixtures/html-pages-test/public/assets/app.css +3 -0
  73. package/tests/fixtures/html-pages-test/public/data.json +1 -0
  74. package/tests/fixtures/html-pages-test/public/index.html +9 -0
  75. package/tests/fixtures/html-pages-test/public/sub/page2.html +9 -0
  76. package/tests/fixtures/html-pages-test/publisher.json +5 -0
  77. package/tests/fixtures/html-pages-test/report.malloy +1 -0
  78. package/tests/integration/authorize/compile_authorize_http.integration.spec.ts +92 -0
  79. package/tests/integration/duckdb_storage/duckdb_storage.integration.spec.ts +138 -0
  80. package/tests/integration/html_pages/html_pages.integration.spec.ts +378 -0
  81. package/tests/integration/materialization/manifest_binding.integration.spec.ts +175 -0
  82. package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +277 -266
  83. package/tests/integration/watch-mode/watch_mode.integration.spec.ts +421 -0
  84. package/tests/unit/duckdb/attached_databases.test.ts +111 -0
  85. package/tests/unit/duckdb/duckdb_connection.test.ts +181 -0
  86. package/tests/unit/duckdb/legacy_schema_migration.test.ts +7 -4
  87. package/tests/unit/duckdb/repositories.test.ts +208 -0
  88. package/dist/app/assets/HomePage-CNFt_eUU.js +0 -1
  89. package/dist/app/assets/MaterializationsPage-B5goxVXW.js +0 -1
  90. package/dist/app/assets/ModelPage-Ba7Xh4lL.js +0 -1
  91. package/src/controller/manifest.controller.ts +0 -38
  92. package/src/service/manifest_service.spec.ts +0 -206
  93. package/src/service/manifest_service.ts +0 -117
  94. package/src/service/materialized_table_gc.spec.ts +0 -384
  95. package/src/service/materialized_table_gc.ts +0 -231
  96. package/src/storage/duckdb/DuckDBManifestStore.ts +0 -70
  97. package/src/storage/duckdb/ManifestRepository.ts +0 -120
  98. package/src/storage/duckdb/manifest_store.spec.ts +0 -133
  99. package/src/storage/ducklake/DuckLakeManifestStore.ts +0 -155
  100. package/tests/unit/storage/StorageManager.test.ts +0 -166
@@ -1,35 +1,11 @@
1
- import { Mutex } from "async-mutex";
2
- import * as crypto from "crypto";
3
- import {
4
- isCatalogVersionSupported,
5
- SUPPORTED_CATALOG_VERSIONS,
6
- } from "../ducklake_version";
7
- import { ConnectionAuthError } from "../errors";
8
1
  import { logger } from "../logger";
9
- import {
10
- handlePgAttachError,
11
- pgConnectTimeoutSeconds,
12
- redactPgSecrets,
13
- withPgConnectTimeout,
14
- } from "../pg_helpers";
15
- import {
16
- DatabaseConnection,
17
- ManifestStore,
18
- ResourceRepository,
19
- } from "./DatabaseInterface";
2
+ import { DatabaseConnection, ResourceRepository } from "./DatabaseInterface";
20
3
  import { DuckDBConnection } from "./duckdb/DuckDBConnection";
21
- import { DuckDBManifestStore } from "./duckdb/DuckDBManifestStore";
22
4
  import { DuckDBRepository } from "./duckdb/DuckDBRepository";
23
5
  import { initializeSchema } from "./duckdb/schema";
24
- import { DuckLakeManifestStore } from "./ducklake/DuckLakeManifestStore";
25
6
 
26
7
  export type StorageType = "duckdb" | "postgres" | "mysql";
27
8
 
28
- export interface DuckLakeManifestConfig {
29
- catalogUrl: string;
30
- dataPath: string;
31
- }
32
-
33
9
  export interface StorageConfig {
34
10
  type: StorageType;
35
11
  duckdb?: {
@@ -51,98 +27,16 @@ export interface StorageConfig {
51
27
  };
52
28
  }
53
29
 
54
- function escapeSQL(value: string): string {
55
- return value.replace(/'/g, "''");
56
- }
57
-
58
- function configKey(c: DuckLakeManifestConfig): string {
59
- return `${c.catalogUrl}|${c.dataPath}`;
60
- }
61
-
62
- function catalogNameForConfig(c: DuckLakeManifestConfig): string {
63
- const hash = crypto
64
- .createHash("sha256")
65
- .update(configKey(c))
66
- .digest("hex")
67
- .slice(0, 8);
68
- return `manifest_lake_${hash}`;
69
- }
70
-
71
- // Read the catalog's recorded DuckLake format version from its
72
- // `ducklake_metadata` table via a plain postgres ATTACH (does NOT invoke
73
- // the DuckLake extension on the catalog). Returns the version string on
74
- // success, or `undefined` on any failure (missing table, query timeout,
75
- // connect failure) so the main ATTACH path stays the source of truth for
76
- // unrelated errors. Only meaningful for postgres-backed catalogs; the
77
- // caller must guard with `isPostgres`.
78
- async function readDuckLakeCatalogVersion(
79
- connection: DuckDBConnection,
80
- catalogUrl: string,
81
- catalogName: string,
82
- ): Promise<string | undefined> {
83
- if (!catalogUrl.startsWith("postgres:")) {
84
- return undefined;
85
- }
86
- const pgConnString = catalogUrl.slice("postgres:".length);
87
- const tempDb = `${catalogName}_preflight`;
88
- const escaped = escapeSQL(pgConnString);
89
- try {
90
- await connection.run(
91
- `ATTACH '${escaped}' AS ${tempDb} (TYPE postgres, READ_ONLY);`,
92
- );
93
- const rows = await connection.all<{ value: string }>(
94
- `SELECT value FROM ${tempDb}.ducklake_metadata WHERE key = 'version' LIMIT 1;`,
95
- );
96
- const value = rows[0]?.value;
97
- return typeof value === "string" ? value : undefined;
98
- } catch (error) {
99
- logger.warn(
100
- "DuckLake catalog version preflight failed; falling back to ATTACH",
101
- {
102
- catalogName,
103
- error: redactPgSecrets(
104
- error instanceof Error ? error.message : String(error),
105
- ),
106
- },
107
- );
108
- return undefined;
109
- } finally {
110
- try {
111
- await connection.run(`DETACH ${tempDb};`);
112
- } catch {
113
- // ATTACH may have failed, so DETACH may have nothing to do.
114
- }
115
- }
116
- }
117
-
118
30
  /**
119
- * Manages the storage backend (DuckDB, Postgres, etc.) and per-environment
120
- * manifest stores. Environments without `materializationStorage` config use
121
- * the default DuckDB manifest store. Environments with the config get a
122
- * DuckLake-backed store attached lazily on first access.
31
+ * Manages the storage backend (DuckDB today) and exposes the resource
32
+ * repository. The publisher persists only its own control records
33
+ * (environments, packages, connections, materializations) here; build
34
+ * manifests are returned inline on the materialization resource and persisted
35
+ * by the control plane.
123
36
  */
124
37
  export class StorageManager {
125
38
  private connection: DatabaseConnection | null = null;
126
- private duckDbConnection: DuckDBConnection | null = null;
127
39
  private repository: ResourceRepository | null = null;
128
- private defaultManifestStore: ManifestStore | null = null;
129
-
130
- /** Per-environment DuckLake manifest stores, keyed by environmentId. */
131
- private environmentManifestStores = new Map<string, ManifestStore>();
132
-
133
- /**
134
- * Tracks attached DuckLake catalogs as `configKey -> catalogName`. Each
135
- * unique materializationStorage config gets its own ATTACHment under a
136
- * deterministic catalog name, so multiple configs can coexist on one worker.
137
- */
138
- private attachedCatalogs = new Map<string, string>();
139
-
140
- // Serializes DuckLake catalog attaches. Concurrent POST /environments calls
141
- // hitting the same DuckDB connection would otherwise race on extension
142
- // autoload (httpfs/azure/etc.), where multiple connections download the
143
- // extension to `.tmp-<uuid>` files in parallel; only one wins the rename
144
- // and the rest crash with "Could not remove file ... No such file or directory".
145
- private duckLakeAttachMutex: Mutex = new Mutex();
146
40
 
147
41
  private config: StorageConfig;
148
42
 
@@ -173,166 +67,12 @@ export class StorageManager {
173
67
  }
174
68
 
175
69
  private async initializeDuckDB(reinit: boolean): Promise<void> {
176
- const dbPath = this.config.duckdb?.path;
177
-
178
- const connection = new DuckDBConnection(dbPath);
179
-
70
+ const connection = new DuckDBConnection(this.config.duckdb?.path);
180
71
  await connection.initialize();
181
-
182
72
  await initializeSchema(connection, reinit);
183
73
 
184
74
  this.connection = connection;
185
- this.duckDbConnection = connection;
186
75
  this.repository = new DuckDBRepository(connection);
187
- this.defaultManifestStore = new DuckDBManifestStore(this.repository);
188
- }
189
-
190
- /**
191
- * Lazily initializes a DuckLake manifest store for an environment.
192
- *
193
- * One shared catalog per materializationStorage config: every environment
194
- * pointing at the same (catalogUrl, dataPath) shares one `build_manifests`
195
- * table inside it, partitioned by `environment_id` (set to the environment's name
196
- * so it's stable across worker replicas — required for cross-pod manifest
197
- * visibility in orchestrated mode). Different configs (e.g. different
198
- * orgs) attach as separate catalogs under distinct deterministic aliases.
199
- */
200
- async initializeDuckLakeForEnvironment(
201
- environmentId: string,
202
- environmentName: string,
203
- config: DuckLakeManifestConfig,
204
- ): Promise<void> {
205
- if (!this.duckDbConnection) {
206
- throw new Error("Storage not initialized. Call initialize() first.");
207
- }
208
-
209
- const key = configKey(config);
210
- const catalogName = await this.duckLakeAttachMutex.runExclusive(
211
- async () => {
212
- const existing = this.attachedCatalogs.get(key);
213
- if (existing) return existing;
214
- // Catalog name derived from the config so multiple configs can coexist as
215
- // separate ATTACHments without colliding on the name.
216
- const name = catalogNameForConfig(config);
217
- await this.attachDuckLakeCatalog(config, name);
218
- this.attachedCatalogs.set(key, name);
219
- return name;
220
- },
221
- );
222
-
223
- const store = new DuckLakeManifestStore(
224
- this.duckDbConnection,
225
- catalogName,
226
- environmentName,
227
- );
228
- await store.bootstrapSchema();
229
-
230
- this.environmentManifestStores.set(environmentId, store);
231
- logger.info("DuckLake manifest store initialized for environment", {
232
- environmentId,
233
- environmentName,
234
- catalogName,
235
- });
236
- }
237
-
238
- private async attachDuckLakeCatalog(
239
- config: DuckLakeManifestConfig,
240
- catalogName: string,
241
- ): Promise<void> {
242
- const connection = this.duckDbConnection!;
243
-
244
- await connection.run("INSTALL ducklake; LOAD ducklake;");
245
-
246
- const isPostgres = config.catalogUrl.startsWith("postgres:");
247
- if (isPostgres) {
248
- await connection.run("INSTALL postgres; LOAD postgres;");
249
- }
250
-
251
- // For PG-backed catalogs, inject connect_timeout so a wedged libpq
252
- // handshake fails the caller in seconds rather than hanging the
253
- // worker until the K8s liveness probe trips (the 2026-05 incident).
254
- // Non-PG catalogs (e.g. SQLite, MySQL) pass through unchanged.
255
- const catalogUrl = isPostgres
256
- ? withPgConnectTimeout(config.catalogUrl, pgConnectTimeoutSeconds())
257
- : config.catalogUrl;
258
-
259
- const escapedCatalogUrl = escapeSQL(catalogUrl);
260
- const escapedDataPath = escapeSQL(config.dataPath);
261
- const isCloudStorage =
262
- config.dataPath.startsWith("gs://") ||
263
- config.dataPath.startsWith("s3://");
264
-
265
- // Pre-install httpfs explicitly so the ATTACH below doesn't trigger
266
- // DuckDB's autoloader. The autoloader downloads extensions to
267
- // `<ext>.tmp-<uuid>` and races when multiple connections within the
268
- // same process hit it concurrently — losers fail with
269
- // "Could not remove file ... No such file or directory" on cleanup
270
- // of their .tmp file. INSTALL/LOAD here is idempotent and serialized
271
- // by the caller's mutex.
272
- if (isCloudStorage) {
273
- await connection.run("INSTALL httpfs; LOAD httpfs;");
274
- }
275
-
276
- // Preflight: read the catalog's recorded format version via the
277
- // postgres extension (not DuckLake) and fail fast with a non-retryable
278
- // 422 if the baked DuckLake extension can't read it. Without this,
279
- // an unsupported catalog would surface as a generic DuckDB error
280
- // from the ATTACH below, which retry loops misclassify as transient.
281
- if (isPostgres) {
282
- const catalogVersion = await readDuckLakeCatalogVersion(
283
- connection,
284
- catalogUrl,
285
- catalogName,
286
- );
287
- if (catalogVersion && !isCatalogVersionSupported(catalogVersion)) {
288
- const supportedMax =
289
- SUPPORTED_CATALOG_VERSIONS[
290
- SUPPORTED_CATALOG_VERSIONS.length - 1
291
- ];
292
- throw new ConnectionAuthError(
293
- `DuckLake catalog version ${catalogVersion} is newer than this Publisher's extension supports (max ${supportedMax}). Upgrade the Publisher image or downgrade the catalog.`,
294
- );
295
- }
296
- }
297
-
298
- let attachCmd = `ATTACH 'ducklake:${escapedCatalogUrl}' AS ${catalogName}`;
299
- const attachOpts: string[] = [
300
- `DATA_PATH '${escapedDataPath}'`,
301
- // The manifest table is small relational metadata (one row per build).
302
- // Set a high inlining limit so writes always land transactionally in
303
- // the postgres catalog rather than as parquet files in object storage,
304
- // sidestepping object-storage auth issues entirely for this path.
305
- "DATA_INLINING_ROW_LIMIT 100000",
306
- ];
307
-
308
- if (isCloudStorage) {
309
- attachOpts.push("OVERRIDE_DATA_PATH true");
310
- }
311
- attachCmd += ` (${attachOpts.join(", ")});`;
312
-
313
- logger.info(
314
- `Attaching DuckLake manifest catalog: ${redactPgSecrets(attachCmd)}`,
315
- );
316
- try {
317
- await connection.run(attachCmd);
318
- } catch (error) {
319
- const outcome = handlePgAttachError(
320
- error,
321
- `DuckLake catalog credentials rejected for ${catalogName}`,
322
- );
323
- if (outcome.action === "swallow") {
324
- logger.info(
325
- `DuckLake catalog ${catalogName} is already attached, skipping`,
326
- );
327
- return;
328
- }
329
- if (outcome.error instanceof ConnectionAuthError) {
330
- logger.warn("DuckLake catalog credentials rejected", {
331
- catalogName,
332
- });
333
- }
334
- throw outcome.error;
335
- }
336
76
  }
337
77
 
338
78
  getRepository(): ResourceRepository {
@@ -342,34 +82,11 @@ export class StorageManager {
342
82
  return this.repository;
343
83
  }
344
84
 
345
- /**
346
- * Returns the manifest store for an environment. If the environment has a
347
- * DuckLake store configured, returns that; otherwise returns the
348
- * default DuckDB-backed store.
349
- */
350
- getManifestStore(environmentId?: string): ManifestStore {
351
- if (environmentId) {
352
- const environmentStore =
353
- this.environmentManifestStores.get(environmentId);
354
- if (environmentStore) {
355
- return environmentStore;
356
- }
357
- }
358
- if (!this.defaultManifestStore) {
359
- throw new Error("Storage not initialized. Call initialize() first.");
360
- }
361
- return this.defaultManifestStore;
362
- }
363
-
364
85
  async close(): Promise<void> {
365
86
  if (this.connection) {
366
87
  await this.connection.close();
367
88
  this.connection = null;
368
- this.duckDbConnection = null;
369
89
  this.repository = null;
370
- this.defaultManifestStore = null;
371
- this.environmentManifestStores.clear();
372
- this.attachedCatalogs.clear();
373
90
  }
374
91
  }
375
92
 
@@ -1,11 +1,30 @@
1
1
  import { Mutex } from "async-mutex";
2
- import duckdb from "duckdb";
2
+ import {
3
+ DuckDBConnection as NeoConnection,
4
+ DuckDBInstance,
5
+ type DuckDBValue,
6
+ } from "@duckdb/node-api";
3
7
  import * as path from "path";
4
8
  import { DatabaseConnection } from "../DatabaseInterface";
5
9
 
10
+ /**
11
+ * Embedded persistence layer for the publisher's own metadata (environments,
12
+ * packages, connections, materializations, build manifests) in `publisher.db`.
13
+ *
14
+ * This is a plain DAO over a durable, exclusively-owned DuckDB handle -- it is
15
+ * deliberately NOT Malloy's `@malloydata/db-duckdb` connection, which is an
16
+ * analytical query connection (no prepared-statement parameter binding, a
17
+ * `:memory:` primary with ATTACH/DETACH/idle lifecycle, pooled/shared
18
+ * instances, and a poison-pill close). Those semantics are wrong for a
19
+ * source-of-truth store that must hold one handle open for the server's
20
+ * lifetime and run parameterized CRUD.
21
+ *
22
+ * It wraps `@duckdb/node-api` (the same DuckDB engine Malloy pulls in), so the
23
+ * repo carries a single DuckDB engine rather than a second, redundant driver.
24
+ */
6
25
  export class DuckDBConnection implements DatabaseConnection {
7
- private db: duckdb.Database | null = null;
8
- private connection: duckdb.Connection | null = null;
26
+ private instance: DuckDBInstance | null = null;
27
+ private connection: NeoConnection | null = null;
9
28
  private dbPath: string;
10
29
  private mutex: Mutex = new Mutex();
11
30
 
@@ -15,94 +34,47 @@ export class DuckDBConnection implements DatabaseConnection {
15
34
  }
16
35
 
17
36
  async initialize(): Promise<void> {
18
- return new Promise((resolve, reject) => {
19
- this.db = new duckdb.Database(this.dbPath, {}, (err) => {
20
- if (err) {
21
- console.error("Failed to create DuckDB database:", err);
22
- reject(new Error(`Failed to initialize DuckDB: ${err.message}`));
23
- return;
24
- }
25
-
26
- // Connect synchronously
27
- this.connection = (
28
- this.db as duckdb.Database & { connect(): duckdb.Connection }
29
- ).connect();
30
-
31
- if (!this.connection) {
32
- reject(new Error("Failed to create connection object"));
33
- return;
34
- }
35
-
36
- // Verify connection works
37
- this.connection.all("SELECT 42 as answer", (testErr, _rows) => {
38
- if (testErr) {
39
- console.error("Connection test failed:", testErr);
40
- reject(
41
- new Error(
42
- `Failed to verify DuckDB connection: ${testErr.message}`,
43
- ),
44
- );
45
- return;
46
- }
47
-
48
- resolve();
49
- });
50
- });
51
- });
37
+ try {
38
+ this.instance = await DuckDBInstance.create(this.dbPath);
39
+ this.connection = await this.instance.connect();
40
+ // Verify the connection works
41
+ await this.connection.run("SELECT 42 as answer");
42
+ } catch (err) {
43
+ const message = err instanceof Error ? err.message : String(err);
44
+ console.error("Failed to create DuckDB database:", err);
45
+ throw new Error(`Failed to initialize DuckDB: ${message}`);
46
+ }
52
47
  }
53
48
 
54
49
  async close(): Promise<void> {
55
- return new Promise((resolve, reject) => {
50
+ try {
56
51
  if (this.connection) {
57
- this.connection.close((err) => {
58
- if (err) {
59
- reject(
60
- new Error(
61
- `Failed to close DuckDB connection: ${err.message}`,
62
- ),
63
- );
64
- return;
65
- }
66
-
67
- if (this.db) {
68
- this.db.close((dbErr) => {
69
- if (dbErr) {
70
- reject(
71
- new Error(
72
- `Failed to close DuckDB: ${dbErr.message}`,
73
- ),
74
- );
75
- return;
76
- }
77
- console.log("DuckDB connection closed");
78
- resolve();
79
- });
80
- } else {
81
- resolve();
82
- }
83
- });
84
- } else {
85
- resolve();
52
+ this.connection.closeSync();
53
+ this.connection = null;
86
54
  }
87
- });
55
+ if (this.instance) {
56
+ this.instance.closeSync();
57
+ this.instance = null;
58
+ }
59
+ console.log("DuckDB connection closed");
60
+ } catch (err) {
61
+ const message = err instanceof Error ? err.message : String(err);
62
+ throw new Error(`Failed to close DuckDB connection: ${message}`);
63
+ }
88
64
  }
89
65
 
90
66
  async isInitialized(): Promise<boolean> {
91
67
  if (!this.connection) return false;
92
68
 
93
69
  return this.mutex.runExclusive(async () => {
94
- return new Promise<boolean>((resolve) => {
95
- this.connection!.all(
70
+ try {
71
+ const reader = await this.connection!.runAndReadAll(
96
72
  "SELECT name FROM sqlite_master WHERE type='table' AND name='environments'",
97
- (err, rows) => {
98
- if (err) {
99
- resolve(false);
100
- return;
101
- }
102
- resolve(rows && rows.length > 0);
103
- },
104
73
  );
105
- });
74
+ return reader.getRowObjectsJS().length > 0;
75
+ } catch {
76
+ return false;
77
+ }
106
78
  });
107
79
  }
108
80
 
@@ -112,26 +84,14 @@ export class DuckDBConnection implements DatabaseConnection {
112
84
  }
113
85
 
114
86
  return this.mutex.runExclusive(async () => {
115
- return new Promise<void>((resolve, reject) => {
116
- const callback = (err: Error | null) => {
117
- if (err) {
118
- reject(
119
- new Error(
120
- `Query execution failed: ${err.message}\nQuery: ${query}`,
121
- ),
122
- );
123
- return;
124
- }
125
- resolve();
126
- };
127
-
128
- // Pass params directly without the params argument if empty
129
- if (params && params.length > 0) {
130
- this.connection!.run(query, ...params, callback);
131
- } else {
132
- this.connection!.run(query, callback);
133
- }
134
- });
87
+ try {
88
+ await this.connection!.run(query, params as DuckDBValue[]);
89
+ } catch (err) {
90
+ const message = err instanceof Error ? err.message : String(err);
91
+ throw new Error(
92
+ `Query execution failed: ${message}\nQuery: ${query}`,
93
+ );
94
+ }
135
95
  });
136
96
  }
137
97
 
@@ -141,25 +101,18 @@ export class DuckDBConnection implements DatabaseConnection {
141
101
  }
142
102
 
143
103
  return this.mutex.runExclusive(async () => {
144
- return new Promise<T[]>((resolve, reject) => {
145
- const callback = (err: Error | null, rows: unknown[]) => {
146
- if (err) {
147
- reject(
148
- new Error(
149
- `Query execution failed: ${err.message}\nQuery: ${query}`,
150
- ),
151
- );
152
- return;
153
- }
154
- resolve((rows || []) as T[]);
155
- };
156
-
157
- if (params && params.length > 0) {
158
- this.connection!.all(query, ...params, callback);
159
- } else {
160
- this.connection!.all(query, callback);
161
- }
162
- });
104
+ try {
105
+ const reader = await this.connection!.runAndReadAll(
106
+ query,
107
+ params as DuckDBValue[],
108
+ );
109
+ return reader.getRowObjectsJS() as T[];
110
+ } catch (err) {
111
+ const message = err instanceof Error ? err.message : String(err);
112
+ throw new Error(
113
+ `Query execution failed: ${message}\nQuery: ${query}`,
114
+ );
115
+ }
163
116
  });
164
117
  }
165
118
 
@@ -167,11 +120,4 @@ export class DuckDBConnection implements DatabaseConnection {
167
120
  const rows = await this.all<T>(query, params);
168
121
  return rows.length > 0 ? rows[0] : null;
169
122
  }
170
-
171
- getConnection(): duckdb.Connection {
172
- if (!this.connection) {
173
- throw new Error("Database not initialized");
174
- }
175
- return this.connection;
176
- }
177
123
  }
@@ -1,16 +1,15 @@
1
1
  import {
2
2
  Connection,
3
3
  Environment,
4
- ManifestEntry,
5
4
  Materialization,
6
5
  MaterializationStatus,
6
+ MaterializationUpdate,
7
7
  Package,
8
8
  ResourceRepository,
9
9
  } from "../DatabaseInterface";
10
10
  import { ConnectionRepository } from "./ConnectionRepository";
11
11
  import { DuckDBConnection } from "./DuckDBConnection";
12
12
  import { EnvironmentRepository } from "./EnvironmentRepository";
13
- import { ManifestRepository } from "./ManifestRepository";
14
13
  import { MaterializationRepository } from "./MaterializationRepository";
15
14
  import { PackageRepository } from "./PackageRepository";
16
15
 
@@ -19,14 +18,12 @@ export class DuckDBRepository implements ResourceRepository {
19
18
  private packageRepo: PackageRepository;
20
19
  private connectionRepo: ConnectionRepository;
21
20
  private materializationRepo: MaterializationRepository;
22
- private manifestRepo: ManifestRepository;
23
21
 
24
22
  constructor(public db: DuckDBConnection) {
25
23
  this.environmentRepo = new EnvironmentRepository(db);
26
24
  this.packageRepo = new PackageRepository(db);
27
25
  this.connectionRepo = new ConnectionRepository(db);
28
26
  this.materializationRepo = new MaterializationRepository(db);
29
- this.manifestRepo = new ManifestRepository(db);
30
27
  }
31
28
 
32
29
  // ==================== ENVIRONMENTS ====================
@@ -57,7 +54,6 @@ export class DuckDBRepository implements ResourceRepository {
57
54
  }
58
55
 
59
56
  async deleteEnvironment(id: string): Promise<void> {
60
- await this.manifestRepo.deleteEntriesByEnvironmentId(id);
61
57
  await this.materializationRepo.deleteByEnvironmentId(id);
62
58
  await this.connectionRepo.deleteConnectionsByEnvironmentId(id);
63
59
  await this.packageRepo.deletePackagesByEnvironmentId(id);
@@ -97,10 +93,6 @@ export class DuckDBRepository implements ResourceRepository {
97
93
  async deletePackage(id: string): Promise<void> {
98
94
  const pkg = await this.packageRepo.getPackageById(id);
99
95
  if (pkg) {
100
- await this.manifestRepo.deleteEntriesByPackage(
101
- pkg.environmentId,
102
- pkg.name,
103
- );
104
96
  await this.materializationRepo.deleteByPackage(
105
97
  pkg.environmentId,
106
98
  pkg.name,
@@ -188,13 +180,7 @@ export class DuckDBRepository implements ResourceRepository {
188
180
 
189
181
  async updateMaterialization(
190
182
  id: string,
191
- updates: {
192
- status?: MaterializationStatus;
193
- startedAt?: Date;
194
- completedAt?: Date;
195
- error?: string | null;
196
- metadata?: Record<string, unknown> | null;
197
- },
183
+ updates: MaterializationUpdate,
198
184
  ): Promise<Materialization> {
199
185
  return this.materializationRepo.update(id, updates);
200
186
  }
@@ -202,23 +188,4 @@ export class DuckDBRepository implements ResourceRepository {
202
188
  async deleteMaterialization(id: string): Promise<void> {
203
189
  return this.materializationRepo.deleteById(id);
204
190
  }
205
-
206
- // ==================== BUILD MANIFESTS ====================
207
-
208
- async listManifestEntries(
209
- environmentId: string,
210
- packageName: string,
211
- ): Promise<ManifestEntry[]> {
212
- return this.manifestRepo.listEntries(environmentId, packageName);
213
- }
214
-
215
- async upsertManifestEntry(
216
- entry: Omit<ManifestEntry, "id" | "createdAt" | "updatedAt">,
217
- ): Promise<ManifestEntry> {
218
- return this.manifestRepo.upsertEntry(entry);
219
- }
220
-
221
- async deleteManifestEntry(id: string): Promise<void> {
222
- return this.manifestRepo.deleteEntry(id);
223
- }
224
191
  }