@malloy-publisher/server 0.0.208 → 0.0.209

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 (39) hide show
  1. package/dist/app/api-doc.yaml +84 -66
  2. package/dist/app/assets/{EnvironmentPage-DDRxo015.js → EnvironmentPage-BRMCY9d8.js} +1 -1
  3. package/dist/app/assets/HomePage-DQzgkiMI.js +1 -0
  4. package/dist/app/assets/{MainPage-DHVFRXPc.js → MainPage-sZdUjUcu.js} +2 -2
  5. package/dist/app/assets/MaterializationsPage-C_jQQUCJ.js +1 -0
  6. package/dist/app/assets/{ModelPage-B8tF_hYc.js → ModelPage-Bh62OIEq.js} +1 -1
  7. package/dist/app/assets/{PackagePage-LzaaviPn.js → PackagePage-DJW4xjLp.js} +1 -1
  8. package/dist/app/assets/{RouteError-vAYvRAi3.js → RouteError-Vi5yGs_F.js} +1 -1
  9. package/dist/app/assets/{WorkbookPage-CTjs2hXr.js → WorkbookPage-BvJMi21d.js} +1 -1
  10. package/dist/app/assets/{core-AOmIKwkc.es-B29cca-e.js → core-BbW0t3RQ.es-L-mZcOk3.js} +1 -1
  11. package/dist/app/assets/{index-Dj4uKosi.js → index-CCcHdeew.js} +1 -1
  12. package/dist/app/assets/{index-CVGIZdxd.js → index-CNFX-CGL.js} +1 -1
  13. package/dist/app/assets/{index-Db2wvjL3.js → index-DuqTjxM_.js} +114 -114
  14. package/dist/app/assets/{index.umd-BRRXibWA.js → index.umd-GgEb4WfT.js} +1 -1
  15. package/dist/app/index.html +1 -1
  16. package/dist/server.mjs +5822 -304
  17. package/package.json +2 -1
  18. package/src/controller/materialization.controller.spec.ts +125 -0
  19. package/src/controller/materialization.controller.ts +23 -27
  20. package/src/materialization_metrics.ts +117 -34
  21. package/src/server-old.ts +2 -11
  22. package/src/server.ts +2 -10
  23. package/src/service/build_plan.spec.ts +116 -0
  24. package/src/service/build_plan.ts +238 -0
  25. package/src/service/connection.ts +4 -0
  26. package/src/service/connection_config.spec.ts +182 -1
  27. package/src/service/connection_config.ts +70 -0
  28. package/src/service/db_utils.spec.ts +159 -1
  29. package/src/service/db_utils.ts +131 -0
  30. package/src/service/materialization_service.spec.ts +388 -184
  31. package/src/service/materialization_service.ts +156 -442
  32. package/src/service/materialization_test_fixtures.ts +119 -0
  33. package/src/service/package.ts +41 -1
  34. package/src/storage/DatabaseInterface.ts +5 -13
  35. package/src/storage/duckdb/MaterializationRepository.ts +5 -14
  36. package/src/storage/duckdb/schema.ts +4 -5
  37. package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +72 -211
  38. package/dist/app/assets/HomePage-BeIoPOVO.js +0 -1
  39. package/dist/app/assets/MaterializationsPage-BYnr56IV.js +0 -1
