@malloy-publisher/server 0.0.225 → 0.0.226
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 +29 -24
- package/dist/app/assets/{EnvironmentPage-DYTeXDll.js → EnvironmentPage-DvOJ7L_b.js} +1 -1
- package/dist/app/assets/{HomePage-pDK2BPJY.js → HomePage-CXguJsXS.js} +1 -1
- package/dist/app/assets/{LightMode-C2bwGPY1.js → LightMode-ZsshUznu.js} +1 -1
- package/dist/app/assets/{MainPage-WtBulMH_.js → MainPage-BIe0VwBa.js} +2 -2
- package/dist/app/assets/{MaterializationsPage-hMgOtflG.js → MaterializationsPage-BuZ6UJVx.js} +1 -1
- package/dist/app/assets/{ModelPage-B2N5kYII.js → ModelPage-DsPf-s8B.js} +1 -1
- package/dist/app/assets/{PackagePage-CEN90nQG.js → PackagePage-CEVNAKZa.js} +1 -1
- package/dist/app/assets/{RouteError-BG2c5Zf0.js → RouteError-Chn7lL96.js} +1 -1
- package/dist/app/assets/{ThemeEditorPage-DNfeUwEZ.js → ThemeEditorPage-DWC_FdNU.js} +1 -1
- package/dist/app/assets/{WorkbookPage-NKI1BhFS.js → WorkbookPage-CGrsFz8p.js} +1 -1
- package/dist/app/assets/{core-C6anj5c0.es-DDLHqpzt.js → core-vVgoO8IR.es-BD_THWs_.js} +1 -1
- package/dist/app/assets/{index-DMQtnaf4.js → index-BioohWQj.js} +1 -1
- package/dist/app/assets/{index-CzNqKMVl.js → index-D6YtyiJ0.js} +1 -1
- package/dist/app/assets/{index-C6gZ6sSY.js → index-DNUZpnaa.js} +4 -4
- package/dist/app/assets/{index-JXgvyZna.js → index-gEWxu09x.js} +1 -1
- package/dist/app/index.html +1 -1
- package/dist/instrumentation.mjs +71 -15
- package/dist/package_load_worker.mjs +83 -16
- package/dist/server.mjs +128 -57
- package/package.json +1 -1
- package/src/controller/package.controller.spec.ts +17 -17
- package/src/controller/package.controller.ts +7 -6
- package/src/logger.spec.ts +193 -0
- package/src/logger.ts +115 -18
- package/src/mcp/skills/skills_bundle.json +1 -1
- package/src/package_load/package_load_pool.ts +5 -1
- package/src/package_load/package_load_worker.ts +7 -0
- package/src/package_load/protocol.ts +5 -1
- package/src/service/build_plan.spec.ts +42 -60
- package/src/service/build_plan.ts +37 -61
- package/src/service/environment.ts +4 -0
- package/src/service/materialization_test_fixtures.ts +6 -8
- package/src/service/package.ts +83 -44
- package/src/service/package_manifest.spec.ts +22 -1
- package/src/service/package_manifest.ts +29 -0
- package/src/service/persistence_policy.spec.ts +234 -0
- package/src/service/materialization_cron_gate.spec.ts +0 -181
|
@@ -81,7 +81,10 @@ import { fileURLToPath, pathToFileURL } from "url";
|
|
|
81
81
|
|
|
82
82
|
import { ModelCompilationError } from "../errors";
|
|
83
83
|
import { logger } from "../logger";
|
|
84
|
-
import type {
|
|
84
|
+
import type {
|
|
85
|
+
PackageMaterializationConfig,
|
|
86
|
+
PackageScope,
|
|
87
|
+
} from "../service/package_manifest";
|
|
85
88
|
import type {
|
|
86
89
|
ConnectionMetadataRequest,
|
|
87
90
|
ConnectionMetadataResponse,
|
|
@@ -235,6 +238,7 @@ export interface LoadPackageOutcome {
|
|
|
235
238
|
queryableSources?: "declared" | "all";
|
|
236
239
|
manifestLocation?: string | null;
|
|
237
240
|
materialization?: PackageMaterializationConfig | null;
|
|
241
|
+
scope?: PackageScope;
|
|
238
242
|
};
|
|
239
243
|
models: Array<
|
|
240
244
|
Omit<SerializedModel, "modelDef" | "sourceInfos"> & {
|
|
@@ -85,7 +85,9 @@ import { validateAuthorizeProbes } from "../service/authorize";
|
|
|
85
85
|
import { type FilterDefinition } from "../service/filter";
|
|
86
86
|
import {
|
|
87
87
|
PackageMaterializationConfig,
|
|
88
|
+
PackageScope,
|
|
88
89
|
parsePackageMaterialization,
|
|
90
|
+
parsePackageScope,
|
|
89
91
|
} from "../service/package_manifest";
|
|
90
92
|
import {
|
|
91
93
|
extractQueriesFromModelDef,
|
|
@@ -387,6 +389,7 @@ async function readPackageMetadata(packagePath: string): Promise<{
|
|
|
387
389
|
queryableSources?: "declared" | "all";
|
|
388
390
|
manifestLocation?: string | null;
|
|
389
391
|
materialization?: PackageMaterializationConfig | null;
|
|
392
|
+
scope?: PackageScope;
|
|
390
393
|
}> {
|
|
391
394
|
const manifestPath = path.join(packagePath, PACKAGE_MANIFEST_NAME);
|
|
392
395
|
const contents = await fs.promises.readFile(manifestPath, "utf8");
|
|
@@ -397,6 +400,7 @@ async function readPackageMetadata(packagePath: string): Promise<{
|
|
|
397
400
|
queryableSources?: unknown;
|
|
398
401
|
manifestLocation?: unknown;
|
|
399
402
|
materialization?: unknown;
|
|
403
|
+
scope?: unknown;
|
|
400
404
|
};
|
|
401
405
|
return {
|
|
402
406
|
name: parsed.name,
|
|
@@ -416,6 +420,9 @@ async function readPackageMetadata(packagePath: string): Promise<{
|
|
|
416
420
|
// Package-level Malloy Persistence policy; surfaced to the control plane,
|
|
417
421
|
// which owns scheduling. Only `schedule` is read today.
|
|
418
422
|
materialization: parsePackageMaterialization(parsed.materialization),
|
|
423
|
+
// Package-level persist scope mode; defaults to "package". An invalid
|
|
424
|
+
// value throws here and fails the load (scope is load-bearing).
|
|
425
|
+
scope: parsePackageScope(parsed.scope),
|
|
419
426
|
};
|
|
420
427
|
}
|
|
421
428
|
|
|
@@ -65,7 +65,10 @@
|
|
|
65
65
|
*/
|
|
66
66
|
|
|
67
67
|
import type { SQLSourceDef, TableSourceDef } from "@malloydata/malloy";
|
|
68
|
-
import type {
|
|
68
|
+
import type {
|
|
69
|
+
PackageMaterializationConfig,
|
|
70
|
+
PackageScope,
|
|
71
|
+
} from "../service/package_manifest";
|
|
69
72
|
|
|
70
73
|
// ──────────────────────────────────────────────────────────────────────
|
|
71
74
|
// Direction: main ──▶ worker (load-package job)
|
|
@@ -181,6 +184,7 @@ export interface LoadPackageResult {
|
|
|
181
184
|
queryableSources?: "declared" | "all";
|
|
182
185
|
manifestLocation?: string | null;
|
|
183
186
|
materialization?: PackageMaterializationConfig | null;
|
|
187
|
+
scope?: PackageScope;
|
|
184
188
|
};
|
|
185
189
|
models: SerializedModel[];
|
|
186
190
|
/** Wall-clock ms inside the worker for the full package load. */
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
deriveBuildPlan,
|
|
11
11
|
flattenDependsOn,
|
|
12
12
|
iterGraphSources,
|
|
13
|
-
|
|
13
|
+
resolveFreshness,
|
|
14
14
|
resolvePackageConnections,
|
|
15
15
|
} from "./build_plan";
|
|
16
16
|
import { fakeSource } from "./materialization_test_fixtures";
|
|
@@ -213,10 +213,10 @@ describe("deriveBuildPlan", () => {
|
|
|
213
213
|
});
|
|
214
214
|
});
|
|
215
215
|
|
|
216
|
-
it("reports declared
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
// the
|
|
216
|
+
it("reports declared refresh verbatim (null when unset) and does not emit sharing/schedule", () => {
|
|
217
|
+
// `refresh` is a metadata pass-through; `sharing`/`schedule` were retired
|
|
218
|
+
// from the contract and must not be emitted as typed fields (they stay in
|
|
219
|
+
// the raw annotationFields for the publish-time validator to detect).
|
|
220
220
|
const declared = fakeSource({
|
|
221
221
|
name: "declared",
|
|
222
222
|
sourceEntityId: "bid-d",
|
|
@@ -243,17 +243,22 @@ describe("deriveBuildPlan", () => {
|
|
|
243
243
|
{ duckdb: "dig" },
|
|
244
244
|
);
|
|
245
245
|
|
|
246
|
-
expect(plan.sources["declared@m"].sharing).toBe("private");
|
|
247
246
|
expect(plan.sources["declared@m"].refresh).toBe("incremental");
|
|
248
|
-
//
|
|
249
|
-
|
|
247
|
+
// Retired typed fields are absent from the wire projection.
|
|
248
|
+
expect(
|
|
249
|
+
(plan.sources["declared@m"] as Record<string, unknown>).sharing,
|
|
250
|
+
).toBeUndefined();
|
|
251
|
+
expect(
|
|
252
|
+
(plan.sources["declared@m"] as Record<string, unknown>).schedule,
|
|
253
|
+
).toBeUndefined();
|
|
254
|
+
// The raw annotation map still carries every field (so the validator can
|
|
255
|
+
// reject a source-level sharing/schedule at publish).
|
|
250
256
|
expect(plan.sources["declared@m"].annotationFields).toEqual({
|
|
251
257
|
name: "d_table",
|
|
252
258
|
sharing: "private",
|
|
253
259
|
refresh: "incremental",
|
|
254
260
|
});
|
|
255
|
-
// Unset is null
|
|
256
|
-
expect(plan.sources["unset@m"].sharing).toBeNull();
|
|
261
|
+
// Unset refresh is null on the wire.
|
|
257
262
|
expect(plan.sources["unset@m"].refresh).toBeNull();
|
|
258
263
|
});
|
|
259
264
|
|
|
@@ -297,71 +302,48 @@ describe("deriveBuildPlan", () => {
|
|
|
297
302
|
});
|
|
298
303
|
});
|
|
299
304
|
|
|
300
|
-
describe("
|
|
301
|
-
it("reports source-level freshness
|
|
305
|
+
describe("resolveFreshness", () => {
|
|
306
|
+
it("reports source-level freshness verbatim", () => {
|
|
302
307
|
const source = fakeSource({
|
|
303
308
|
name: "s",
|
|
304
309
|
sourceEntityId: "bid",
|
|
305
310
|
freshnessSchedule: {
|
|
306
311
|
freshness: { window: "1h", fallback: "stale_ok" },
|
|
307
|
-
schedule: "0 */6 * * *",
|
|
308
312
|
},
|
|
309
313
|
});
|
|
310
|
-
expect(
|
|
311
|
-
|
|
312
|
-
|
|
314
|
+
expect(resolveFreshness(source, null)).toEqual({
|
|
315
|
+
window: "1h",
|
|
316
|
+
fallback: "stale_ok",
|
|
313
317
|
});
|
|
314
318
|
});
|
|
315
319
|
|
|
316
|
-
it("returns null
|
|
320
|
+
it("returns null when unset at every level", () => {
|
|
317
321
|
const source = fakeSource({ name: "s", sourceEntityId: "bid" });
|
|
318
|
-
expect(
|
|
319
|
-
freshness: null,
|
|
320
|
-
schedule: null,
|
|
321
|
-
});
|
|
322
|
+
expect(resolveFreshness(source, null)).toBeNull();
|
|
322
323
|
});
|
|
323
324
|
|
|
324
325
|
it("falls back to model-file then package per field (most-specific-wins)", () => {
|
|
325
|
-
// freshness.window from source, freshness.fallback from model-file.
|
|
326
|
-
// package-level cron is NOT inherited as the source's effective schedule;
|
|
327
|
-
// schedule here comes only from the source's own model-file default.
|
|
326
|
+
// freshness.window from source, freshness.fallback from model-file.
|
|
328
327
|
const source = fakeSource({
|
|
329
328
|
name: "s",
|
|
330
329
|
sourceEntityId: "bid",
|
|
331
330
|
freshnessSchedule: { freshness: { window: "1h" } },
|
|
332
|
-
modelFreshnessSchedule: {
|
|
333
|
-
freshness: { fallback: "fail" },
|
|
334
|
-
schedule: "0 3 * * *",
|
|
335
|
-
},
|
|
331
|
+
modelFreshnessSchedule: { freshness: { fallback: "fail" } },
|
|
336
332
|
});
|
|
337
333
|
const pkg = {
|
|
338
|
-
schedule:
|
|
334
|
+
schedule: null,
|
|
339
335
|
freshness: { window: "24h", fallback: "live" as const },
|
|
340
336
|
};
|
|
341
|
-
expect(
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
});
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
it("does not inherit the package-level cron as the source's schedule", () => {
|
|
348
|
-
const source = fakeSource({ name: "s", sourceEntityId: "bid" });
|
|
349
|
-
const pkg = { schedule: "0 0 * * *", freshness: { window: "24h" } };
|
|
350
|
-
// Freshness inherits from the package; schedule stays null (the package
|
|
351
|
-
// cron is gated separately, not folded into the per-source cron).
|
|
352
|
-
expect(resolveFreshnessSchedule(source, pkg)).toEqual({
|
|
353
|
-
freshness: { window: "24h" },
|
|
354
|
-
schedule: null,
|
|
337
|
+
expect(resolveFreshness(source, pkg)).toEqual({
|
|
338
|
+
window: "1h",
|
|
339
|
+
fallback: "fail",
|
|
355
340
|
});
|
|
356
341
|
});
|
|
357
342
|
|
|
358
343
|
it("inherits the package freshness when the source and model are unset", () => {
|
|
359
344
|
const source = fakeSource({ name: "s", sourceEntityId: "bid" });
|
|
360
345
|
const pkg = { schedule: null, freshness: { window: "24h" } };
|
|
361
|
-
expect(
|
|
362
|
-
freshness: { window: "24h" },
|
|
363
|
-
schedule: null,
|
|
364
|
-
});
|
|
346
|
+
expect(resolveFreshness(source, pkg)).toEqual({ window: "24h" });
|
|
365
347
|
});
|
|
366
348
|
|
|
367
349
|
it("drops an invalid fallback rather than defaulting it", () => {
|
|
@@ -370,23 +352,17 @@ describe("resolveFreshnessSchedule", () => {
|
|
|
370
352
|
sourceEntityId: "bid",
|
|
371
353
|
freshnessSchedule: { freshness: { window: "1h", fallback: "bogus" } },
|
|
372
354
|
});
|
|
373
|
-
expect(
|
|
374
|
-
freshness: { window: "1h" },
|
|
375
|
-
schedule: null,
|
|
376
|
-
});
|
|
355
|
+
expect(resolveFreshness(source, null)).toEqual({ window: "1h" });
|
|
377
356
|
});
|
|
378
357
|
});
|
|
379
358
|
|
|
380
|
-
describe("deriveBuildPlan freshness
|
|
381
|
-
it("projects the resolved per-source freshness
|
|
359
|
+
describe("deriveBuildPlan freshness", () => {
|
|
360
|
+
it("projects the resolved per-source freshness onto the plan (no schedule/sharing)", () => {
|
|
382
361
|
const source = fakeSource({
|
|
383
362
|
name: "s",
|
|
384
363
|
sourceEntityId: "bid",
|
|
385
|
-
annotationFields: { name: "s_table"
|
|
386
|
-
freshnessSchedule: {
|
|
387
|
-
freshness: { window: "1h" },
|
|
388
|
-
schedule: "0 */6 * * *",
|
|
389
|
-
},
|
|
364
|
+
annotationFields: { name: "s_table" },
|
|
365
|
+
freshnessSchedule: { freshness: { window: "1h" } },
|
|
390
366
|
});
|
|
391
367
|
const plan = deriveBuildPlan(
|
|
392
368
|
[
|
|
@@ -406,12 +382,18 @@ describe("deriveBuildPlan freshness/schedule", () => {
|
|
|
406
382
|
);
|
|
407
383
|
|
|
408
384
|
// Source window wins over the package default; package fallback fills the
|
|
409
|
-
// unset source fallback
|
|
385
|
+
// unset source fallback.
|
|
410
386
|
expect(plan.sources["s@m"].freshness).toEqual({
|
|
411
387
|
window: "1h",
|
|
412
388
|
fallback: "live",
|
|
413
389
|
});
|
|
414
|
-
|
|
390
|
+
// Retired fields are not emitted.
|
|
391
|
+
expect(
|
|
392
|
+
(plan.sources["s@m"] as Record<string, unknown>).schedule,
|
|
393
|
+
).toBeUndefined();
|
|
394
|
+
expect(
|
|
395
|
+
(plan.sources["s@m"] as Record<string, unknown>).sharing,
|
|
396
|
+
).toBeUndefined();
|
|
415
397
|
});
|
|
416
398
|
});
|
|
417
399
|
|
|
@@ -25,11 +25,10 @@ type WirePackageMaterialization =
|
|
|
25
25
|
const FRESHNESS_FALLBACKS = ["live", "stale_ok", "fail"] as const;
|
|
26
26
|
type FreshnessFallback = (typeof FRESHNESS_FALLBACKS)[number];
|
|
27
27
|
|
|
28
|
-
/** One layer's contribution to the resolved freshness
|
|
29
|
-
interface
|
|
28
|
+
/** One layer's contribution to the resolved freshness (all optional). */
|
|
29
|
+
interface FreshnessLayer {
|
|
30
30
|
window?: string;
|
|
31
31
|
fallback?: FreshnessFallback;
|
|
32
|
-
schedule?: string;
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
/** Minimal path-reader over a Malloy `Tag` (see `@malloydata/malloy-tag`). */
|
|
@@ -121,16 +120,17 @@ export function deriveAnnotationFields(
|
|
|
121
120
|
}
|
|
122
121
|
|
|
123
122
|
/**
|
|
124
|
-
* Read the freshness
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
123
|
+
* Read the freshness keys (`freshness.window`, `freshness.fallback`) from one
|
|
124
|
+
* Malloy tag layer, keeping only recognized values. These are dotted/nested tag
|
|
125
|
+
* properties, so they are NOT captured by the scalar {@link deriveAnnotationFields}
|
|
126
|
+
* loop and must be read by path here. (Per-source `sharing`/`schedule` were
|
|
127
|
+
* retired — scope is package-level and a schedule is package-root-only — so they
|
|
128
|
+
* are not resolved here; declaring either on a source is a publish-time manifest
|
|
129
|
+
* error, enforced in Package.persistencePolicyWarnings.)
|
|
128
130
|
*/
|
|
129
|
-
function
|
|
130
|
-
tag: ReadableTag | undefined,
|
|
131
|
-
): FreshnessScheduleLayer {
|
|
131
|
+
function tagFreshnessLayer(tag: ReadableTag | undefined): FreshnessLayer {
|
|
132
132
|
if (!tag || typeof tag.text !== "function") return {};
|
|
133
|
-
const layer:
|
|
133
|
+
const layer: FreshnessLayer = {};
|
|
134
134
|
const window = tag.text("freshness", "window");
|
|
135
135
|
if (typeof window === "string") layer.window = window;
|
|
136
136
|
const fallback = tag.text("freshness", "fallback");
|
|
@@ -140,24 +140,15 @@ function tagFreshnessScheduleLayer(
|
|
|
140
140
|
) {
|
|
141
141
|
layer.fallback = fallback as FreshnessFallback;
|
|
142
142
|
}
|
|
143
|
-
const schedule = tag.text("schedule");
|
|
144
|
-
if (typeof schedule === "string") layer.schedule = schedule;
|
|
145
143
|
return layer;
|
|
146
144
|
}
|
|
147
145
|
|
|
148
|
-
/**
|
|
149
|
-
* The package-level `materialization` config as a freshness resolution layer.
|
|
150
|
-
* Only `freshness` inherits down to a source: the package-level cron
|
|
151
|
-
* (`materialization.schedule`) is a distinct package-grain concept surfaced on
|
|
152
|
-
* `PackageMaterializationConfig.schedule` and gated on its own, so it is NOT
|
|
153
|
-
* folded into a source's effective per-source cron (doing so would double-count
|
|
154
|
-
* against the package cron gate).
|
|
155
|
-
*/
|
|
146
|
+
/** The package-level `materialization.freshness` as a resolution layer. */
|
|
156
147
|
function packageFreshnessLayer(
|
|
157
148
|
cfg: WirePackageMaterialization | null | undefined,
|
|
158
|
-
):
|
|
149
|
+
): FreshnessLayer {
|
|
159
150
|
if (!cfg) return {};
|
|
160
|
-
const layer:
|
|
151
|
+
const layer: FreshnessLayer = {};
|
|
161
152
|
if (cfg.freshness?.window) layer.window = cfg.freshness.window;
|
|
162
153
|
const fallback = cfg.freshness?.fallback;
|
|
163
154
|
if (
|
|
@@ -192,36 +183,30 @@ function safeModelTag(source: PersistSource): ReadableTag | undefined {
|
|
|
192
183
|
}
|
|
193
184
|
|
|
194
185
|
/**
|
|
195
|
-
* Resolve a source's EFFECTIVE freshness
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
* - `schedule`: source annotation > model-file default (the source's OWN cron).
|
|
202
|
-
* The package-level cron is a package-grain concept, surfaced and gated
|
|
203
|
-
* separately, so it is not folded into the per-source effective cron.
|
|
186
|
+
* Resolve a source's EFFECTIVE freshness with most-specific-wins precedence,
|
|
187
|
+
* per field: source annotation > model-file default > package default. Reported
|
|
188
|
+
* verbatim (unset at every level stays null so the control plane can
|
|
189
|
+
* distinguish "unset" — apply platform default — from an explicit declaration).
|
|
190
|
+
* Invalid `fallback` values are dropped, never defaulted. Freshness is valid in
|
|
191
|
+
* both scope modes.
|
|
204
192
|
*/
|
|
205
|
-
export function
|
|
193
|
+
export function resolveFreshness(
|
|
206
194
|
source: PersistSource,
|
|
207
195
|
packageMaterialization: WirePackageMaterialization | null | undefined,
|
|
208
|
-
):
|
|
209
|
-
const sourceLayer =
|
|
210
|
-
const modelLayer =
|
|
196
|
+
): WireFreshness | null {
|
|
197
|
+
const sourceLayer = tagFreshnessLayer(safeSourceTag(source));
|
|
198
|
+
const modelLayer = tagFreshnessLayer(safeModelTag(source));
|
|
211
199
|
const pkgLayer = packageFreshnessLayer(packageMaterialization);
|
|
212
200
|
|
|
213
201
|
const window = sourceLayer.window ?? modelLayer.window ?? pkgLayer.window;
|
|
214
202
|
const fallback =
|
|
215
203
|
sourceLayer.fallback ?? modelLayer.fallback ?? pkgLayer.fallback;
|
|
216
|
-
const schedule = sourceLayer.schedule ?? modelLayer.schedule ?? null;
|
|
217
204
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
return { freshness, schedule };
|
|
205
|
+
if (window === undefined && fallback === undefined) return null;
|
|
206
|
+
const freshness: WireFreshness = {};
|
|
207
|
+
if (window !== undefined) freshness.window = window;
|
|
208
|
+
if (fallback !== undefined) freshness.fallback = fallback;
|
|
209
|
+
return freshness;
|
|
225
210
|
}
|
|
226
211
|
|
|
227
212
|
/** Flatten Malloy's nested BuildNode.dependsOn into a list of sourceIDs. */
|
|
@@ -439,15 +424,14 @@ export function deriveBuildPlan(
|
|
|
439
424
|
for (const [sourceID, source] of Object.entries(sources)) {
|
|
440
425
|
if (include && !include.has(source.name)) continue;
|
|
441
426
|
const annotationFields = deriveAnnotationFields(source);
|
|
442
|
-
// EFFECTIVE per-source freshness
|
|
427
|
+
// EFFECTIVE per-source freshness, resolved most-specific-wins
|
|
443
428
|
// (source > model-file > package) and reported verbatim (null = unset at
|
|
444
|
-
// every level).
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
);
|
|
429
|
+
// every level). Freshness is a dotted/nested tag key, so it comes from
|
|
430
|
+
// resolveFreshness rather than the scalar annotationFields map, and is
|
|
431
|
+
// valid in both scope modes. Per-source `sharing`/`schedule` are NOT
|
|
432
|
+
// emitted (retired from the contract); if a source declares either it is
|
|
433
|
+
// rejected at publish (Package.persistencePolicyWarnings) — the raw keys
|
|
434
|
+
// still ride `annotationFields` so the validator can detect them.
|
|
451
435
|
wireSources[sourceID] = {
|
|
452
436
|
name: source.name,
|
|
453
437
|
sourceID: source.sourceID,
|
|
@@ -455,16 +439,8 @@ export function deriveBuildPlan(
|
|
|
455
439
|
dialect: source.dialectName,
|
|
456
440
|
sourceEntityId: computeSourceEntityId(source, connectionDigests),
|
|
457
441
|
sql: source.getSQL(),
|
|
458
|
-
// Declared `#@ persist` scope/refresh knobs, reported VERBATIM (null
|
|
459
|
-
// = unset). The control plane applies the platform default (unset =>
|
|
460
|
-
// shared) itself, so the publisher must never substitute it — unset
|
|
461
|
-
// has to stay distinguishable from an explicit `shared`. The source's
|
|
462
|
-
// own annotation is the most-specific declaration layer, and the only
|
|
463
|
-
// one that exists today.
|
|
464
|
-
sharing: annotationFields.sharing ?? null,
|
|
465
442
|
refresh: annotationFields.refresh ?? null,
|
|
466
|
-
freshness,
|
|
467
|
-
schedule,
|
|
443
|
+
freshness: resolveFreshness(source, packageMaterialization),
|
|
468
444
|
columns: deriveColumns(source),
|
|
469
445
|
annotationFields,
|
|
470
446
|
modelPath: sourceModelPaths?.[sourceID],
|
|
@@ -1347,6 +1347,10 @@ export class Environment {
|
|
|
1347
1347
|
// control plane misread the gap as a schedule removal. It is not a
|
|
1348
1348
|
// PATCH-editable field, so always preserve the existing value.
|
|
1349
1349
|
materialization: existing.materialization,
|
|
1350
|
+
// Same rationale for the manifest-derived persist `scope`: not
|
|
1351
|
+
// PATCH-editable, so preserve it rather than dropping it to the
|
|
1352
|
+
// default until the next reload.
|
|
1353
|
+
scope: existing.scope,
|
|
1350
1354
|
});
|
|
1351
1355
|
|
|
1352
1356
|
// Strict-reject, symmetric with the publish path
|
|
@@ -75,17 +75,16 @@ export function makeInstruction(
|
|
|
75
75
|
* build internals touch (name/id, deterministic sourceEntityId, SQL, and the
|
|
76
76
|
* `#@ persist name=` annotation reader, defaulted to "unset").
|
|
77
77
|
*/
|
|
78
|
-
/** A freshness
|
|
78
|
+
/** A freshness layer for the fake source's `#@` or `##` tag. */
|
|
79
79
|
interface FakeFreshnessSchedule {
|
|
80
80
|
freshness?: { window?: string; fallback?: string };
|
|
81
|
-
schedule?: string;
|
|
82
81
|
}
|
|
83
82
|
|
|
84
83
|
/**
|
|
85
84
|
* Build a fake Malloy `Tag` supporting both readers the build plan uses:
|
|
86
85
|
* `entries()` (scalar `#@ persist` key=value pairs, for deriveAnnotationFields)
|
|
87
|
-
* and the path-based `text(...at)` (dotted `freshness.window
|
|
88
|
-
*
|
|
86
|
+
* and the path-based `text(...at)` (dotted `freshness.window`, for
|
|
87
|
+
* resolveFreshness).
|
|
89
88
|
*/
|
|
90
89
|
function fakeTag(
|
|
91
90
|
fields: Record<string, string> | undefined,
|
|
@@ -99,7 +98,6 @@ function fakeTag(
|
|
|
99
98
|
},
|
|
100
99
|
text(...at: string[]): string | undefined {
|
|
101
100
|
if (at.length === 1) {
|
|
102
|
-
if (at[0] === "schedule") return fs?.schedule;
|
|
103
101
|
return fields?.[at[0]];
|
|
104
102
|
}
|
|
105
103
|
if (at.length === 2 && at[0] === "freshness") {
|
|
@@ -116,11 +114,11 @@ export function fakeSource(opts: {
|
|
|
116
114
|
sql?: string;
|
|
117
115
|
connectionName?: string;
|
|
118
116
|
dialectName?: string;
|
|
119
|
-
/** key=value fields of the `#@ persist` annotation (e.g.
|
|
117
|
+
/** key=value fields of the `#@ persist` annotation (e.g. name, refresh). */
|
|
120
118
|
annotationFields?: Record<string, string>;
|
|
121
|
-
/** Source-level (`#@`) freshness
|
|
119
|
+
/** Source-level (`#@`) freshness (dotted keys). */
|
|
122
120
|
freshnessSchedule?: FakeFreshnessSchedule;
|
|
123
|
-
/** Model-file-level (`##`) freshness
|
|
121
|
+
/** Model-file-level (`##`) freshness default. */
|
|
124
122
|
modelFreshnessSchedule?: FakeFreshnessSchedule;
|
|
125
123
|
}): PersistSource {
|
|
126
124
|
const fields = opts.annotationFields;
|