@malloy-publisher/server 0.0.214 → 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.
@@ -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. Null/absent = no package-level policy.
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/absent = no
2509
- scheduled re-materialization (publish / on-demand only).
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 ?? null
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) {
@@ -248857,8 +248860,12 @@ class MaterializationService {
248857
248860
  }
248858
248861
  async executeInstructedBuild(compiled, instructions, seedEntries, signal) {
248859
248862
  const { graphs, sources, connectionDigests, connections } = compiled;
248863
+ const bySourceID = new Map;
248860
248864
  const byBuildId = new Map;
248861
248865
  for (const instruction of instructions) {
248866
+ if (instruction.sourceID) {
248867
+ bySourceID.set(instruction.sourceID, instruction);
248868
+ }
248862
248869
  byBuildId.set(instruction.buildId, instruction);
248863
248870
  }
248864
248871
  const manifest = new Manifest;
@@ -248878,7 +248885,7 @@ class MaterializationService {
248878
248885
  if (signal.aborted)
248879
248886
  throw new Error("Build cancelled");
248880
248887
  const buildId = computeBuildId(persistSource, connectionDigests);
248881
- const instruction = byBuildId.get(buildId);
248888
+ const instruction = bySourceID.get(persistSource.sourceID) ?? byBuildId.get(buildId);
248882
248889
  if (!instruction)
248883
248890
  continue;
248884
248891
  const entry = await this.buildOneSource(persistSource, instruction, connection, connectionDigests, manifest);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@malloy-publisher/server",
3
3
  "description": "Malloy Publisher Server",
4
- "version": "0.0.214",
4
+ "version": "0.0.216",
5
5
  "main": "dist/server.mjs",
6
6
  "bin": {
7
7
  "malloy-publisher": "dist/server.mjs"
@@ -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
+ });
@@ -560,8 +560,20 @@ export class MaterializationService {
560
560
  ): Promise<Record<string, ManifestEntry>> {
561
561
  const { graphs, sources, connectionDigests, connections } = compiled;
562
562
 
563
+ // Index instructions by sourceID (the stable per-source handle) so the
564
+ // build no longer recomputes the buildId to find an instruction.
565
+ // Recomputing it here forced a caller's buildId to equal the publisher's
566
+ // content hash, so a caller that derives buildIds by any other scheme
567
+ // would have its sources silently skipped (the recomputed buildId would
568
+ // not match the instruction). buildId is treated as opaque, caller-assigned
569
+ // identity. A buildId index is kept as a fallback for instructions without
570
+ // a sourceID (e.g. standalone auto-run).
571
+ const bySourceID = new Map<string, BuildInstruction>();
563
572
  const byBuildId = new Map<string, BuildInstruction>();
564
573
  for (const instruction of instructions) {
574
+ if (instruction.sourceID) {
575
+ bySourceID.set(instruction.sourceID, instruction);
576
+ }
565
577
  byBuildId.set(instruction.buildId, instruction);
566
578
  }
567
579
 
@@ -587,8 +599,15 @@ export class MaterializationService {
587
599
  for (const persistSource of iterGraphSources(graph, sources)) {
588
600
  if (signal.aborted) throw new Error("Build cancelled");
589
601
 
602
+ // The manifest is keyed by the content buildId — what Malloy
603
+ // recomputes to resolve upstream persist references during SQL
604
+ // generation — independent of the instruction's identity buildId.
590
605
  const buildId = computeBuildId(persistSource, connectionDigests);
591
- const instruction = byBuildId.get(buildId);
606
+ // Prefer sourceID matching (so the caller's buildId scheme stays
607
+ // opaque to the build); fall back to buildId for instructions
608
+ // without a sourceID (auto-run).
609
+ const instruction =
610
+ bySourceID.get(persistSource.sourceID) ?? byBuildId.get(buildId);
592
611
  if (!instruction) continue;
593
612
 
594
613
  const entry = await this.buildOneSource(
@@ -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
- materialization: outcome.packageMetadata.materialization ?? null,
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