@malloy-publisher/server 0.0.215 → 0.0.216
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/api-doc.yaml
CHANGED
|
@@ -2443,7 +2443,12 @@ components:
|
|
|
2443
2443
|
description: |
|
|
2444
2444
|
Package-level Malloy Persistence policy declared in
|
|
2445
2445
|
malloy-publisher.json. The control plane reads it to drive scheduled
|
|
2446
|
-
re-materialization.
|
|
2446
|
+
re-materialization. The object is present whenever the package has
|
|
2447
|
+
loaded — with `schedule: null` when the manifest declares no
|
|
2448
|
+
schedule — so the caller can treat object-present as the
|
|
2449
|
+
authoritative manifest policy. Null/absent only when package
|
|
2450
|
+
metadata is unavailable for this request (not loaded), which the
|
|
2451
|
+
caller must treat as "unknown", not as a removal.
|
|
2447
2452
|
manifestBindingStatus:
|
|
2448
2453
|
type: string
|
|
2449
2454
|
readOnly: true
|
|
@@ -2499,14 +2504,17 @@ components:
|
|
|
2499
2504
|
Package-level Malloy Persistence policy from malloy-publisher.json's
|
|
2500
2505
|
`materialization` block. Surfaced verbatim so the control plane can drive
|
|
2501
2506
|
scheduled version-level re-materialization without re-reading the package
|
|
2502
|
-
files.
|
|
2507
|
+
files. The object is emitted whenever the package is loaded, even when no
|
|
2508
|
+
policy is declared (all fields null), so its presence is an authoritative
|
|
2509
|
+
"the manifest says this" signal rather than "policy missing".
|
|
2503
2510
|
properties:
|
|
2504
2511
|
schedule:
|
|
2505
2512
|
type: ["string", "null"]
|
|
2506
2513
|
description: >-
|
|
2507
2514
|
5-field UNIX cron controlling how often the control plane
|
|
2508
|
-
re-materializes this package's published versions. Null
|
|
2509
|
-
scheduled re-materialization (publish /
|
|
2515
|
+
re-materializes this package's published versions. Null = no
|
|
2516
|
+
scheduled re-materialization declared in the manifest (publish /
|
|
2517
|
+
on-demand only).
|
|
2510
2518
|
|
|
2511
2519
|
Model:
|
|
2512
2520
|
type: object
|
package/dist/server.mjs
CHANGED
|
@@ -239566,7 +239566,9 @@ class Package {
|
|
|
239566
239566
|
explores: outcome.packageMetadata.explores,
|
|
239567
239567
|
queryableSources: outcome.packageMetadata.queryableSources,
|
|
239568
239568
|
manifestLocation: outcome.packageMetadata.manifestLocation ?? null,
|
|
239569
|
-
materialization: outcome.packageMetadata.materialization ??
|
|
239569
|
+
materialization: outcome.packageMetadata.materialization ?? {
|
|
239570
|
+
schedule: null
|
|
239571
|
+
}
|
|
239570
239572
|
};
|
|
239571
239573
|
const models = new Map;
|
|
239572
239574
|
for (const sm of outcome.models) {
|
|
@@ -240514,7 +240516,8 @@ ${source}` : source;
|
|
|
240514
240516
|
location: body.location,
|
|
240515
240517
|
explores,
|
|
240516
240518
|
queryableSources,
|
|
240517
|
-
manifestLocation
|
|
240519
|
+
manifestLocation,
|
|
240520
|
+
materialization: existing.materialization
|
|
240518
240521
|
});
|
|
240519
240522
|
const invalidMsg = _package.formatInvalidExplores();
|
|
240520
240523
|
if (invalidMsg) {
|
package/package.json
CHANGED
|
@@ -1340,6 +1340,13 @@ export class Environment {
|
|
|
1340
1340
|
explores,
|
|
1341
1341
|
queryableSources,
|
|
1342
1342
|
manifestLocation,
|
|
1343
|
+
// Carry the manifest-derived materialization policy through the
|
|
1344
|
+
// PATCH. `setPackageMetadata` replaces the whole object, so omitting
|
|
1345
|
+
// this wipes the in-memory `materialization` until the next reload —
|
|
1346
|
+
// which makes a later getPackage report no schedule and lets the
|
|
1347
|
+
// control plane misread the gap as a schedule removal. It is not a
|
|
1348
|
+
// PATCH-editable field, so always preserve the existing value.
|
|
1349
|
+
materialization: existing.materialization,
|
|
1343
1350
|
});
|
|
1344
1351
|
|
|
1345
1352
|
// Strict-reject, symmetric with the publish path
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import * as fs from "fs/promises";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
|
|
6
|
+
import { Environment } from "./environment";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The publisher must surface a non-null `materialization` object on a loaded
|
|
10
|
+
* package's metadata so the control plane can treat object-present as the
|
|
11
|
+
* authoritative manifest policy ("this is what the manifest says") and
|
|
12
|
+
* object-absent as "metadata not loaded this request" — never as a schedule
|
|
13
|
+
* removal. A metadata PATCH must not drop that policy from the in-memory
|
|
14
|
+
* metadata. Regression coverage for the re-materialization schedule self-wipe
|
|
15
|
+
* (docs/bugs/materialization-schedule-self-wipe.md).
|
|
16
|
+
*
|
|
17
|
+
* Runs against a real `Environment` + real `Package.create` over temp dirs.
|
|
18
|
+
*/
|
|
19
|
+
describe("materialization schedule surfacing", () => {
|
|
20
|
+
const MODEL = `source: ones is duckdb.sql("SELECT 1 as x")\n`;
|
|
21
|
+
let rootDir: string;
|
|
22
|
+
let envPath: string;
|
|
23
|
+
|
|
24
|
+
async function writePackageDir(
|
|
25
|
+
manifest: Record<string, unknown>,
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
const dir = path.join(envPath, "pkg");
|
|
28
|
+
await fs.mkdir(dir, { recursive: true });
|
|
29
|
+
await fs.writeFile(
|
|
30
|
+
path.join(dir, "publisher.json"),
|
|
31
|
+
JSON.stringify({ name: "pkg", description: "fixture", ...manifest }),
|
|
32
|
+
);
|
|
33
|
+
await fs.writeFile(path.join(dir, "model.malloy"), MODEL);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
beforeEach(async () => {
|
|
37
|
+
rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "publisher-mat-"));
|
|
38
|
+
envPath = path.join(rootDir, "env");
|
|
39
|
+
await fs.mkdir(envPath, { recursive: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(async () => {
|
|
43
|
+
await fs.rm(rootDir, { recursive: true, force: true }).catch(() => {});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it(
|
|
47
|
+
"surfaces the manifest's materialization.schedule on load",
|
|
48
|
+
async () => {
|
|
49
|
+
const env = await Environment.create("testEnv", envPath, []);
|
|
50
|
+
await writePackageDir({ materialization: { schedule: "0 6 * * *" } });
|
|
51
|
+
await env.addPackage("pkg");
|
|
52
|
+
|
|
53
|
+
const pkg = await env.getPackage("pkg", false);
|
|
54
|
+
expect(pkg.getPackageMetadata().materialization).toEqual({
|
|
55
|
+
schedule: "0 6 * * *",
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
{ timeout: 20000 },
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
it(
|
|
62
|
+
"surfaces a non-null materialization object even when the manifest declares none",
|
|
63
|
+
async () => {
|
|
64
|
+
const env = await Environment.create("testEnv", envPath, []);
|
|
65
|
+
await writePackageDir({});
|
|
66
|
+
await env.addPackage("pkg");
|
|
67
|
+
|
|
68
|
+
// Object present with a null schedule — NOT a null object — so the
|
|
69
|
+
// control plane reads it as an authoritative "no schedule declared",
|
|
70
|
+
// not as "metadata unavailable".
|
|
71
|
+
const pkg = await env.getPackage("pkg", false);
|
|
72
|
+
expect(pkg.getPackageMetadata().materialization).toEqual({
|
|
73
|
+
schedule: null,
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
{ timeout: 20000 },
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
it(
|
|
80
|
+
"preserves the materialization policy across a metadata PATCH",
|
|
81
|
+
async () => {
|
|
82
|
+
const env = await Environment.create("testEnv", envPath, []);
|
|
83
|
+
await writePackageDir({ materialization: { schedule: "0 6 * * *" } });
|
|
84
|
+
await env.addPackage("pkg");
|
|
85
|
+
|
|
86
|
+
// A description-only PATCH must not wipe the schedule from the
|
|
87
|
+
// in-memory metadata. The bug: setPackageMetadata replaces the whole
|
|
88
|
+
// object, so a later getPackage reported no schedule and the control
|
|
89
|
+
// plane misread the gap as a removal and cleared the cadence.
|
|
90
|
+
const updated = await env.updatePackage("pkg", {
|
|
91
|
+
name: "pkg",
|
|
92
|
+
description: "updated",
|
|
93
|
+
});
|
|
94
|
+
expect(updated.materialization).toEqual({ schedule: "0 6 * * *" });
|
|
95
|
+
|
|
96
|
+
const pkg = await env.getPackage("pkg", false);
|
|
97
|
+
expect(pkg.getPackageMetadata().materialization).toEqual({
|
|
98
|
+
schedule: "0 6 * * *",
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
{ timeout: 20000 },
|
|
102
|
+
);
|
|
103
|
+
});
|
package/src/service/package.ts
CHANGED
|
@@ -331,7 +331,16 @@ export class Package {
|
|
|
331
331
|
explores: outcome.packageMetadata.explores,
|
|
332
332
|
queryableSources: outcome.packageMetadata.queryableSources,
|
|
333
333
|
manifestLocation: outcome.packageMetadata.manifestLocation ?? null,
|
|
334
|
-
|
|
334
|
+
// Always surface a non-null `materialization` object once the package
|
|
335
|
+
// has loaded (schedule null when the manifest declares no policy). The
|
|
336
|
+
// control plane treats object-present as the authoritative "this is
|
|
337
|
+
// what the manifest says" signal and object-absent as "metadata not
|
|
338
|
+
// available this request" — so it must never be dropped to null on a
|
|
339
|
+
// successfully loaded package, or the CP can misread a transient
|
|
340
|
+
// absence as a schedule removal. See `parsePackageMaterialization`.
|
|
341
|
+
materialization: outcome.packageMetadata.materialization ?? {
|
|
342
|
+
schedule: null,
|
|
343
|
+
},
|
|
335
344
|
};
|
|
336
345
|
|
|
337
346
|
// Build live `Model`s from worker output. Any per-model compile
|