@malloy-publisher/server 0.0.205 → 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 (69) hide show
  1. package/dist/app/api-doc.yaml +363 -395
  2. package/dist/app/assets/{EnvironmentPage-CAge6UHD.js → EnvironmentPage-BYwBeC2F.js} +1 -1
  3. package/dist/app/assets/{HomePage-DhTe8qpa.js → HomePage-ivu4vdpj.js} +1 -1
  4. package/dist/app/assets/{MainPage-CeTxxGex.js → MainPage-B2DnHEDU.js} +2 -2
  5. package/dist/app/assets/{MaterializationsPage-CpDHB70t.js → MaterializationsPage-BZEuwF9P.js} +1 -1
  6. package/dist/app/assets/{ModelPage-D9sSMb75.js → ModelPage-Dpu3bfPg.js} +1 -1
  7. package/dist/app/assets/{PackagePage-LRqQWrFY.js → PackagePage-B8PwcRHt.js} +1 -1
  8. package/dist/app/assets/{RouteError-xT6kuCNw.js → RouteError-BhbywAeC.js} +1 -1
  9. package/dist/app/assets/{WorkbookPage-DsIh9svZ.js → WorkbookPage-C-JXsJG0.js} +1 -1
  10. package/dist/app/assets/{core-C2sQrwVu.es-Bjem0hym.js → core-pPlPr7jK.es-CNEOlxKB.js} +1 -1
  11. package/dist/app/assets/{index-BdOZDcce.js → index-BHEm8Egc.js} +1 -1
  12. package/dist/app/assets/{index-DHHAcY5o.js → index-BsvDrV14.js} +1 -1
  13. package/dist/app/assets/{index-RX3QOTde.js → index-ChR1fKR2.js} +127 -127
  14. package/dist/app/assets/{index.umd-D2WH3D-f.js → index.umd-BVLPYNuj.js} +1 -1
  15. package/dist/app/index.html +1 -1
  16. package/dist/instrumentation.mjs +18 -8
  17. package/dist/package_load_worker.mjs +19 -2
  18. package/dist/server.mjs +1151 -1264
  19. package/package.json +1 -1
  20. package/src/constants.ts +12 -0
  21. package/src/controller/materialization.controller.ts +79 -35
  22. package/src/controller/package.controller.spec.ts +179 -0
  23. package/src/controller/package.controller.ts +60 -73
  24. package/src/dto/package.dto.ts +16 -1
  25. package/src/errors.spec.ts +12 -0
  26. package/src/errors.ts +18 -0
  27. package/src/health.spec.ts +34 -1
  28. package/src/instrumentation.ts +33 -17
  29. package/src/materialization_metrics.ts +66 -0
  30. package/src/package_load/package_load_pool.ts +7 -1
  31. package/src/package_load/package_load_worker.ts +44 -4
  32. package/src/package_load/protocol.ts +7 -1
  33. package/src/server-old.ts +7 -149
  34. package/src/server.ts +9 -188
  35. package/src/service/authorize_integration.spec.ts +67 -0
  36. package/src/service/environment.ts +212 -12
  37. package/src/service/environment_store.spec.ts +0 -81
  38. package/src/service/environment_store.ts +0 -23
  39. package/src/service/explore_visibility.spec.ts +434 -0
  40. package/src/service/exports_probe.spec.ts +107 -0
  41. package/src/service/manifest_loader.spec.ts +99 -0
  42. package/src/service/manifest_loader.ts +95 -0
  43. package/src/service/materialization_service.spec.ts +324 -512
  44. package/src/service/materialization_service.ts +816 -656
  45. package/src/service/model.ts +400 -13
  46. package/src/service/package.spec.ts +14 -2
  47. package/src/service/package.ts +254 -14
  48. package/src/service/package_rollback.spec.ts +190 -0
  49. package/src/service/package_worker_path.spec.ts +223 -0
  50. package/src/service/query_boundary.spec.ts +470 -0
  51. package/src/storage/DatabaseInterface.ts +35 -57
  52. package/src/storage/StorageManager.mock.ts +0 -9
  53. package/src/storage/StorageManager.ts +7 -290
  54. package/src/storage/duckdb/DuckDBRepository.ts +2 -35
  55. package/src/storage/duckdb/MaterializationRepository.ts +52 -27
  56. package/src/storage/duckdb/schema.ts +4 -20
  57. package/tests/integration/materialization/manifest_binding.integration.spec.ts +175 -0
  58. package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +277 -266
  59. package/tests/unit/duckdb/legacy_schema_migration.test.ts +7 -4
  60. package/src/controller/manifest.controller.ts +0 -38
  61. package/src/service/manifest_service.spec.ts +0 -206
  62. package/src/service/manifest_service.ts +0 -117
  63. package/src/service/materialized_table_gc.spec.ts +0 -384
  64. package/src/service/materialized_table_gc.ts +0 -231
  65. package/src/storage/duckdb/DuckDBManifestStore.ts +0 -70
  66. package/src/storage/duckdb/ManifestRepository.ts +0 -120
  67. package/src/storage/duckdb/manifest_store.spec.ts +0 -133
  68. package/src/storage/ducklake/DuckLakeManifestStore.ts +0 -155
  69. package/tests/unit/storage/StorageManager.test.ts +0 -166
