@malloy-publisher/server 0.0.198-dev4 → 0.0.198-dev6

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.
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Integration test: exercise `Package.create` with the package-load
3
+ * worker pool enabled (PACKAGE_LOAD_WORKERS=1).
4
+ *
5
+ * Validates that the worker-load path:
6
+ * - reads the manifest, probes embedded databases, and compiles
7
+ * every model in a single off-thread job
8
+ * - produces a live `Package` whose `Model`s have populated
9
+ * `modelDef` / `sources` / `queries`
10
+ * - hydrates the `ModelMaterializer` from `modelDef` on first
11
+ * query (no recompile) — verified end-to-end by running a
12
+ * query through the resulting Model and getting a result
13
+ *
14
+ * Kept separate from `package.spec.ts` so the existing tests keep
15
+ * running on the in-process path without paying worker startup cost.
16
+ *
17
+ * Pool reuse strategy: one `PackageLoadPool` shared across all
18
+ * cases in this file. Spawning a fresh worker per test crashes Bun
19
+ * (segfault) because DuckDB's native bindings don't tolerate being
20
+ * loaded concurrently into multiple worker isolates of the same Bun
21
+ * process. Production uses one pool; this matches.
22
+ */
23
+ import {
24
+ afterAll,
25
+ afterEach,
26
+ beforeAll,
27
+ beforeEach,
28
+ describe,
29
+ expect,
30
+ it,
31
+ } from "bun:test";
32
+ import * as fs from "fs";
33
+ import * as os from "os";
34
+ import * as path from "path";
35
+ import {
36
+ PackageLoadPool,
37
+ __setPackageLoadPoolForTests,
38
+ } from "../package_load/package_load_pool";
39
+ import { Package } from "./package";
40
+
41
+ const ORIGINAL_ENV = process.env.PACKAGE_LOAD_WORKERS;
42
+
43
+ describe("Package.create via worker pool", () => {
44
+ let tempDir: string;
45
+ let pool: PackageLoadPool;
46
+
47
+ beforeAll(async () => {
48
+ process.env.PACKAGE_LOAD_WORKERS = "1";
49
+ pool = new PackageLoadPool(1);
50
+ // Wire our pool into the module-level singleton so Package.create
51
+ // picks it up via getPackageLoadPool().
52
+ await __setPackageLoadPoolForTests(pool);
53
+ });
54
+
55
+ afterAll(async () => {
56
+ await __setPackageLoadPoolForTests(null);
57
+ if (ORIGINAL_ENV === undefined) {
58
+ delete process.env.PACKAGE_LOAD_WORKERS;
59
+ } else {
60
+ process.env.PACKAGE_LOAD_WORKERS = ORIGINAL_ENV;
61
+ }
62
+ });
63
+
64
+ beforeEach(() => {
65
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "publisher-pkg-worker-"));
66
+ });
67
+
68
+ afterEach(() => {
69
+ if (tempDir) {
70
+ // tempDir gets wiped by Package.create on failure (it's the
71
+ // staging-cleanup path); ignore ENOENT here.
72
+ try {
73
+ fs.rmSync(tempDir, { recursive: true, force: true });
74
+ } catch {
75
+ /* already gone */
76
+ }
77
+ tempDir = "";
78
+ }
79
+ });
80
+
81
+ async function makeMalloyConfig(): Promise<{
82
+ malloyConfig: import("@malloydata/malloy").MalloyConfig;
83
+ duckdb: { close: () => Promise<void> };
84
+ }> {
85
+ const { MalloyConfig, FixedConnectionMap } = await import(
86
+ "@malloydata/malloy"
87
+ );
88
+ const { DuckDBConnection } = await import("@malloydata/db-duckdb");
89
+ const duckdb = new DuckDBConnection("duckdb", ":memory:");
90
+ const connections = new FixedConnectionMap(
91
+ new Map([["duckdb", duckdb]]),
92
+ "duckdb",
93
+ );
94
+ const malloyConfig = new MalloyConfig({ connections: {} });
95
+ malloyConfig.wrapConnections(() => connections);
96
+ return { malloyConfig, duckdb };
97
+ }
98
+
99
+ function writeManifest(): void {
100
+ fs.writeFileSync(
101
+ path.join(tempDir, "publisher.json"),
102
+ JSON.stringify({ name: "pkg", description: "test package" }),
103
+ );
104
+ }
105
+
106
+ it("loads a package end-to-end and serves a query through the hydrated materializer", async () => {
107
+ writeManifest();
108
+ // Define `total_v` as a *view* on the source so the query
109
+ // builder's `run: nums -> total_v` form resolves. (Top-level
110
+ // queries take the `run: total_q` form — orthogonal path that
111
+ // the in-process tests already cover.)
112
+ fs.writeFileSync(
113
+ path.join(tempDir, "trivial.malloy"),
114
+ `source: nums is duckdb.sql("select 1 as a, 2 as b") extend {
115
+ measure: total is a.sum()
116
+ view: total_v is { aggregate: total }
117
+ }`,
118
+ );
119
+
120
+ const { malloyConfig, duckdb } = await makeMalloyConfig();
121
+ try {
122
+ const pkg = await Package.create("env", "pkg", tempDir, malloyConfig);
123
+ expect(pkg).toBeInstanceOf(Package);
124
+ expect(pkg.getModelPaths()).toEqual(["trivial.malloy"]);
125
+
126
+ const model = pkg.getModel("trivial.malloy");
127
+ expect(model).toBeDefined();
128
+ const apiModel = (await model!.getModel()) as {
129
+ modelDef?: string;
130
+ sources?: { name?: string }[];
131
+ };
132
+ expect(apiModel.modelDef).toBeDefined();
133
+ expect(apiModel.sources?.[0]?.name).toBe("nums");
134
+
135
+ // First query against the package — hydrates the
136
+ // ModelMaterializer from the worker's modelDef without a
137
+ // recompile, then runs the SQL against the *main thread's*
138
+ // DuckDB connection (the only one with the in-memory `nums`
139
+ // source loaded via duckdb.sql()).
140
+ const { result } = await model!.getQueryResults(
141
+ "nums",
142
+ "total_v",
143
+ undefined,
144
+ );
145
+ expect(result.data).toBeDefined();
146
+ } finally {
147
+ await duckdb.close();
148
+ }
149
+ });
150
+
151
+ it("propagates a per-model compile failure as a thrown error from Package.create", async () => {
152
+ writeManifest();
153
+ fs.writeFileSync(
154
+ path.join(tempDir, "broken.malloy"),
155
+ `source: bad is duckdb.sql("select 1 as a") extend {
156
+ measure: oops is THIS_FUNC_DOES_NOT_EXIST(a)
157
+ }`,
158
+ );
159
+
160
+ const { malloyConfig, duckdb } = await makeMalloyConfig();
161
+ try {
162
+ await expect(
163
+ Package.create("env", "pkg", tempDir, malloyConfig),
164
+ ).rejects.toBeInstanceOf(Error);
165
+ } finally {
166
+ await duckdb.close();
167
+ }
168
+ });
169
+
170
+ // NB: kept last in this describe — swapping the singleton for a
171
+ // pre-shutdown pool also tears down the shared `pool` (the swap
172
+ // implementation shuts down the outgoing singleton). Subsequent
173
+ // tests in this describe would see a dead pool. afterAll only
174
+ // resets the singleton to null, so this is safe at the tail.
175
+ it("rewraps pool-infrastructure failures as ServiceUnavailableError (HTTP 503)", async () => {
176
+ writeManifest();
177
+ fs.writeFileSync(
178
+ path.join(tempDir, "trivial.malloy"),
179
+ `source: nums is duckdb.sql("select 1 as a")`,
180
+ );
181
+
182
+ const deadPool = new PackageLoadPool(1);
183
+ await deadPool.shutdown();
184
+ await __setPackageLoadPoolForTests(deadPool);
185
+
186
+ const { ServiceUnavailableError } = await import("../errors");
187
+ const { malloyConfig, duckdb } = await makeMalloyConfig();
188
+ try {
189
+ await expect(
190
+ Package.create("env", "pkg", tempDir, malloyConfig),
191
+ ).rejects.toBeInstanceOf(ServiceUnavailableError);
192
+ } finally {
193
+ await duckdb.close();
194
+ }
195
+ });
196
+ });