@@ -0,0 +1,119 @@
1
+ import type { PersistSource } from "@malloydata/malloy";
2
+ import {
3
+ BuildInstruction,
4
+ BuildPlan,
5
+ Materialization,
6
+ } from "../storage/DatabaseInterface";
7
+ import { CompiledBuildPlan } from "./build_plan";
8
+
9
+ /**
10
+ * Shared fixtures for the materialization unit specs (service, controller,
11
+ * build-plan). Centralizing the builders keeps the per-source stand-ins and
12
+ * record shapes consistent across files and avoids drift when the wire types
13
+ * change.
14
+ */
15
+
16
+ /** A persisted materialization record with sensible PENDING defaults. */
17
+ export function makeMaterialization(
18
+ overrides: Partial<Materialization> = {},
19
+ ): Materialization {
20
+ return {
21
+ id: "mat-1",
22
+ environmentId: "env-1",
23
+ packageName: "pkg",
24
+ status: "PENDING",
25
+ manifest: null,
26
+ startedAt: null,
27
+ completedAt: null,
28
+ error: null,
29
+ metadata: null,
30
+ createdAt: new Date("2026-01-01"),
31
+ updatedAt: new Date("2026-01-01"),
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ /** A single-source wire BuildPlan (the `Package.buildPlan` artifact). */
37
+ export function makeBuildPlan(overrides: Partial<BuildPlan> = {}): BuildPlan {
38
+ return {
39
+ graphs: [
40
+ {
41
+ connectionName: "duckdb",
42
+ nodes: [[{ sourceID: "orders@m.malloy", dependsOn: [] }]],
43
+ },
44
+ ],
45
+ sources: {
46
+ "orders@m.malloy": {
47
+ name: "orders",
48
+ sourceID: "orders@m.malloy",
49
+ connectionName: "duckdb",
50
+ dialect: "duckdb",
51
+ buildId: "build-orders",
52
+ sql: "SELECT 1",
53
+ columns: [],
54
+ },
55
+ },
56
+ ...overrides,
57
+ };
58
+ }
59
+
60
+ /** A caller-supplied build instruction matching {@link makeBuildPlan}. */
61
+ export function makeInstruction(
62
+ overrides: Partial<BuildInstruction> = {},
63
+ ): BuildInstruction {
64
+ return {
65
+ buildId: "build-orders",
66
+ materializedTableId: "mt-1",
67
+ physicalTableName: '"orders_v1"',
68
+ realization: "COPY",
69
+ ...overrides,
70
+ };
71
+ }
72
+
73
+ /**
74
+ * A minimal stand-in for a Malloy {@link PersistSource} exposing only what the
75
+ * build internals touch (name/id, deterministic buildId, SQL, and the
76
+ * `#@ persist name=` annotation reader, defaulted to "unset").
77
+ */
78
+ export function fakeSource(opts: {
79
+ name: string;
80
+ buildId: string;
81
+ sql?: string;
82
+ connectionName?: string;
83
+ }): PersistSource {
84
+ return {
85
+ name: opts.name,
86
+ sourceID: opts.name,
87
+ connectionName: opts.connectionName ?? "duckdb",
88
+ makeBuildId: () => opts.buildId,
89
+ getSQL: () => opts.sql ?? "SELECT 1",
90
+ annotations: {
91
+ parseAsTag: () => ({ tag: { text: () => undefined } }),
92
+ },
93
+ } as unknown as PersistSource;
94
+ }
95
+
96
+ /**
97
+ * Assemble a {@link CompiledBuildPlan} from a sources map and dependency
98
+ * levels (one connection, "duckdb"). `connections` is supplied by the caller
99
+ * because build vs. plan-derivation tests need different connection mocks.
100
+ */
101
+ export function compiledWith(
102
+ sources: Record<string, PersistSource>,
103
+ levels: string[][],
104
+ connections: CompiledBuildPlan["connections"] = new Map(),
105
+ ): CompiledBuildPlan {
106
+ return {
107
+ graphs: [
108
+ {
109
+ connectionName: "duckdb",
110
+ nodes: levels.map((level) =>
111
+ level.map((sourceID) => ({ sourceID, dependsOn: [] })),
112
+ ),
113
+ },
114
+ ] as unknown as CompiledBuildPlan["graphs"],
115
+ sources,
116
+ connectionDigests: { duckdb: "dig" },
117
+ connections,
118
+ };
119
+ }
@@ -28,9 +28,11 @@ import {
28
28
  ServiceUnavailableError,
29
29
  } from "../errors";
30
30
  import { formatDuration, logger } from "../logger";
31
+ import { recordBuildPlanComputeDuration } from "../materialization_metrics";
31
32
  import { assertSafeEnvironmentPath, safeJoinUnderRoot } from "../path_safety";
32
- import { BuildManifest } from "../storage/DatabaseInterface";
33
+ import { BuildManifest, BuildPlan } from "../storage/DatabaseInterface";
33
34
  import { ignoreDotfiles } from "../utils";
35
+ import { computePackageBuildPlan } from "./build_plan";
34
36
  import { Model } from "./model";
35
37
 
36
38
  type ApiDatabase = components["schemas"]["Database"];
@@ -67,6 +69,13 @@ export class Package {
67
69
  "unbound";
68
70
  private manifestEntryCount = 0;
69
71
  private boundManifestUri: string | null = null;
72
+ // The package's persist build plan: a deterministic property of the compiled
73
+ // package (per-source buildId, columns, build SQL, dependency graphs),
74
+ // computed once at load from the live (unbound) models so it is stable for a
75
+ // given (package version, connection config). Null when the package declares
76
+ // no persist source. Surfaced read-only on getPackageMetadata() so a caller
77
+ // can derive build instructions without a separate plan round-trip.
78
+ private buildPlan: BuildPlan | null = null;
70
79
  private static meter = metrics.getMeter("publisher");
71
80
  private static packageLoadHistogram = this.meter.createHistogram(
72
81
  "malloy_package_load_duration",
@@ -377,6 +386,26 @@ export class Package {
377
386
  malloyConfig,
378
387
  );
379
388
 
389
+ // Compute the persist build plan off the live (unbound) models, before the
390
+ // caller binds any configured manifest, so the surfaced plan reflects the
391
+ // canonical build (not the manifest-rewritten SQL). Best-effort: a plan
392
+ // failure is logged, not fatal — the package still serves; the plan is
393
+ // just absent. Recompiles the models (duplicate schema RPCs vs the worker
394
+ // compile); accepted for now.
395
+ try {
396
+ const buildPlanStart = Date.now();
397
+ pkg.buildPlan = await computePackageBuildPlan(pkg);
398
+ recordBuildPlanComputeDuration(Date.now() - buildPlanStart);
399
+ } catch (err) {
400
+ logger.warn(
401
+ `Failed to compute build plan for package ${packageName}`,
402
+ {
403
+ packageName,
404
+ error: err instanceof Error ? err.message : String(err),
405
+ },
406
+ );
407
+ }
408
+
380
409
  // Fail-safe at load: a bad explores entry doesn't fail the package
381
410
  // (its models still load and listModels hides the unmatched entry — it
382
411
  // never falls back to listing everything). Warn so the misconfig is
@@ -397,6 +426,16 @@ export class Package {
397
426
  return this.packageName;
398
427
  }
399
428
 
429
+ /**
430
+ * The package's persist build plan (per-source buildId, columns, build SQL,
431
+ * dependency graphs), or null when the package declares no persist source.
432
+ * A deterministic property of the compiled package; callers derive build
433
+ * instructions from it for an orchestrated materialization.
434
+ */
435
+ public getBuildPlan(): BuildPlan | null {
436
+ return this.buildPlan;
437
+ }
438
+
400
439
  public getPackageMetadata(): ApiPackage {
401
440
  // Overlay the server-computed fields onto the stored metadata: the
402
441
  // explores misconfig warnings (loading is fail-safe — the package still
@@ -418,6 +457,7 @@ export class Package {
418
457
  manifestBindingStatus: this.manifestBindingStatus,
419
458
  manifestEntryCount: this.manifestEntryCount,
420
459
  boundManifestUri: this.boundManifestUri,
460
+ buildPlan: this.buildPlan,
421
461
  };
422
462
  const warnings = this.exploreWarnings();
423
463
  if (warnings.length > 0) {
@@ -108,8 +108,8 @@ export interface Connection {
108
108
  updatedAt: Date;
109
109
  }
110
110
 
111
- // Wire types for the two-round build protocol, kept in sync with the OpenAPI
112
- // spec via the generated `api.ts`.
111
+ // Wire types for the build protocol, kept in sync with the OpenAPI spec via
112
+ // the generated `api.ts`.
113
113
  export type MaterializationStatus =
114
114
  components["schemas"]["MaterializationStatus"];
115
115
  export type BuildPlan = components["schemas"]["BuildPlan"];
@@ -122,15 +122,8 @@ export interface Materialization {
122
122
  id: string;
123
123
  environmentId: string;
124
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;
130
125
  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. */
126
+ /** Build output. Null until status = MANIFEST_FILE_READY. */
134
127
  manifest: BuildManifestResult | null;
135
128
  startedAt: Date | null;
136
129
  completedAt: Date | null;
@@ -142,7 +135,6 @@ export interface Materialization {
142
135
 
143
136
  export interface MaterializationUpdate {
144
137
  status?: MaterializationStatus;
145
- buildPlan?: BuildPlan | null;
146
138
  manifest?: BuildManifestResult | null;
147
139
  startedAt?: Date;
148
140
  completedAt?: Date;
@@ -154,8 +146,8 @@ export interface MaterializationUpdate {
154
146
  * Malloy-facing build manifest: maps a buildId to the physical table backing
155
147
  * that persist source. This is the shape the Malloy runtime consumes when
156
148
  * (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).
149
+ * Distinct from {@link BuildManifestResult} (the wire build output, which also
150
+ * carries caller bookkeeping like materializedTableId and rowCount).
159
151
  */
160
152
  export interface BuildManifestEntry {
161
153
  tableName: string;
@@ -1,6 +1,5 @@
1
1
  import {
2
2
  BuildManifestResult,
3
- BuildPlan,
4
3
  Materialization,
5
4
  MaterializationStatus,
6
5
  MaterializationUpdate,
@@ -16,7 +15,6 @@ const TERMINAL_STATUSES: ReadonlySet<MaterializationStatus> = new Set([
16
15
  /** Non-terminal statuses count as "active" for the single-active guard. */
17
16
  const ACTIVE_STATUSES: readonly MaterializationStatus[] = [
18
17
  "PENDING",
19
- "BUILD_PLAN_READY",
20
18
  "MANIFEST_ROWS_READY",
21
19
  ];
22
20
 
@@ -40,8 +38,9 @@ export class DuplicateActiveMaterializationError extends Error {
40
38
  /**
41
39
  * DuckDB-backed repository for package materializations.
42
40
  *
43
- * A Materialization tracks a single build run for an (environment, package) pair
44
- * through its lifecycle: PENDING -> RUNNING -> SUCCESS | FAILED | CANCELLED.
41
+ * A Materialization tracks a single build run for an (environment, package)
42
+ * pair through its lifecycle: PENDING -> MANIFEST_ROWS_READY ->
43
+ * MANIFEST_FILE_READY, or -> FAILED | CANCELLED.
45
44
  */
46
45
  export class MaterializationRepository {
47
46
  constructor(private db: DuckDBConnection) {}
@@ -114,8 +113,8 @@ export class MaterializationRepository {
114
113
 
115
114
  try {
116
115
  const rows = await this.db.all<Record<string, unknown>>(
117
- `INSERT INTO materializations (id, environment_id, package_name, status, active_key, metadata, build_plan, manifest, created_at, updated_at)
118
- VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?)
116
+ `INSERT INTO materializations (id, environment_id, package_name, status, active_key, metadata, manifest, created_at, updated_at)
117
+ VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?)
119
118
  RETURNING *`,
120
119
  [
121
120
  id,
@@ -181,12 +180,6 @@ export class MaterializationRepository {
181
180
  updates.metadata ? JSON.stringify(updates.metadata) : null,
182
181
  );
183
182
  }
184
- if (updates.buildPlan !== undefined) {
185
- setClauses.push(`build_plan = ?`);
186
- params.push(
187
- updates.buildPlan ? JSON.stringify(updates.buildPlan) : null,
188
- );
189
- }
190
183
  if (updates.manifest !== undefined) {
191
184
  setClauses.push(`manifest = ?`);
192
185
  params.push(
@@ -237,9 +230,7 @@ export class MaterializationRepository {
237
230
  id: row.id as string,
238
231
  environmentId: row.environment_id as string,
239
232
  packageName: row.package_name as string,
240
- pauseBetweenPhases: metadata?.pauseBetweenPhases === true,
241
233
  status: row.status as MaterializationStatus,
242
- buildPlan: parseJsonColumn<BuildPlan>(row.build_plan),
243
234
  manifest: parseJsonColumn<BuildManifestResult>(row.manifest),
244
235
  metadata,
245
236
  startedAt: row.started_at ? new Date(row.started_at as string) : null,
@@ -75,15 +75,15 @@ export async function initializeSchema(
75
75
 
76
76
  // Materializations table.
77
77
  //
78
- // `active_key` enforces at-most-one active (PENDING or RUNNING)
79
- // materialization per (environment, package) at the DB layer. It is set to
78
+ // `active_key` enforces at-most-one active (non-terminal) materialization
79
+ // per (environment, package) at the DB layer. It is set to
80
80
  // `{environment_id}|{package_name}` while the row is active and cleared
81
81
  // to NULL on transition to any terminal state. A unique index on
82
82
  // `active_key` (see below) makes the insert-then-check race impossible —
83
83
  // a second concurrent create fails with a constraint violation, which the
84
84
  // service layer translates to `MaterializationConflictError`.
85
- // `build_plan` (Round 1) and `manifest` (Round 2) are JSON blobs holding
86
- // the two-round protocol payloads returned inline on the resource.
85
+ // `manifest` is a JSON blob holding the build output returned inline on the
86
+ // resource.
87
87
  await db.run(`
88
88
  CREATE TABLE IF NOT EXISTS materializations (
89
89
  id VARCHAR PRIMARY KEY,
@@ -95,7 +95,6 @@ export async function initializeSchema(
95
95
  completed_at TIMESTAMP,
96
96
  error TEXT,
97
97
  metadata JSON,
98
- build_plan JSON,
99
98
  manifest JSON,
100
99
  created_at TIMESTAMP NOT NULL,
101
100
  updated_at TIMESTAMP NOT NULL,