@@ -0,0 +1,470 @@
1
+ /**
2
+ * Integration test: Query Boundary (`queryableSources`).
3
+ *
4
+ * Drives `Package.create` through the package-load worker pool and verifies the
5
+ * rule "queryable == discoverable": under the default `queryableSources:
6
+ * "declared"`, only the discovery surface (`explores` files + their `export {}`
7
+ * closure) is a valid top-level query target. Everything else still compiles,
8
+ * imports, joins, and extends, but is denied a direct query with a generic 404
9
+ * (`NotQueryableError`). `"all"` decouples the axes (the prior behavior), and a
10
+ * package with no `explores` is unaffected in either mode.
11
+ *
12
+ * Enforcement is two-step (mirroring the authorize gate): an early
13
+ * pre-compilation gate that positively denies what it can resolve (schema-
14
+ * oracle defense), and a compiled backstop that settles everything else
15
+ * against the run target Malloy actually executes (read off the prepared
16
+ * query's structRef — inspecting compiler output, never altering compilation).
17
+ * Ad-hoc derivation over a curated source is admitted via the same alias walk
18
+ * filters use; an unresolvable target fails closed.
19
+ *
20
+ * This is the *what* axis; `#(authorize)` (the *who* axis) is orthogonal and
21
+ * tested in explore_visibility.spec.ts / the authorize specs. Notebooks are
22
+ * always public and never gated.
23
+ */
24
+ import {
25
+ afterAll,
26
+ afterEach,
27
+ beforeAll,
28
+ beforeEach,
29
+ describe,
30
+ expect,
31
+ it,
32
+ } from "bun:test";
33
+ import * as fs from "fs";
34
+ import * as os from "os";
35
+ import * as path from "path";
36
+ import {
37
+ PackageLoadPool,
38
+ __setPackageLoadPoolForTests,
39
+ } from "../package_load/package_load_pool";
40
+ import { NotQueryableError } from "../errors";
41
+ import { Package } from "./package";
42
+
43
+ const ORIGINAL_ENV = process.env.PACKAGE_LOAD_WORKERS;
44
+
45
+ describe("Query Boundary (queryableSources) via worker pool", () => {
46
+ let tempDir: string;
47
+ let pool: PackageLoadPool;
48
+
49
+ beforeAll(async () => {
50
+ process.env.PACKAGE_LOAD_WORKERS = "1";
51
+ pool = new PackageLoadPool(1);
52
+ await __setPackageLoadPoolForTests(pool);
53
+ });
54
+
55
+ afterAll(async () => {
56
+ await __setPackageLoadPoolForTests(null);
57
+ if (ORIGINAL_ENV === undefined) delete process.env.PACKAGE_LOAD_WORKERS;
58
+ else process.env.PACKAGE_LOAD_WORKERS = ORIGINAL_ENV;
59
+ });
60
+
61
+ beforeEach(() => {
62
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "publisher-qbound-"));
63
+ });
64
+
65
+ afterEach(() => {
66
+ if (tempDir) {
67
+ try {
68
+ fs.rmSync(tempDir, { recursive: true, force: true });
69
+ } catch {
70
+ /* already gone */
71
+ }
72
+ tempDir = "";
73
+ }
74
+ });
75
+
76
+ async function makeMalloyConfig(): Promise<{
77
+ malloyConfig: import("@malloydata/malloy").MalloyConfig;
78
+ duckdb: { close: () => Promise<void> };
79
+ }> {
80
+ const { MalloyConfig, FixedConnectionMap } = await import(
81
+ "@malloydata/malloy"
82
+ );
83
+ const { DuckDBConnection } = await import("@malloydata/db-duckdb");
84
+ const duckdb = new DuckDBConnection("duckdb", ":memory:");
85
+ const connections = new FixedConnectionMap(
86
+ new Map([["duckdb", duckdb]]),
87
+ "duckdb",
88
+ );
89
+ const malloyConfig = new MalloyConfig({ connections: {} });
90
+ malloyConfig.wrapConnections(() => connections);
91
+ return { malloyConfig, duckdb };
92
+ }
93
+
94
+ function writeManifest(extra: Record<string, unknown> = {}): void {
95
+ fs.writeFileSync(
96
+ path.join(tempDir, "publisher.json"),
97
+ JSON.stringify({ name: "pkg", description: "test package", ...extra }),
98
+ );
99
+ }
100
+
101
+ // base.malloy (a building block, never an explore) + index.malloy (the
102
+ // explore) which imports/joins it, exports only `customers`, and keeps a
103
+ // local `helper` source it does NOT export. Each runnable source carries a
104
+ // view so it can actually be queried.
105
+ function writeLayeredModels(): void {
106
+ fs.writeFileSync(
107
+ path.join(tempDir, "base.malloy"),
108
+ `source: base_source is duckdb.sql("select 1 as id, 5 as n") extend {
109
+ measure: total_n is n.sum()
110
+ view: v is { aggregate: total_n }
111
+ }`,
112
+ );
113
+ fs.writeFileSync(
114
+ path.join(tempDir, "index.malloy"),
115
+ `import "base.malloy"
116
+ source: helper is duckdb.sql("select 1 as id") extend {
117
+ measure: c is count()
118
+ view: hv is { aggregate: c }
119
+ }
120
+ source: customers is duckdb.sql("select 1 as id, 100 as amt") extend {
121
+ join_one: b is base_source on id = b.id
122
+ measure: total is amt.sum()
123
+ view: v is { aggregate: total }
124
+ }
125
+ export { customers }`,
126
+ );
127
+ fs.writeFileSync(
128
+ path.join(tempDir, "report.malloynb"),
129
+ `>>>markdown\n# Report\nAlways public.`,
130
+ );
131
+ }
132
+
133
+ // -- default mode ("declared") -----------------------------------------
134
+
135
+ it("declared: exported source in an explores file IS queryable (incl. join-through)", async () => {
136
+ writeManifest({ explores: ["index.malloy"] }); // queryableSources defaults to "declared"
137
+ writeLayeredModels();
138
+ const { malloyConfig, duckdb } = await makeMalloyConfig();
139
+ try {
140
+ const pkg = await Package.create("env", "pkg", tempDir, malloyConfig);
141
+ const { result } = await pkg
142
+ .getModel("index.malloy")!
143
+ .getQueryResults("customers", "v", undefined);
144
+ expect(result.data).toBeDefined();
145
+ } finally {
146
+ await duckdb.close();
147
+ }
148
+ });
149
+
150
+ it("declared: a non-explores model file is not queryable (file-level, 404)", async () => {
151
+ writeManifest({ explores: ["index.malloy"] });
152
+ writeLayeredModels();
153
+ const { malloyConfig, duckdb } = await makeMalloyConfig();
154
+ try {
155
+ const pkg = await Package.create("env", "pkg", tempDir, malloyConfig);
156
+ // base.malloy is hidden (not an explore) → not a query entry point.
157
+ await expect(
158
+ pkg
159
+ .getModel("base.malloy")!
160
+ .getQueryResults("base_source", "v", undefined),
161
+ ).rejects.toBeInstanceOf(NotQueryableError);
162
+ } finally {
163
+ await duckdb.close();
164
+ }
165
+ });
166
+
167
+ it("declared: a non-exported source inside an explores file is not queryable", async () => {
168
+ writeManifest({ explores: ["index.malloy"] });
169
+ writeLayeredModels();
170
+ const { malloyConfig, duckdb } = await makeMalloyConfig();
171
+ try {
172
+ const pkg = await Package.create("env", "pkg", tempDir, malloyConfig);
173
+ // `helper` compiles and is joinable, but is not in index's export{}.
174
+ await expect(
175
+ pkg.getModel("index.malloy")!.getQueryResults("helper", "hv"),
176
+ ).rejects.toBeInstanceOf(NotQueryableError);
177
+ } finally {
178
+ await duckdb.close();
179
+ }
180
+ });
181
+
182
+ it("declared: ad-hoc query is admitted for a curated source, denied for a hidden one", async () => {
183
+ writeManifest({ explores: ["index.malloy"] });
184
+ writeLayeredModels();
185
+ const { malloyConfig, duckdb } = await makeMalloyConfig();
186
+ try {
187
+ const pkg = await Package.create("env", "pkg", tempDir, malloyConfig);
188
+ const model = pkg.getModel("index.malloy")!;
189
+
190
+ const { result } = await model.getQueryResults(
191
+ undefined,
192
+ undefined,
193
+ "run: customers -> { aggregate: total }",
194
+ );
195
+ expect(result.data).toBeDefined();
196
+
197
+ await expect(
198
+ model.getQueryResults(
199
+ undefined,
200
+ undefined,
201
+ "run: helper -> { aggregate: c }",
202
+ ),
203
+ ).rejects.toBeInstanceOf(NotQueryableError);
204
+ } finally {
205
+ await duckdb.close();
206
+ }
207
+ });
208
+
209
+ it("declared: multi-statement is settled by the COMPILED run target (last statement wins)", async () => {
210
+ writeManifest({ explores: ["index.malloy"] });
211
+ writeLayeredModels();
212
+ const { malloyConfig, duckdb } = await makeMalloyConfig();
213
+ try {
214
+ const pkg = await Package.create("env", "pkg", tempDir, malloyConfig);
215
+ const model = pkg.getModel("index.malloy")!;
216
+
217
+ // Curated decoy first, hidden real target last. The early gate only
218
+ // sees the first `run:` (curated → defers); the compiled backstop
219
+ // resolves the LAST statement — the one Malloy actually executes —
220
+ // and denies it. This is the case the structRef read exists for.
221
+ await expect(
222
+ model.getQueryResults(
223
+ undefined,
224
+ undefined,
225
+ "run: customers -> { aggregate: total }\nrun: helper -> { aggregate: c }",
226
+ ),
227
+ ).rejects.toBeInstanceOf(NotQueryableError);
228
+
229
+ // Hidden target FIRST is positively denied by the early gate, before
230
+ // compilation — its compile errors can't be used as a schema oracle.
231
+ await expect(
232
+ model.getQueryResults(
233
+ undefined,
234
+ undefined,
235
+ "run: helper -> { aggregate: c }\nrun: customers -> { aggregate: total }",
236
+ ),
237
+ ).rejects.toBeInstanceOf(NotQueryableError);
238
+
239
+ // All-curated multi-statement is legitimate and admitted (no false
240
+ // denial from the old fail-closed-on-shape heuristic).
241
+ const { result } = await model.getQueryResults(
242
+ undefined,
243
+ undefined,
244
+ "run: customers -> { aggregate: total }\nrun: customers -> { group_by: id }",
245
+ );
246
+ expect(result.data).toBeDefined();
247
+ } finally {
248
+ await duckdb.close();
249
+ }
250
+ });
251
+
252
+ it("declared: ad-hoc derivation over a curated source is queryable; over a hidden one is not", async () => {
253
+ writeManifest({ explores: ["index.malloy"] });
254
+ writeLayeredModels();
255
+ const { malloyConfig, duckdb } = await makeMalloyConfig();
256
+ try {
257
+ const pkg = await Package.create("env", "pkg", tempDir, malloyConfig);
258
+ const model = pkg.getModel("index.malloy")!;
259
+
260
+ // Composing over a queryable source is itself queryable: the compiled
261
+ // structRef names the ad-hoc alias `x`, and the derivation walk maps
262
+ // x → customers (curated).
263
+ const { result } = await model.getQueryResults(
264
+ undefined,
265
+ undefined,
266
+ "source: x is customers extend { measure: m is count() }\nrun: x -> { aggregate: m }",
267
+ );
268
+ expect(result.data).toBeDefined();
269
+
270
+ // Laundering a hidden source through an ad-hoc alias is denied: the
271
+ // chain y → helper never reaches the curated surface.
272
+ await expect(
273
+ model.getQueryResults(
274
+ undefined,
275
+ undefined,
276
+ "source: y is helper extend { measure: m is count() }\nrun: y -> { aggregate: m }",
277
+ ),
278
+ ).rejects.toBeInstanceOf(NotQueryableError);
279
+ } finally {
280
+ await duckdb.close();
281
+ }
282
+ });
283
+
284
+ it("declared: an exported named query reading a hidden source is admitted by name", async () => {
285
+ // Exporting a query is the author's deliberate exposure of a result,
286
+ // even when the source it reads stays hidden. The explicit queryName
287
+ // request is cleared by the early gate and the compiled backstop is
288
+ // skipped — re-deriving the underlying (hidden) source must not re-deny
289
+ // the author's chosen entry point.
290
+ writeManifest({ explores: ["index.malloy"] });
291
+ fs.writeFileSync(
292
+ path.join(tempDir, "index.malloy"),
293
+ `source: helper is duckdb.sql("select 1 as id") extend {
294
+ measure: c is count()
295
+ }
296
+ source: customers is duckdb.sql("select 1 as id, 100 as amt") extend {
297
+ measure: total is amt.sum()
298
+ }
299
+ query: helper_stats is helper -> { aggregate: c }
300
+ export { customers, helper_stats }`,
301
+ );
302
+ const { malloyConfig, duckdb } = await makeMalloyConfig();
303
+ try {
304
+ const pkg = await Package.create("env", "pkg", tempDir, malloyConfig);
305
+ const model = pkg.getModel("index.malloy")!;
306
+
307
+ const { result } = await model.getQueryResults(
308
+ undefined,
309
+ "helper_stats",
310
+ undefined,
311
+ );
312
+ expect(result.data).toBeDefined();
313
+
314
+ // …while the hidden source itself stays non-queryable directly.
315
+ await expect(
316
+ model.getQueryResults(
317
+ undefined,
318
+ undefined,
319
+ "run: helper -> { aggregate: c }",
320
+ ),
321
+ ).rejects.toBeInstanceOf(NotQueryableError);
322
+ } finally {
323
+ await duckdb.close();
324
+ }
325
+ });
326
+
327
+ it("declared: a hidden source is not queryable via a view whose name collides with an exported query (regression)", async () => {
328
+ // Guards the early-gate fast-path. An exported top-level query named
329
+ // `stats` must admit the pure named query `run: stats`, but must NOT clear
330
+ // `run: helper->stats` where `helper` is hidden and `stats` is also a view
331
+ // on it. Clearing on the bare queryName (ignoring the source prefix) would
332
+ // skip the compiled backstop and let the hidden source be read through the
333
+ // name collision — so the source-prefixed form is gated on the *source*
334
+ // being curated, not on the query name.
335
+ writeManifest({ explores: ["index.malloy"] });
336
+ fs.writeFileSync(
337
+ path.join(tempDir, "index.malloy"),
338
+ `source: helper is duckdb.sql("select 1 as id") extend {
339
+ measure: c is count()
340
+ view: stats is { aggregate: c }
341
+ }
342
+ source: customers is duckdb.sql("select 1 as id, 100 as amt") extend {
343
+ measure: total is amt.sum()
344
+ }
345
+ query: stats is customers -> { aggregate: total }
346
+ export { customers, stats }`,
347
+ );
348
+ const { malloyConfig, duckdb } = await makeMalloyConfig();
349
+ try {
350
+ const pkg = await Package.create("env", "pkg", tempDir, malloyConfig);
351
+ const model = pkg.getModel("index.malloy")!;
352
+
353
+ // The exported top-level query is reachable on its own name.
354
+ const { result } = await model.getQueryResults(
355
+ undefined,
356
+ "stats",
357
+ undefined,
358
+ );
359
+ expect(result.data).toBeDefined();
360
+
361
+ // …but the hidden source is NOT reachable through the colliding view
362
+ // name when explicitly targeted as the run source.
363
+ await expect(
364
+ model.getQueryResults("helper", "stats", undefined),
365
+ ).rejects.toBeInstanceOf(NotQueryableError);
366
+ } finally {
367
+ await duckdb.close();
368
+ }
369
+ });
370
+
371
+ it("declared: ad-hoc derivation over a backtick-quoted exported source is queryable", async () => {
372
+ // A source whose name needs backticks (a hyphen) must still be walkable
373
+ // by the derivation alias map — otherwise composing over it returns a
374
+ // false 404. Guards buildAliasMap's quoted-identifier handling.
375
+ writeManifest({ explores: ["index.malloy"] });
376
+ fs.writeFileSync(
377
+ path.join(tempDir, "index.malloy"),
378
+ `source: \`customer-orders\` is duckdb.sql("select 1 as id, 100 as amt") extend {
379
+ measure: total is amt.sum()
380
+ }
381
+ export { \`customer-orders\` }`,
382
+ );
383
+ const { malloyConfig, duckdb } = await makeMalloyConfig();
384
+ try {
385
+ const pkg = await Package.create("env", "pkg", tempDir, malloyConfig);
386
+ const model = pkg.getModel("index.malloy")!;
387
+
388
+ const { result } = await model.getQueryResults(
389
+ undefined,
390
+ undefined,
391
+ "source: x is `customer-orders` extend { measure: m is count() }\nrun: x -> { aggregate: m }",
392
+ );
393
+ expect(result.data).toBeDefined();
394
+ } finally {
395
+ await duckdb.close();
396
+ }
397
+ });
398
+
399
+ it("declared: a notebook is exempt from the boundary (always public)", async () => {
400
+ // The /compile path runs assertQueryBoundaryEarly against the target
401
+ // model; a notebook is never in `explores`, so without an explicit
402
+ // exemption it would 404 — contradicting "notebooks are always public".
403
+ writeManifest({ explores: ["index.malloy"] });
404
+ writeLayeredModels();
405
+ const { malloyConfig, duckdb } = await makeMalloyConfig();
406
+ try {
407
+ const pkg = await Package.create("env", "pkg", tempDir, malloyConfig);
408
+ const notebook = pkg.getModel("report.malloynb");
409
+ expect(notebook).toBeDefined();
410
+ // Boundary is inert for notebooks even though it's not an explore.
411
+ expect(
412
+ notebook!.assertQueryBoundaryEarly(undefined, undefined, "run: x"),
413
+ ).toBe("cleared");
414
+ expect(() =>
415
+ notebook!.assertQueryBoundaryCompiled("anything", "run: x"),
416
+ ).not.toThrow();
417
+ } finally {
418
+ await duckdb.close();
419
+ }
420
+ });
421
+
422
+ // -- "all" mode (decoupled) --------------------------------------------
423
+
424
+ it("all: explores gates discovery only — hidden file/source stay queryable", async () => {
425
+ writeManifest({ explores: ["index.malloy"], queryableSources: "all" });
426
+ writeLayeredModels();
427
+ const { malloyConfig, duckdb } = await makeMalloyConfig();
428
+ try {
429
+ const pkg = await Package.create("env", "pkg", tempDir, malloyConfig);
430
+
431
+ // Listing is still curated to the explore.
432
+ const listed = (await pkg.listModels()).map((m) => m.path);
433
+ expect(listed).toEqual(["index.malloy"]);
434
+
435
+ // …but the non-explores file and the non-exported source are queryable.
436
+ const base = await pkg
437
+ .getModel("base.malloy")!
438
+ .getQueryResults("base_source", "v", undefined);
439
+ expect(base.result.data).toBeDefined();
440
+ const helper = await pkg
441
+ .getModel("index.malloy")!
442
+ .getQueryResults("helper", "hv", undefined);
443
+ expect(helper.result.data).toBeDefined();
444
+ } finally {
445
+ await duckdb.close();
446
+ }
447
+ });
448
+
449
+ // -- no explores declared (boundary inert) -----------------------------
450
+
451
+ it("declared default + no explores: everything stays queryable (backward compatible)", async () => {
452
+ writeManifest(); // no explores → no curated surface to enforce
453
+ writeLayeredModels();
454
+ const { malloyConfig, duckdb } = await makeMalloyConfig();
455
+ try {
456
+ const pkg = await Package.create("env", "pkg", tempDir, malloyConfig);
457
+ const base = await pkg
458
+ .getModel("base.malloy")!
459
+ .getQueryResults("base_source", "v", undefined);
460
+ expect(base.result.data).toBeDefined();
461
+ // `helper` is non-exported, but with no explores there is no boundary.
462
+ const helper = await pkg
463
+ .getModel("index.malloy")!
464
+ .getQueryResults("helper", "hv", undefined);
465
+ expect(helper.result.data).toBeDefined();
466
+ } finally {
467
+ await duckdb.close();
468
+ }
469
+ });
470
+ });
@@ -1,3 +1,5 @@
1
+ import { components } from "../api";
2
+
1
3
  /**
2
4
  * Generic database interface for storage abstraction
3
5
  * This allows switching between DuckDB, PostgreSQL, MySQL, etc.
@@ -70,24 +72,9 @@ export interface ResourceRepository {
70
72
  ): Promise<Materialization>;
71
73
  updateMaterialization(
72
74
  id: string,
73
- updates: {
74
- status?: MaterializationStatus;
75
- startedAt?: Date;
76
- completedAt?: Date;
77
- error?: string | null;
78
- metadata?: Record<string, unknown> | null;
79
- },
75
+ updates: MaterializationUpdate,
80
76
  ): Promise<Materialization>;
81
77
  deleteMaterialization(id: string): Promise<void>;
82
- // Build Manifests
83
- listManifestEntries(
84
- environmentId: string,
85
- packageName: string,
86
- ): Promise<ManifestEntry[]>;
87
- upsertManifestEntry(
88
- entry: Omit<ManifestEntry, "id" | "createdAt" | "updatedAt">,
89
- ): Promise<ManifestEntry>;
90
- deleteManifestEntry(id: string): Promise<void>;
91
78
  }
92
79
 
93
80
  export interface Environment {
@@ -121,18 +108,30 @@ export interface Connection {
121
108
  updatedAt: Date;
122
109
  }
123
110
 
111
+ // Wire types for the two-round build protocol, kept in sync with the OpenAPI
112
+ // spec via the generated `api.ts`.
124
113
  export type MaterializationStatus =
125
- | "PENDING"
126
- | "RUNNING"
127
- | "SUCCESS"
128
- | "FAILED"
129
- | "CANCELLED";
114
+ components["schemas"]["MaterializationStatus"];
115
+ export type BuildPlan = components["schemas"]["BuildPlan"];
116
+ export type BuildManifestResult = components["schemas"]["BuildManifest"];
117
+ export type ManifestEntry = components["schemas"]["ManifestEntry"];
118
+ export type BuildInstruction = components["schemas"]["BuildInstruction"];
119
+ export type Realization = components["schemas"]["Realization"];
130
120
 
131
121
  export interface Materialization {
132
122
  id: string;
133
123
  environmentId: string;
134
124
  packageName: string;
125
+ /**
126
+ * Echoes the create-time flag (read from metadata). False (default) =
127
+ * auto-run all phases; true = pause at BUILD_PLAN_READY for Round 2.
128
+ */
129
+ pauseBetweenPhases: boolean;
135
130
  status: MaterializationStatus;
