@malloy-publisher/server 0.0.221 → 0.0.223
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/assets/{EnvironmentPage-D6G5n6mY.js → EnvironmentPage-3wCdljov.js} +1 -1
- package/dist/app/assets/{HomePage-DgZluD8u.js → HomePage-K0GHwqq2.js} +1 -1
- package/dist/app/assets/{LightMode-w9v0pEbg.js → LightMode-CwU3kN4I.js} +1 -1
- package/dist/app/assets/{MainPage-Bi_Tukvr.js → MainPage-CTncHE5T.js} +1 -1
- package/dist/app/assets/{MaterializationsPage-BVERjnmG.js → MaterializationsPage-DziAOD6-.js} +1 -1
- package/dist/app/assets/{ModelPage-2OjS259-.js → ModelPage-XrS2jXEc.js} +1 -1
- package/dist/app/assets/{PackagePage-C8L2On4z.js → PackagePage-BkwgFxG8.js} +1 -1
- package/dist/app/assets/{RouteError-DJoNC_Vl.js → RouteError-9cIQga6p.js} +1 -1
- package/dist/app/assets/{ThemeEditorPage-B-hJ1zgd.js → ThemeEditorPage-DWiCRfEe.js} +1 -1
- package/dist/app/assets/{WorkbookPage-CTbDgGeJ.js → WorkbookPage-Ce7FM_Po.js} +1 -1
- package/dist/app/assets/{core-D9Hl0IDY.es-CmgzHn4c.js → core-D9Hl0IDY.es-CrO01m4X.js} +1 -1
- package/dist/app/assets/{index-BF8PkFm8.js → index-CtQm7kvp.js} +1 -1
- package/dist/app/assets/{index-Cr-asgoV.js → index-D2sm5RBA.js} +4 -4
- package/dist/app/assets/{index-DcCw_qxr.js → index-Dz8hgkmy.js} +1 -1
- package/dist/app/assets/{index-BEjJCJjX.js → index-wJU_kzl2.js} +2 -2
- package/dist/app/index.html +1 -1
- package/dist/server.mjs +1 -6
- package/package.json +1 -1
- package/tests/integration/materialization/manifest_binding.integration.spec.ts +135 -3
- package/dist/sshcrypto-8m50vnmb.node +0 -0
|
@@ -91,17 +91,107 @@ describe("Manifest binding via Package.manifestLocation (E2E)", () => {
|
|
|
91
91
|
});
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
const ROUTING_QUERY = "run: order_summary -> { aggregate: c is count() }";
|
|
95
|
+
|
|
94
96
|
async function queryOrderSummaryStatus(): Promise<number> {
|
|
95
97
|
const res = await fetch(`${baseUrl}${API}/models/${MODEL_PATH}/query`, {
|
|
96
98
|
method: "POST",
|
|
97
99
|
headers: { "Content-Type": "application/json" },
|
|
98
|
-
body: JSON.stringify({
|
|
99
|
-
query: "run: order_summary -> { aggregate: c is count() }",
|
|
100
|
-
}),
|
|
100
|
+
body: JSON.stringify({ query: ROUTING_QUERY }),
|
|
101
101
|
});
|
|
102
102
|
return res.status;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/** The SQL the served query actually compiled to (routing evidence). */
|
|
106
|
+
async function executedSql(): Promise<string> {
|
|
107
|
+
const res = await fetch(`${baseUrl}${API}/models/${MODEL_PATH}/query`, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: { "Content-Type": "application/json" },
|
|
110
|
+
body: JSON.stringify({ query: ROUTING_QUERY }),
|
|
111
|
+
});
|
|
112
|
+
expect(res.status).toBe(200);
|
|
113
|
+
const body = (await res.json()) as { result: string };
|
|
114
|
+
return (JSON.parse(body.result) as { sql: string }).sql;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Read the package's build plan and return the persist source's real sourceEntityId. */
|
|
118
|
+
async function orderSummarySourceEntityId(): Promise<string> {
|
|
119
|
+
const res = await fetch(url(""));
|
|
120
|
+
expect(res.status).toBe(200);
|
|
121
|
+
const pkg = (await res.json()) as {
|
|
122
|
+
buildPlan?: { sources?: Record<string, { sourceEntityId?: string }> };
|
|
123
|
+
};
|
|
124
|
+
const sources = pkg.buildPlan?.sources ?? {};
|
|
125
|
+
const sourceEntityId = Object.values(sources)[0]?.sourceEntityId;
|
|
126
|
+
expect(typeof sourceEntityId).toBe("string");
|
|
127
|
+
return sourceEntityId as string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Poll a materialization until it reaches a terminal state. */
|
|
131
|
+
async function pollUntilTerminal(
|
|
132
|
+
id: string,
|
|
133
|
+
timeoutMs = 90_000,
|
|
134
|
+
): Promise<Record<string, unknown>> {
|
|
135
|
+
const terminal = ["MANIFEST_FILE_READY", "FAILED", "CANCELLED"];
|
|
136
|
+
const deadline = Date.now() + timeoutMs;
|
|
137
|
+
while (Date.now() < deadline) {
|
|
138
|
+
const res = await fetch(url(`/materializations/${id}`));
|
|
139
|
+
expect(res.status).toBe(200);
|
|
140
|
+
const data = (await res.json()) as Record<string, unknown>;
|
|
141
|
+
if (terminal.includes(data.status as string)) return data;
|
|
142
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
143
|
+
}
|
|
144
|
+
throw new Error(`Materialization ${id} did not reach a terminal state`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Build + physicalise the persist source (auto-run self-assigns
|
|
149
|
+
* physicalTableName = "order_summary" from `#@ persist name=...`) so a
|
|
150
|
+
* `SELECT * FROM order_summary` is actually queryable, then revert the
|
|
151
|
+
* package to live (unbound) — keeping the table — so a subsequent
|
|
152
|
+
* manifest-URI bind is what routes the served query.
|
|
153
|
+
*/
|
|
154
|
+
async function buildTableThenRevertToLive(): Promise<void> {
|
|
155
|
+
const createRes = await fetch(url("/materializations"), {
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: { "Content-Type": "application/json" },
|
|
158
|
+
body: JSON.stringify({}),
|
|
159
|
+
});
|
|
160
|
+
expect(createRes.status).toBe(201);
|
|
161
|
+
const { id } = (await createRes.json()) as { id: string };
|
|
162
|
+
const built = await pollUntilTerminal(id);
|
|
163
|
+
expect(built.status).toBe("MANIFEST_FILE_READY");
|
|
164
|
+
// Revert to live: drop the auto-load binding but leave the built table.
|
|
165
|
+
await fetch(url("?reload=true"));
|
|
166
|
+
// Retire the run record so it doesn't hold the per-package active slot.
|
|
167
|
+
await fetch(url(`/materializations/${id}`), { method: "DELETE" });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Write a manifest file keyed by the persist source's real sourceEntityId. */
|
|
171
|
+
async function writeRoutingManifest(
|
|
172
|
+
sourceEntityId: string,
|
|
173
|
+
physicalTableName: string,
|
|
174
|
+
): Promise<string> {
|
|
175
|
+
const file = path.join(tmpDir, `routing-manifest-${Date.now()}.json`);
|
|
176
|
+
await fsp.writeFile(
|
|
177
|
+
file,
|
|
178
|
+
JSON.stringify({
|
|
179
|
+
builtAt: new Date().toISOString(),
|
|
180
|
+
strict: false,
|
|
181
|
+
entries: {
|
|
182
|
+
[sourceEntityId]: {
|
|
183
|
+
sourceEntityId,
|
|
184
|
+
sourceName: "order_summary",
|
|
185
|
+
physicalTableName,
|
|
186
|
+
connectionName: "duckdb",
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
}),
|
|
190
|
+
"utf8",
|
|
191
|
+
);
|
|
192
|
+
return file;
|
|
193
|
+
}
|
|
194
|
+
|
|
105
195
|
async function writeManifest(): Promise<string> {
|
|
106
196
|
const file = path.join(tmpDir, `manifest-${Date.now()}.json`);
|
|
107
197
|
await fsp.writeFile(
|
|
@@ -171,6 +261,48 @@ describe("Manifest binding via Package.manifestLocation (E2E)", () => {
|
|
|
171
261
|
{ timeout: 60_000 },
|
|
172
262
|
);
|
|
173
263
|
|
|
264
|
+
it(
|
|
265
|
+
"routes served queries to the materialized table after a manifest-URI bind",
|
|
266
|
+
async () => {
|
|
267
|
+
// Start from live so the baseline recomputes from the base CSV.
|
|
268
|
+
await getPackage(true);
|
|
269
|
+
expect(await executedSql()).toContain("data/orders.csv");
|
|
270
|
+
|
|
271
|
+
// Physically build the persist source, then revert to live (keeping
|
|
272
|
+
// the table) so the manifest-URI bind below is the only thing that
|
|
273
|
+
// could route the served query.
|
|
274
|
+
await buildTableThenRevertToLive();
|
|
275
|
+
expect(await executedSql()).toContain("data/orders.csv");
|
|
276
|
+
|
|
277
|
+
// Bind the CP-shaped manifest via manifestLocation, keyed by the
|
|
278
|
+
// source's REAL sourceEntityId (the value Malloy recomputes at serve
|
|
279
|
+
// time to resolve the persist reference). Anything else silently misses.
|
|
280
|
+
const sourceEntityId = await orderSummarySourceEntityId();
|
|
281
|
+
const manifestFile = await writeRoutingManifest(
|
|
282
|
+
sourceEntityId,
|
|
283
|
+
"order_summary",
|
|
284
|
+
);
|
|
285
|
+
const patchRes = await patchPackage({
|
|
286
|
+
manifestLocation: manifestFile,
|
|
287
|
+
});
|
|
288
|
+
expect(patchRes.status).toBe(200);
|
|
289
|
+
const patched = await patchRes.json();
|
|
290
|
+
expect(patched.manifestBindingStatus).toBe("bound");
|
|
291
|
+
expect(patched.manifestEntryCount).toBe(1);
|
|
292
|
+
|
|
293
|
+
// The payoff: the served query now scans the materialized table and no
|
|
294
|
+
// longer recomputes from the base CSV.
|
|
295
|
+
const routed = await executedSql();
|
|
296
|
+
expect(routed).not.toContain("data/orders.csv");
|
|
297
|
+
expect(routed).toContain("order_summary");
|
|
298
|
+
|
|
299
|
+
// Cleanup: revert to live so the dangling binding doesn't leak.
|
|
300
|
+
await patchPackage({ manifestLocation: null });
|
|
301
|
+
await getPackage(true);
|
|
302
|
+
},
|
|
303
|
+
{ timeout: 120_000 },
|
|
304
|
+
);
|
|
305
|
+
|
|
174
306
|
it("degrades to serving live when the manifest is unreachable", async () => {
|
|
175
307
|
const missing = path.join(tmpDir, "does-not-exist.json");
|
|
176
308
|
|
|
Binary file
|