131
+ /** Round 1 output. Null until status >= BUILD_PLAN_READY. */
132
+ buildPlan: BuildPlan | null;
133
+ /** Round 2 output. Null until status = MANIFEST_FILE_READY. */
134
+ manifest: BuildManifestResult | null;
136
135
  startedAt: Date | null;
137
136
  completedAt: Date | null;
138
137
  error: string | null;
@@ -141,20 +140,23 @@ export interface Materialization {
141
140
  updatedAt: Date;
142
141
  }
143
142
 
144
- export interface ManifestEntry {
145
- id: string;
146
- environmentId: string;
147
- packageName: string;
148
- buildId: string;
149
- tableName: string;
150
- sourceName: string;
151
- connectionName: string;
152
- createdAt: Date;
153
- updatedAt: Date;
143
+ export interface MaterializationUpdate {
144
+ status?: MaterializationStatus;
145
+ buildPlan?: BuildPlan | null;
146
+ manifest?: BuildManifestResult | null;
147
+ startedAt?: Date;
148
+ completedAt?: Date;
149
+ error?: string | null;
150
+ metadata?: Record<string, unknown> | null;
154
151
  }
155
152
 
156
- // ==================== MANIFEST STORE ====================
157
-
153
+ /**
154
+ * Malloy-facing build manifest: maps a buildId to the physical table backing
155
+ * that persist source. This is the shape the Malloy runtime consumes when
156
+ * (re)loading models so persist references resolve to materialized tables.
157
+ * Distinct from {@link BuildManifestResult} (the wire/Round 2 output, which
158
+ * also carries CP bookkeeping like materializedTableId and rowCount).
159
+ */
158
160
  export interface BuildManifestEntry {
159
161
  tableName: string;
160
162
  }
@@ -163,27 +165,3 @@ export interface BuildManifest {
163
165
  entries: Record<string, BuildManifestEntry>;
164
166
  strict?: boolean;
165
167
  }
166
-
167
- /**
168
- * Abstraction for manifest storage. Standalone mode uses DuckDB;
169
- * orchestrated mode swaps in a DuckLakeManifestStore.
170
- */
171
- export interface ManifestStore {
172
- getManifest(
173
- environmentId: string,
174
- packageName: string,
175
- ): Promise<BuildManifest>;
176
- writeEntry(
177
- environmentId: string,
178
- packageName: string,
179
- buildId: string,
180
- tableName: string,
181
- sourceName: string,
182
- connectionName: string,
183
- ): Promise<void>;
184
- deleteEntry(id: string): Promise<void>;
185
- listEntries(
186
- environmentId: string,
187
- packageName: string,
188
- ): Promise<ManifestEntry[]>;
189
- }
@@ -47,13 +47,4 @@ 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
- }
59
50
  }