@malloy-publisher/server 0.0.235 → 0.0.236

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 (301) hide show
  1. package/dist/app/api-doc.yaml +47 -17
  2. package/dist/package_load_worker.mjs +8 -1
  3. package/dist/server.mjs +471 -54
  4. package/package.json +5 -2
  5. package/.eslintrc.json +0 -44
  6. package/.prettierignore +0 -1
  7. package/.prettierrc +0 -4
  8. package/build.ts +0 -97
  9. package/eslint.config.mjs +0 -76
  10. package/k6-tests/README.md +0 -144
  11. package/k6-tests/bun.lock +0 -797
  12. package/k6-tests/load-test/load-test-crud-connections.ts +0 -263
  13. package/k6-tests/load-test/load-test-crud-packages.ts +0 -461
  14. package/k6-tests/load-test/load-test-crud-projects.ts +0 -160
  15. package/k6-tests/load-test/load-test-crud.ts +0 -142
  16. package/k6-tests/load-test/load-test.ts +0 -784
  17. package/k6-tests/package.json +0 -27
  18. package/k6-tests/scripts/run-with-otel.sh +0 -50
  19. package/k6-tests/smoke-test/smoke-test.ts +0 -158
  20. package/k6-tests/tsconfig.json +0 -29
  21. package/k6-tests/utils/client_factory.ts +0 -128
  22. package/k6-tests/utils/common.ts +0 -489
  23. package/k6-tests/utils/logger.ts +0 -62
  24. package/k6-tests/utils/types.d.ts +0 -35
  25. package/malloy_mcp.dxt +0 -0
  26. package/publisher.config.example.bigquery.json +0 -33
  27. package/publisher.config.example.duckdb.json +0 -23
  28. package/publisher.config.json +0 -23
  29. package/scripts/bake-duckdb-extensions.js +0 -110
  30. package/src/config.spec.ts +0 -1535
  31. package/src/config.theme.spec.ts +0 -178
  32. package/src/config.ts +0 -1142
  33. package/src/constants.ts +0 -105
  34. package/src/controller/compile.controller.ts +0 -40
  35. package/src/controller/connection.controller.spec.ts +0 -849
  36. package/src/controller/connection.controller.ts +0 -901
  37. package/src/controller/database.controller.ts +0 -24
  38. package/src/controller/materialization.controller.spec.ts +0 -293
  39. package/src/controller/materialization.controller.ts +0 -319
  40. package/src/controller/model.controller.ts +0 -164
  41. package/src/controller/package.controller.spec.ts +0 -273
  42. package/src/controller/package.controller.ts +0 -306
  43. package/src/controller/query.controller.ts +0 -152
  44. package/src/controller/theme.controller.ts +0 -83
  45. package/src/controller/watch-mode.controller.ts +0 -243
  46. package/src/data_styles.ts +0 -71
  47. package/src/default-publisher.config.json +0 -23
  48. package/src/ducklake_version.spec.ts +0 -163
  49. package/src/ducklake_version.ts +0 -153
  50. package/src/errors.spec.ts +0 -119
  51. package/src/errors.ts +0 -237
  52. package/src/filter_deprecation.spec.ts +0 -64
  53. package/src/filter_deprecation.ts +0 -42
  54. package/src/health.spec.ts +0 -123
  55. package/src/health.ts +0 -233
  56. package/src/heap_check.spec.ts +0 -144
  57. package/src/heap_check.ts +0 -144
  58. package/src/instrumentation.ts +0 -214
  59. package/src/json_utils.spec.ts +0 -51
  60. package/src/json_utils.ts +0 -33
  61. package/src/logger.spec.ts +0 -210
  62. package/src/logger.ts +0 -251
  63. package/src/malloy_pin_prereqs.spec.ts +0 -25
  64. package/src/materialization_metrics.spec.ts +0 -169
  65. package/src/materialization_metrics.ts +0 -346
  66. package/src/mcp/error_messages.spec.ts +0 -35
  67. package/src/mcp/error_messages.ts +0 -183
  68. package/src/mcp/handler_utils.spec.ts +0 -108
  69. package/src/mcp/handler_utils.ts +0 -251
  70. package/src/mcp/mcp_constants.ts +0 -6
  71. package/src/mcp/query_envelope.spec.ts +0 -229
  72. package/src/mcp/query_envelope.ts +0 -240
  73. package/src/mcp/server.protocol.spec.ts +0 -250
  74. package/src/mcp/server.ts +0 -87
  75. package/src/mcp/skills/build_skills_bundle.spec.ts +0 -51
  76. package/src/mcp/skills/build_skills_bundle.ts +0 -176
  77. package/src/mcp/skills/skills_bundle.json +0 -1
  78. package/src/mcp/skills/skills_bundle.spec.ts +0 -171
  79. package/src/mcp/tool_response.spec.ts +0 -108
  80. package/src/mcp/tool_response.ts +0 -138
  81. package/src/mcp/tools/compile_tool.spec.ts +0 -315
  82. package/src/mcp/tools/compile_tool.ts +0 -208
  83. package/src/mcp/tools/docs_search/build_docs_index.ts +0 -132
  84. package/src/mcp/tools/docs_search/malloy_docs_index.json +0 -1
  85. package/src/mcp/tools/docs_search_tool.spec.ts +0 -32
  86. package/src/mcp/tools/docs_search_tool.ts +0 -128
  87. package/src/mcp/tools/embedding_index.spec.ts +0 -1236
  88. package/src/mcp/tools/embedding_index.ts +0 -808
  89. package/src/mcp/tools/execute_query_tool.spec.ts +0 -294
  90. package/src/mcp/tools/execute_query_tool.ts +0 -316
  91. package/src/mcp/tools/get_context_eval.ts +0 -275
  92. package/src/mcp/tools/get_context_tool.spec.ts +0 -592
  93. package/src/mcp/tools/get_context_tool.ts +0 -585
  94. package/src/mcp/tools/reload_package_tool.spec.ts +0 -232
  95. package/src/mcp/tools/reload_package_tool.ts +0 -132
  96. package/src/mcp_config.spec.ts +0 -919
  97. package/src/mcp_config.ts +0 -425
  98. package/src/oom_guards.integration.spec.ts +0 -269
  99. package/src/package_load/package_load_pool.spec.ts +0 -252
  100. package/src/package_load/package_load_pool.ts +0 -931
  101. package/src/package_load/package_load_worker.ts +0 -1034
  102. package/src/package_load/protocol.ts +0 -364
  103. package/src/package_load/rpc_wait_accountant.spec.ts +0 -109
  104. package/src/package_load/rpc_wait_accountant.ts +0 -76
  105. package/src/package_load_metrics.spec.ts +0 -114
  106. package/src/package_load_metrics.ts +0 -127
  107. package/src/path_safety.spec.ts +0 -158
  108. package/src/path_safety.ts +0 -146
  109. package/src/pg_helpers.spec.ts +0 -223
  110. package/src/pg_helpers.ts +0 -52
  111. package/src/query_cap_metrics.spec.ts +0 -89
  112. package/src/query_cap_metrics.ts +0 -116
  113. package/src/query_concurrency.spec.ts +0 -247
  114. package/src/query_concurrency.ts +0 -237
  115. package/src/query_metadata_metrics.ts +0 -49
  116. package/src/query_param_utils.ts +0 -29
  117. package/src/query_timeout.spec.ts +0 -224
  118. package/src/query_timeout.ts +0 -179
  119. package/src/runtime/publisher.js +0 -328
  120. package/src/server-old.ts +0 -988
  121. package/src/server.ts +0 -2059
  122. package/src/service/annotations.spec.ts +0 -118
  123. package/src/service/annotations.ts +0 -91
  124. package/src/service/authorize.spec.ts +0 -154
  125. package/src/service/authorize.ts +0 -498
  126. package/src/service/authorize_integration.spec.ts +0 -2041
  127. package/src/service/build_plan.spec.ts +0 -685
  128. package/src/service/build_plan.ts +0 -797
  129. package/src/service/compile_authorize.spec.ts +0 -85
  130. package/src/service/compile_fragment_techniques.spec.ts +0 -156
  131. package/src/service/connection.spec.ts +0 -2360
  132. package/src/service/connection.ts +0 -2187
  133. package/src/service/connection_config.spec.ts +0 -734
  134. package/src/service/connection_config.ts +0 -899
  135. package/src/service/connection_federation.spec.ts +0 -184
  136. package/src/service/connection_fingerprint.spec.ts +0 -102
  137. package/src/service/connection_service.spec.ts +0 -711
  138. package/src/service/connection_service.ts +0 -291
  139. package/src/service/cron_evaluator.spec.ts +0 -79
  140. package/src/service/cron_evaluator.ts +0 -105
  141. package/src/service/db_utils.spec.ts +0 -1085
  142. package/src/service/db_utils.ts +0 -1502
  143. package/src/service/duckdb_instance_isolation.spec.ts +0 -137
  144. package/src/service/ducklake_lazy_attach.spec.ts +0 -110
  145. package/src/service/embedding_provider.spec.ts +0 -329
  146. package/src/service/embedding_provider.ts +0 -236
  147. package/src/service/environment.ts +0 -2191
  148. package/src/service/environment_admission.spec.ts +0 -344
  149. package/src/service/environment_compile.spec.ts +0 -197
  150. package/src/service/environment_store.spec.ts +0 -2274
  151. package/src/service/environment_store.ts +0 -2434
  152. package/src/service/environment_store_anchoring.spec.ts +0 -107
  153. package/src/service/environment_store_clone.spec.ts +0 -350
  154. package/src/service/explore_visibility.spec.ts +0 -434
  155. package/src/service/exports_probe.spec.ts +0 -107
  156. package/src/service/extension_fetch_policy.spec.ts +0 -256
  157. package/src/service/filter.spec.ts +0 -458
  158. package/src/service/filter.ts +0 -341
  159. package/src/service/filter_bypass.spec.ts +0 -418
  160. package/src/service/filter_integration.spec.ts +0 -1004
  161. package/src/service/freshness.spec.ts +0 -183
  162. package/src/service/freshness.ts +0 -112
  163. package/src/service/gcs_s3_utils.ts +0 -427
  164. package/src/service/given.ts +0 -105
  165. package/src/service/givens_integration.spec.ts +0 -219
  166. package/src/service/manifest_loader.spec.ts +0 -189
  167. package/src/service/manifest_loader.ts +0 -156
  168. package/src/service/materialization_build_session.spec.ts +0 -435
  169. package/src/service/materialization_build_session.ts +0 -681
  170. package/src/service/materialization_config_validation.spec.ts +0 -99
  171. package/src/service/materialization_config_validation.ts +0 -120
  172. package/src/service/materialization_eligibility.spec.ts +0 -158
  173. package/src/service/materialization_eligibility.ts +0 -305
  174. package/src/service/materialization_schedule_surface.spec.ts +0 -256
  175. package/src/service/materialization_scheduler.spec.ts +0 -289
  176. package/src/service/materialization_scheduler.ts +0 -304
  177. package/src/service/materialization_serve_transform.spec.ts +0 -1003
  178. package/src/service/materialization_serve_transform.ts +0 -779
  179. package/src/service/materialization_service.spec.ts +0 -2373
  180. package/src/service/materialization_service.ts +0 -2342
  181. package/src/service/materialization_test_fixtures.ts +0 -249
  182. package/src/service/model.spec.ts +0 -1157
  183. package/src/service/model.ts +0 -3370
  184. package/src/service/model_limits.spec.ts +0 -209
  185. package/src/service/model_limits.ts +0 -131
  186. package/src/service/model_storage_serve.spec.ts +0 -193
  187. package/src/service/model_storage_serve_joins.spec.ts +0 -193
  188. package/src/service/package.spec.ts +0 -612
  189. package/src/service/package.ts +0 -1799
  190. package/src/service/package_manifest.spec.ts +0 -230
  191. package/src/service/package_manifest.ts +0 -241
  192. package/src/service/package_memory_governor.spec.ts +0 -173
  193. package/src/service/package_memory_governor.ts +0 -233
  194. package/src/service/package_quote_bound_tables.spec.ts +0 -171
  195. package/src/service/package_race.spec.ts +0 -208
  196. package/src/service/package_reload_safety.spec.ts +0 -193
  197. package/src/service/package_rollback.spec.ts +0 -190
  198. package/src/service/package_worker_path.spec.ts +0 -645
  199. package/src/service/path_injection.spec.ts +0 -39
  200. package/src/service/persist_annotation_validation.spec.ts +0 -89
  201. package/src/service/persist_annotation_validation.ts +0 -59
  202. package/src/service/persistence_policy.spec.ts +0 -374
  203. package/src/service/proxy.spec.ts +0 -414
  204. package/src/service/proxy.ts +0 -248
  205. package/src/service/query_boundary.spec.ts +0 -470
  206. package/src/service/query_metadata.spec.ts +0 -408
  207. package/src/service/query_metadata.ts +0 -492
  208. package/src/service/query_metadata_identity.spec.ts +0 -149
  209. package/src/service/query_text.spec.ts +0 -79
  210. package/src/service/query_text.ts +0 -43
  211. package/src/service/quoting.spec.ts +0 -135
  212. package/src/service/quoting.ts +0 -78
  213. package/src/service/resolve_environment.ts +0 -15
  214. package/src/service/restricted_mode.spec.ts +0 -299
  215. package/src/service/source_extraction.ts +0 -225
  216. package/src/service/theme_store.ts +0 -199
  217. package/src/storage/DatabaseInterface.ts +0 -198
  218. package/src/storage/StorageManager.ts +0 -113
  219. package/src/storage/duckdb/ConnectionRepository.ts +0 -143
  220. package/src/storage/duckdb/DuckDBConnection.ts +0 -123
  221. package/src/storage/duckdb/DuckDBRepository.ts +0 -208
  222. package/src/storage/duckdb/EnvironmentRepository.ts +0 -148
  223. package/src/storage/duckdb/MaterializationRepository.spec.ts +0 -191
  224. package/src/storage/duckdb/MaterializationRepository.ts +0 -336
  225. package/src/storage/duckdb/PackageRepository.ts +0 -135
  226. package/src/storage/duckdb/schema.ts +0 -229
  227. package/src/stream_helpers.spec.ts +0 -280
  228. package/src/stream_helpers.ts +0 -162
  229. package/src/telemetry.ts +0 -20
  230. package/src/test_helpers/metrics_harness.ts +0 -166
  231. package/src/theme_key_parity.spec.ts +0 -57
  232. package/src/utils.ts +0 -29
  233. package/tests/fixtures/authorize-compile/model.malloy +0 -9
  234. package/tests/fixtures/authorize-compile/publisher.json +0 -4
  235. package/tests/fixtures/html-pages-nopublic/model.malloy +0 -1
  236. package/tests/fixtures/html-pages-nopublic/publisher.json +0 -5
  237. package/tests/fixtures/html-pages-test/data.csv +0 -3
  238. package/tests/fixtures/html-pages-test/public/assets/app.css +0 -3
  239. package/tests/fixtures/html-pages-test/public/barehtml.html +0 -4
  240. package/tests/fixtures/html-pages-test/public/bodymeta.html +0 -8
  241. package/tests/fixtures/html-pages-test/public/data.json +0 -1
  242. package/tests/fixtures/html-pages-test/public/fitcomment.html +0 -11
  243. package/tests/fixtures/html-pages-test/public/index.html +0 -9
  244. package/tests/fixtures/html-pages-test/public/nohead.html +0 -8
  245. package/tests/fixtures/html-pages-test/public/notfit.html +0 -13
  246. package/tests/fixtures/html-pages-test/public/slides.html +0 -12
  247. package/tests/fixtures/html-pages-test/public/sub/page2.html +0 -9
  248. package/tests/fixtures/html-pages-test/public/unterminated.html +0 -10
  249. package/tests/fixtures/html-pages-test/publisher.json +0 -5
  250. package/tests/fixtures/html-pages-test/report.malloy +0 -1
  251. package/tests/fixtures/persist-multi-level/data/orders.csv +0 -5
  252. package/tests/fixtures/persist-multi-level/multi_level.malloy +0 -18
  253. package/tests/fixtures/persist-multi-level/publisher.json +0 -5
  254. package/tests/fixtures/persist-schedule-test/data/orders.csv +0 -5
  255. package/tests/fixtures/persist-schedule-test/persist_schedule_test.malloy +0 -11
  256. package/tests/fixtures/persist-schedule-test/publisher.json +0 -7
  257. package/tests/fixtures/persist-schedule-warn-test/data/orders.csv +0 -5
  258. package/tests/fixtures/persist-schedule-warn-test/persist_warn_test.malloy +0 -11
  259. package/tests/fixtures/persist-schedule-warn-test/publisher.json +0 -6
  260. package/tests/fixtures/persist-test/data/orders.csv +0 -5
  261. package/tests/fixtures/persist-test/persist_test.malloy +0 -11
  262. package/tests/fixtures/persist-test/publisher.json +0 -5
  263. package/tests/fixtures/publisher.config.json +0 -15
  264. package/tests/fixtures/query-givens/data/orders.csv +0 -7
  265. package/tests/fixtures/query-givens/model.malloy +0 -34
  266. package/tests/fixtures/query-givens/publisher.json +0 -5
  267. package/tests/fixtures/xlsx/database.xlsx +0 -0
  268. package/tests/harness/mcp_test_setup.ts +0 -332
  269. package/tests/harness/rest_e2e.ts +0 -68
  270. package/tests/integration/authorize/compile_authorize_http.integration.spec.ts +0 -92
  271. package/tests/integration/concurrent_package/concurrent_package.integration.spec.ts +0 -280
  272. package/tests/integration/duckdb_storage/duckdb_storage.integration.spec.ts +0 -138
  273. package/tests/integration/first_boot/readiness_line.integration.spec.ts +0 -177
  274. package/tests/integration/html_pages/html_pages.integration.spec.ts +0 -440
  275. package/tests/integration/legacy_routes/legacy_routes.integration.spec.ts +0 -259
  276. package/tests/integration/materialization/environment_list.integration.spec.ts +0 -133
  277. package/tests/integration/materialization/freshness_gate.integration.spec.ts +0 -292
  278. package/tests/integration/materialization/manifest_binding.integration.spec.ts +0 -430
  279. package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +0 -441
  280. package/tests/integration/materialization/orchestrated_rebind.integration.spec.ts +0 -150
  281. package/tests/integration/materialization/reference_manifest.integration.spec.ts +0 -251
  282. package/tests/integration/materialization/schedule_edit.integration.spec.ts +0 -126
  283. package/tests/integration/materialization/scheduler.integration.spec.ts +0 -200
  284. package/tests/integration/materialization/scheduler_recovery.integration.spec.ts +0 -172
  285. package/tests/integration/materialization/scheduler_transitions.integration.spec.ts +0 -256
  286. package/tests/integration/materialization/scheduler_wiring.integration.spec.ts +0 -143
  287. package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +0 -438
  288. package/tests/integration/mcp/mcp_get_context_semantic.integration.spec.ts +0 -235
  289. package/tests/integration/mcp/mcp_transport.integration.spec.ts +0 -98
  290. package/tests/integration/query_givens/query_givens.integration.spec.ts +0 -146
  291. package/tests/integration/query_givens/query_givens_authorize.integration.spec.ts +0 -121
  292. package/tests/integration/sdk_givens/sdk_givens.integration.spec.ts +0 -110
  293. package/tests/integration/watch-mode/watch_mode.integration.spec.ts +0 -421
  294. package/tests/unit/controller/theme.controller.spec.ts +0 -141
  295. package/tests/unit/duckdb/attached_databases.test.ts +0 -1243
  296. package/tests/unit/duckdb/duckdb_connection.test.ts +0 -181
  297. package/tests/unit/duckdb/legacy_schema_migration.test.ts +0 -197
  298. package/tests/unit/duckdb/repositories.test.ts +0 -208
  299. package/tests/unit/ducklake/ducklake.test.ts +0 -952
  300. package/tests/unit/service/theme_store.spec.ts +0 -139
  301. package/tsconfig.json +0 -32
@@ -1,797 +0,0 @@
1
- import type {
2
- AtomicField,
3
- BuildGraph as MalloyBuildGraph,
4
- BuildNode,
5
- MalloyConfig,
6
- Connection as MalloyConnection,
7
- PersistSource,
8
- } from "@malloydata/malloy";
9
- import { Annotations } from "@malloydata/malloy";
10
- import { components } from "../api";
11
- import { MaterializationEligibilityError } from "../errors";
12
- import { MODEL_FILE_SUFFIX } from "../constants";
13
- import { logger } from "../logger";
14
- import {
15
- recordConnectionDigestSkipped,
16
- recordEligibilityRefused,
17
- } from "../materialization_metrics";
18
- import { errMessage } from "../utils";
19
- import { assertMaterializationEligible } from "./materialization_eligibility";
20
- import { Model } from "./model";
21
- import { quoteIdentifier } from "./quoting";
22
-
23
- type WireBuildGraph = components["schemas"]["BuildGraph"];
24
- type WirePersistSourcePlan = components["schemas"]["PersistSourcePlan"];
25
- type WireColumn = components["schemas"]["Column"];
26
- type BuildPlan = components["schemas"]["BuildPlan"];
27
- type WireFreshness = components["schemas"]["Freshness"];
28
- type WirePackageMaterialization =
29
- components["schemas"]["PackageMaterializationConfig"];
30
- type QueryMetadata = components["schemas"]["QueryMetadata"];
31
-
32
- /** The freshness `fallback` values the publisher recognizes; others are dropped. */
33
- const FRESHNESS_FALLBACKS = ["live", "stale_ok", "fail"] as const;
34
- type FreshnessFallback = (typeof FRESHNESS_FALLBACKS)[number];
35
-
36
- /** One layer's contribution to the resolved freshness (all optional). */
37
- interface FreshnessLayer {
38
- window?: string;
39
- fallback?: FreshnessFallback;
40
- }
41
-
42
- /**
43
- * Minimal reader over a Malloy `Tag` (see `@malloydata/malloy-tag`): scalar
44
- * reads by path, plus the subtree read that a property collection like
45
- * `queryMetadata { … }` needs.
46
- */
47
- interface ReadableTag {
48
- text(...path: string[]): string | undefined;
49
- tag(...path: string[]): ReadableTag | undefined;
50
- entries?(): Iterable<[string, { text(): string | undefined }]>;
51
- }
52
-
53
- /**
54
- * Minimal surface a package must expose to compile its build plan. Both
55
- * {@link Package} (for the read-only `Package.buildPlan`) and the
56
- * materialization service (for auto-run and orchestrated builds) satisfy it.
57
- */
58
- export interface BuildPlanPackage {
59
- getModelPaths(): string[];
60
- getPackagePath(): string;
61
- getMalloyConfig(): MalloyConfig;
62
- getMalloyConnection(name: string): Promise<MalloyConnection>;
63
- /**
64
- * The package-level `materialization` config (from malloy-publisher.json),
65
- * used as the least-specific layer when resolving per-source freshness /
66
- * schedule. Optional so existing fixtures/callers that don't track it still
67
- * typecheck (they resolve without a package default).
68
- */
69
- getMaterializationConfig?(): WirePackageMaterialization | null;
70
- }
71
-
72
- /**
73
- * Result of compiling a package's persist sources: the dependency-ordered
74
- * build graphs, the persist sources keyed by sourceID, the per-connection
75
- * digests, and the resolved live connections. The wire {@link BuildPlan} is a
76
- * projection of this (see {@link deriveBuildPlan}); the build engine consumes
77
- * the full structure.
78
- */
79
- export interface CompiledBuildPlan {
80
- graphs: MalloyBuildGraph[];
81
- sources: Record<string, PersistSource>;
82
- connectionDigests: Record<string, string>;
83
- connections: Map<string, MalloyConnection>;
84
- /**
85
- * sourceID -> the package-relative path of the `.malloy` model that declares
86
- * the source (the only place that mapping is known, since a sourceID's
87
- * embedded modelURL is an absolute `file://` path with no package boundary).
88
- * Optional so existing fixtures/callers that don't track it still typecheck.
89
- */
90
- sourceModelPaths?: Record<string, string>;
91
- /**
92
- * Sources carrying a `#@ persist` annotation that Malloy's getBuildPlan() did
93
- * NOT recognize as a materializable build root, so they produced no plan entry
94
- * and would otherwise be a silent no-op (served live, never materialized). See
95
- * {@link detectDroppedPersistSources}. Callers surface these: a load-time
96
- * warning and a hard build failure, so a persist annotation is never silently
97
- * dropped.
98
- */
99
- droppedPersistSources?: { name: string; modelPath: string }[];
100
- }
101
-
102
- /** Output columns of a persist source, degrading to [] if unavailable. */
103
- export function deriveColumns(persistSource: PersistSource): WireColumn[] {
104
- try {
105
- return persistSource._explore.intrinsicFields
106
- .filter((f) => f.isAtomicField())
107
- .map((f) => ({
108
- name: f.name,
109
- type: String((f as AtomicField).type),
110
- }));
111
- } catch (err) {
112
- logger.warn("Failed to derive columns for persist source", {
113
- sourceID: persistSource.sourceID,
114
- error: errMessage(err),
115
- });
116
- return [];
117
- }
118
- }
119
-
120
- /**
121
- * A source's PUBLIC column names, or a throw if they can't be determined. The
122
- * strict counterpart to {@link deriveColumns}, which degrades to `[]` because its
123
- * caller (the wire build plan) only reports columns. Here the list decides what
124
- * gets WRITTEN, so "unknown" must not read as "nothing to narrow".
125
- *
126
- * A name is returned only for a field that is demonstrably a public atomic field;
127
- * an unreadable field list, an unreadable individual field, or an empty result all
128
- * throw. Empty is a throw rather than a pass-through because a source with no
129
- * public atomic columns means everything `getSQL` projects is hidden — the worst
130
- * case to materialize, not a benign one.
131
- */
132
- function publicColumnNames(persistSource: PersistSource): string[] {
133
- let names: string[];
134
- try {
135
- names = persistSource._explore.intrinsicFields
136
- .filter((f) => f.isAtomicField())
137
- .map((f) => f.name)
138
- .filter((n): n is string => typeof n === "string" && n.length > 0);
139
- } catch (err) {
140
- throw new Error(
141
- `the compiled source's field list could not be read (${errMessage(err)})`,
142
- );
143
- }
144
- if (names.length === 0) {
145
- throw new Error("the compiled source exposes no public atomic columns");
146
- }
147
- return names;
148
- }
149
-
150
- /**
151
- * Wrap a source's build SQL to project only its PUBLIC columns. `getSQL` emits
152
- * every underlying column, including ones the source hides (`except:`, non-public
153
- * access modifiers); this narrows the physical table to the public surface.
154
- *
155
- * A `storage=` build must not materialize a hidden column. Reachability THROUGH
156
- * the source is separately bounded by the declared serve shape (see
157
- * `narrowSchemaToPublic`, and the `shape-bounds-physical-columns` scenario that
158
- * proves a physical column absent from the shape does not resolve). What this
159
- * projection prevents is the hidden column's values sitting AT REST in the
160
- * destination store, where direct catalog access reaches them and where the data
161
- * may have crossed a trust boundary the source's visibility rules were meant to
162
- * hold. Applies to BOTH storage build paths (the warehouse-passthrough
163
- * single-source build and the chained "stack on the parent" downstream build).
164
- *
165
- * Fails CLOSED: if the public surface can't be determined the build is refused,
166
- * rather than widening to everything `getSQL` projects. This matches the rest of
167
- * the eligibility surface (see `assertMaterializationEligible`), and it fires
168
- * before any warehouse or destination SQL runs, so a refusal writes nothing and
169
- * leaves the previous generation serving.
170
- *
171
- * @throws {MaterializationEligibilityError} (HTTP 422) naming the source.
172
- */
173
- export function projectToPublicColumns(
174
- persistSource: PersistSource,
175
- buildSQL: string,
176
- ): string {
177
- let cols: string[];
178
- try {
179
- cols = publicColumnNames(persistSource);
180
- } catch (err) {
181
- recordEligibilityRefused("public_surface_unknown");
182
- throw new MaterializationEligibilityError({
183
- message:
184
- `Source '${persistSource.name}' cannot be materialized into a ` +
185
- `storage destination: its public column surface could not be ` +
186
- `determined — ${errMessage(err)}. The publisher narrows a stored ` +
187
- `table to the source's public columns, so it refuses the build ` +
188
- `rather than materialize columns the source hides. This usually ` +
189
- `means the compiled-source shape changed; drop 'storage=' to serve ` +
190
- `this source live in the meantime.`,
191
- });
192
- }
193
- const projection = cols
194
- .map((n) => quoteIdentifier(n, persistSource.dialectName))
195
- .join(", ");
196
- return `SELECT ${projection} FROM (${buildSQL}) AS __public`;
197
- }
198
-
199
- /**
200
- * All key=value fields of a source's `#@ persist` annotation (e.g.
201
- * `{ name: "engaged_events", realization: "COPY" }`), degrading to {} if the
202
- * annotation is absent or unparseable. The control plane consumes these — most
203
- * importantly `name`, which it uses as the materialized table name (and which
204
- * may carry a dialect-style container path like `dataset.table`). Returning the
205
- * full set rather than a fixed subset means new persist directives flow through
206
- * without a publisher change.
207
- */
208
- export function deriveAnnotationFields(
209
- persistSource: PersistSource,
210
- ): Record<string, string> {
211
- const out: Record<string, string> = {};
212
- try {
213
- const tag = persistSource.annotations.parseAsTag("@").tag;
214
- for (const [key, value] of tag.entries()) {
215
- const text = value.text();
216
- if (text !== undefined) out[key] = text;
217
- }
218
- } catch {
219
- // Degrade to {} — mirrors deriveColumns / selfAssignTableName.
220
- }
221
- return out;
222
- }
223
-
224
- /**
225
- * Read the freshness keys (`freshness.window`, `freshness.fallback`) from one
226
- * Malloy tag layer, keeping only recognized values. These are dotted/nested tag
227
- * properties, so they are NOT captured by the scalar {@link deriveAnnotationFields}
228
- * loop and must be read by path here. (Per-source `sharing`/`schedule` were
229
- * retired — scope is package-level and a schedule is package-root-only — so they
230
- * are not resolved here; declaring either on a source is a publish-time manifest
231
- * error, enforced in Package.persistencePolicyWarnings.)
232
- */
233
- function tagFreshnessLayer(tag: ReadableTag | undefined): FreshnessLayer {
234
- if (!tag || typeof tag.text !== "function") return {};
235
- const layer: FreshnessLayer = {};
236
- const window = tag.text("freshness", "window");
237
- if (typeof window === "string") layer.window = window;
238
- const fallback = tag.text("freshness", "fallback");
239
- if (
240
- typeof fallback === "string" &&
241
- (FRESHNESS_FALLBACKS as readonly string[]).includes(fallback)
242
- ) {
243
- layer.fallback = fallback as FreshnessFallback;
244
- }
245
- return layer;
246
- }
247
-
248
- /**
249
- * The two homes a model-file (`##`) knob can be declared in, most specific
250
- * first: the `materialization` envelope, then the bare form.
251
- *
252
- * The model-file level mirrors the manifest's shape verbatim — `##
253
- * materialization.freshness.window="24h"` is the manifest's block in tag syntax
254
- * — so that is where a file-level reader looks. The bare form
255
- * (`## freshness.window="24h"`), which shipped first, stays readable underneath:
256
- * the annotation rides the published package, so a package published before the
257
- * envelope existed keeps resolving until it is republished. Resolution is
258
- * per-property, so a file may declare one knob in each home without the envelope
259
- * hiding the other.
260
- */
261
- function modelTagLayers(
262
- tag: ReadableTag | undefined,
263
- ): (ReadableTag | undefined)[] {
264
- const envelope =
265
- tag && typeof tag.tag === "function"
266
- ? tag.tag("materialization")
267
- : undefined;
268
- return [envelope, tag];
269
- }
270
-
271
- /** The package-level `materialization.freshness` as a resolution layer. */
272
- function packageFreshnessLayer(
273
- cfg: WirePackageMaterialization | null | undefined,
274
- ): FreshnessLayer {
275
- if (!cfg) return {};
276
- const layer: FreshnessLayer = {};
277
- if (cfg.freshness?.window) layer.window = cfg.freshness.window;
278
- const fallback = cfg.freshness?.fallback;
279
- if (
280
- typeof fallback === "string" &&
281
- (FRESHNESS_FALLBACKS as readonly string[]).includes(fallback)
282
- ) {
283
- layer.fallback = fallback as FreshnessFallback;
284
- }
285
- return layer;
286
- }
287
-
288
- /** The source's `#@` tag, or undefined if it fails to parse (degrade to unset). */
289
- function safeSourceTag(source: PersistSource): ReadableTag | undefined {
290
- try {
291
- return source.annotations.parseAsTag("@").tag as ReadableTag;
292
- } catch {
293
- return undefined;
294
- }
295
- }
296
-
297
- /**
298
- * The model-file-level (`##`) tag resolved for the source, or undefined on a
299
- * parse failure. The model-file layer sits between the source annotation and
300
- * the package default in most-specific-wins resolution.
301
- */
302
- function safeModelTag(source: PersistSource): ReadableTag | undefined {
303
- try {
304
- return source.modelAnnotations.parseAsTag().tag as ReadableTag;
305
- } catch {
306
- return undefined;
307
- }
308
- }
309
-
310
- /**
311
- * Resolve a source's EFFECTIVE freshness with most-specific-wins precedence,
312
- * per field: source annotation > model-file default > package default. Reported
313
- * verbatim (unset at every level stays null so the control plane can
314
- * distinguish "unset" — apply platform default — from an explicit declaration).
315
- * Invalid `fallback` values are dropped, never defaulted. Freshness is valid in
316
- * both scope modes.
317
- */
318
- export function resolveFreshness(
319
- source: PersistSource,
320
- packageMaterialization: WirePackageMaterialization | null | undefined,
321
- ): WireFreshness | null {
322
- const layers: FreshnessLayer[] = [
323
- // `#@ persist` declares knobs bare — the annotation IS a materialization
324
- // declaration, so there is no envelope to look under.
325
- tagFreshnessLayer(safeSourceTag(source)),
326
- ...modelTagLayers(safeModelTag(source)).map(tagFreshnessLayer),
327
- packageFreshnessLayer(packageMaterialization),
328
- ];
329
-
330
- const window = layers.map((l) => l.window).find((v) => v !== undefined);
331
- const fallback = layers.map((l) => l.fallback).find((v) => v !== undefined);
332
-
333
- if (window === undefined && fallback === undefined) return null;
334
- const freshness: WireFreshness = {};
335
- if (window !== undefined) freshness.window = window;
336
- if (fallback !== undefined) freshness.fallback = fallback;
337
- return freshness;
338
- }
339
-
340
- /**
341
- * Read the `queryMetadata` property collection from one tag layer. A collection,
342
- * not a scalar: `queryMetadata { team="finance" env="prod" }` and the equivalent
343
- * dotted form `queryMetadata.team="finance"` both land here, and neither is
344
- * captured by the scalar {@link deriveAnnotationFields} loop.
345
- *
346
- * Every string-valued property is kept verbatim, including ones that violate
347
- * Malloy's bag contract — publish reports those as warnings and the runtime
348
- * clamps them, so an author's typo is visible somewhere instead of vanishing
349
- * between the annotation and the warehouse.
350
- */
351
- function tagQueryMetadataLayer(tag: ReadableTag | undefined): QueryMetadata {
352
- const subtree =
353
- tag && typeof tag.tag === "function"
354
- ? tag.tag("queryMetadata")
355
- : undefined;
356
- if (!subtree || typeof subtree.entries !== "function") return {};
357
- const layer: QueryMetadata = {};
358
- try {
359
- for (const [name, value] of subtree.entries()) {
360
- const text = value.text();
361
- if (text !== undefined) layer[name] = text;
362
- }
363
- } catch {
364
- // Degrade to {} — mirrors deriveAnnotationFields / deriveColumns.
365
- return {};
366
- }
367
- return layer;
368
- }
369
-
370
- /**
371
- * Resolve a source's EFFECTIVE per-query metadata, most-specific-wins PER
372
- * PROPERTY: `#@ persist queryMetadata.*` > model-file
373
- * `## materialization.queryMetadata.*` (bare `## queryMetadata.*` underneath) >
374
- * package `materialization.queryMetadata`.
375
- *
376
- * Per-property rather than per-layer, exactly like {@link resolveFreshness}, so a
377
- * package-wide `team` property survives a source that only overrides `workload`.
378
- * Null when no layer declares anything, so absence on the wire always means
379
- * "declared nowhere" rather than "declared empty".
380
- *
381
- * This is the value the publisher attaches (merged under its own context) to
382
- * every statement it issues while building the source. It is deliberately absent
383
- * from the source's content address: changing a tag must never re-address a
384
- * table.
385
- */
386
- export function resolveQueryMetadata(
387
- source: PersistSource,
388
- packageMaterialization: WirePackageMaterialization | null | undefined,
389
- ): QueryMetadata | null {
390
- // Least specific first, so a more specific layer overwrites property by
391
- // property.
392
- const layers: QueryMetadata[] = [
393
- packageMaterialization?.queryMetadata ?? {},
394
- ...modelTagLayers(safeModelTag(source))
395
- .map(tagQueryMetadataLayer)
396
- .reverse(),
397
- tagQueryMetadataLayer(safeSourceTag(source)),
398
- ];
399
- const resolved: QueryMetadata = Object.assign({}, ...layers);
400
- return Object.keys(resolved).length > 0 ? resolved : null;
401
- }
402
-
403
- /** Flatten Malloy's nested BuildNode.dependsOn into a list of sourceIDs. */
404
- export function flattenDependsOn(node: {
405
- dependsOn: { sourceID: string }[];
406
- }): string[] {
407
- return node.dependsOn.map((d) => d.sourceID);
408
- }
409
-
410
- /**
411
- * Yield every persist source of one graph in dependency order — each source's
412
- * persist dependencies before the source itself — resolving each node's
413
- * sourceID against the sources map and skipping nodes whose source is absent.
414
- * Centralizes the graph→source walk shared by the planning and build loops.
415
- *
416
- * <p>Malloy's {@code getBuildPlan()} puts only the <em>root</em> persist sources
417
- * (the terminals nothing else consumes) in {@code graph.nodes}; every transitive
418
- * persist dependency is nested under a root in its recursive {@code dependsOn}
419
- * tree (and present in {@code sources}). Walking only {@code graph.nodes}
420
- * silently skips every intermediate persist source, so it never gets
421
- * materialized (it only gets a table by coincidence when it shares a
422
- * sourceEntityId with a root). We post-order DFS the {@code dependsOn} tree
423
- * so dependencies are built first (a downstream build can then read its upstream
424
- * source's freshly materialized table), deduplicating shared (diamond)
425
- * dependencies by sourceID so each is yielded once. This mirrors the canonical
426
- * malloy-cli reference build loop (its {@code flattenBuildNodes} +
427
- * dedup-by-sourceID; see malloydata/malloy-cli src/malloy/build_graph.ts).
428
- */
429
- export function* iterGraphSources(
430
- graph: MalloyBuildGraph,
431
- sources: Record<string, PersistSource>,
432
- ): Iterable<PersistSource> {
433
- const seen = new Set<string>();
434
-
435
- function* visit(node: BuildNode): Iterable<PersistSource> {
436
- if (seen.has(node.sourceID)) return;
437
- // Dependencies first: a source built later in the run resolves its
438
- // upstream references against the tables built earlier (see the Manifest
439
- // threading in executeInstructedBuild).
440
- for (const dep of node.dependsOn) {
441
- yield* visit(dep);
442
- }
443
- seen.add(node.sourceID);
444
- const source = sources[node.sourceID];
445
- if (source) yield source;
446
- }
447
-
448
- for (const level of graph.nodes) {
449
- for (const node of level) {
450
- yield* visit(node);
451
- }
452
- }
453
- }
454
-
455
- /**
456
- * The sourceEntityId for a persist source: a stable content address of its
457
- * connection identity and canonical SQL. Centralizes the
458
- * (source, connectionDigests) call shape so planning, self-instruction, build,
459
- * and serve-time manifest resolution all agree on the same id.
460
- *
461
- * <p>This is the single seam between the publisher and the Malloy compiler's
462
- * source-identity recipe. Today it delegates to
463
- * {@code PersistSource.makeBuildId(connectionDigest, sql)} — the compiler's
464
- * current hex content hash. The connection's contribution is already
465
- * fingerprint-aware: when a connection carries an API `fingerprint`, its
466
- * digest IS that fingerprint verbatim (see applyConnectionFingerprint in
467
- * connection.ts), so ids stay stable across credential rotation. When the
468
- * compiler ships the scoped UUID5 sourceEntityId recipe (scope + connection
469
- * fingerprint + canonical SQL), swap the delegation below to the new compiler
470
- * API — this function is the only place the publisher derives the id.
471
- */
472
- export function computeSourceEntityId(
473
- source: PersistSource,
474
- connectionDigests: Record<string, string>,
475
- ): string {
476
- return source.makeBuildId(
477
- connectionDigests[source.connectionName],
478
- source.getSQL(),
479
- );
480
- }
481
-
482
- /**
483
- * Resolve a Map<name, Connection> for the names a step is about to touch.
484
- * The package's MalloyConfig caches each lookup, so repeated calls are cheap.
485
- * A failed lookup is logged and omitted; downstream code reports the missing
486
- * connection explicitly.
487
- */
488
- export async function resolvePackageConnections(
489
- pkg: { getMalloyConnection(name: string): Promise<MalloyConnection> },
490
- names: Iterable<string>,
491
- ): Promise<Map<string, MalloyConnection>> {
492
- const map = new Map<string, MalloyConnection>();
493
- const seen = new Set<string>();
494
- for (const name of names) {
495
- if (!name || seen.has(name)) continue;
496
- seen.add(name);
497
- try {
498
- map.set(name, await pkg.getMalloyConnection(name));
499
- } catch (err) {
500
- logger.warn(`Failed to resolve connection ${name}`, {
501
- error: errMessage(err),
502
- });
503
- }
504
- }
505
- return map;
506
- }
507
-
508
- /**
509
- * Compile every model in the package and collect the dependency-ordered
510
- * build graphs, persist sources, connection digests, and resolved
511
- * connections. The build plan is a pure function of the compiled model plus
512
- * connection config (no warehouse access).
513
- */
514
- /**
515
- * Names of sources in a compiled model that carry a `#@ persist` annotation but
516
- * are absent from the model's build plan — i.e. Malloy's getBuildPlan() did not
517
- * recognize them as a materializable build root and silently returned no graph
518
- * for them.
519
- *
520
- * BACKSTOP for a Malloy getBuildPlan() gap: it silently returns no graph for a
521
- * `#@ persist` source whose shape it doesn't treat as a build root (observed for
522
- * a filtered pass-through `X is <table> extend { where … }`, which stays type
523
- * `table`; only query-shaped sources are treated as roots). Without this check
524
- * the annotation is a silent no-op (served live, no build, no error). The primary
525
- * fix is in Malloy (recognize the shape or emit a diagnostic); until then, detect
526
- * the annotated-but-absent source here so callers can refuse loudly.
527
- *
528
- * Reads the annotation exactly as Malloy's own checkPersistAnnotation does
529
- * (`Annotations(def.annotations).parseAsTag('@').tag.has('persist')`). Best-effort
530
- * and fail-open: any introspection failure yields no dropped names rather than
531
- * risking a false positive that would wrongly fail a healthy build.
532
- */
533
- function detectDroppedPersistSources(
534
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
535
- malloyModel: any,
536
- recognizedNames: Set<string>,
537
- ): string[] {
538
- const dropped: string[] = [];
539
- try {
540
- const contents = malloyModel?._modelDef?.contents as
541
- | Record<string, { type?: string; annotations?: unknown }>
542
- | undefined;
543
- if (!contents) return dropped;
544
- for (const [name, def] of Object.entries(contents)) {
545
- if (recognizedNames.has(name)) continue;
546
- if (!def || !def.annotations) continue;
547
- let isPersist = false;
548
- try {
549
- isPersist = new Annotations(def.annotations)
550
- .parseAsTag("@")
551
- .tag.has("persist");
552
- } catch {
553
- continue;
554
- }
555
- if (isPersist) dropped.push(name);
556
- }
557
- } catch {
558
- // Fail open: never let introspection break the build plan.
559
- return [];
560
- }
561
- return dropped;
562
- }
563
-
564
- export async function compilePackageBuildPlan(
565
- pkg: BuildPlanPackage,
566
- signal?: AbortSignal,
567
- ): Promise<CompiledBuildPlan> {
568
- const allGraphs: MalloyBuildGraph[] = [];
569
- const allSources: Record<string, PersistSource> = {};
570
- const sourceModelPaths: Record<string, string> = {};
571
- const droppedPersistSources: { name: string; modelPath: string }[] = [];
572
-
573
- for (const modelPath of pkg.getModelPaths()) {
574
- // Only `.malloy` models declare persist sources. Skip `.malloynb`
575
- // notebooks: getModel() parses a model file as a flat model and throws on
576
- // the notebook's `>>>` cell delimiter, which would abort the entire
577
- // package build plan and silently drop every persist source in it.
578
- if (!modelPath.endsWith(MODEL_FILE_SUFFIX)) continue;
579
- if (signal?.aborted) throw new Error("Build cancelled");
580
-
581
- const { runtime, modelURL, importBaseURL } = await Model.getModelRuntime(
582
- pkg.getPackagePath(),
583
- modelPath,
584
- pkg.getMalloyConfig(),
585
- );
586
- const malloyModel = await runtime
587
- .loadModel(modelURL, { importBaseURL })
588
- .getModel();
589
-
590
- // getBuildPlan() THROWS "Model must have ##! experimental.persistence"
591
- // on any model that lacks the flag — it does NOT return empty. So a
592
- // header-less non-persist model in the package (e.g. an imported base
593
- // model that only defines raw sources) would abort the entire package
594
- // build plan, silently dropping every persist source in every other
595
- // model. Mirror Malloy's own guard and skip such models: without the flag
596
- // a model cannot carry a functioning persist source anyway. Models that
597
- // DO have the flag but declare no persist source return empty graphs and
598
- // are skipped by the `graphs.length === 0` check below.
599
- const modelTag = malloyModel.modelAnnotations.parseAsTag("!").tag;
600
- if (!modelTag.has("experimental", "persistence")) continue;
601
-
602
- const buildPlan = malloyModel.getBuildPlan();
603
- for (const msg of buildPlan.tagParseLog) {
604
- logger.warn("Persist annotation issue", {
605
- modelPath,
606
- message: msg.message,
607
- severity: msg.severity,
608
- });
609
- }
610
-
611
- // Detect `#@ persist` sources the plan didn't recognize (see
612
- // detectDroppedPersistSources). Runs BEFORE the empty-graphs `continue`, so
613
- // a model whose ONLY persist source is a dropped shape is still caught.
614
- const recognizedNames = new Set(
615
- Object.values(buildPlan.sources).map((s) => s.name),
616
- );
617
- for (const name of detectDroppedPersistSources(
618
- malloyModel,
619
- recognizedNames,
620
- )) {
621
- droppedPersistSources.push({ name, modelPath });
622
- }
623
-
624
- if (buildPlan.graphs.length === 0) continue;
625
-
626
- allGraphs.push(...buildPlan.graphs);
627
- for (const [sourceID, source] of Object.entries(buildPlan.sources)) {
628
- allSources[sourceID] = source;
629
- sourceModelPaths[sourceID] = modelPath;
630
- }
631
- }
632
-
633
- const connections = await resolvePackageConnections(
634
- pkg,
635
- allGraphs.map((g) => g.connectionName),
636
- );
637
- const connectionDigests: Record<string, string> = {};
638
- for (const graph of allGraphs) {
639
- const conn = connections.get(graph.connectionName);
640
- if (!conn) {
641
- // The connection failed to resolve (already warned in
642
- // resolvePackageConnections). Its sourceEntityIds will be computed
643
- // without a digest, so surface it as a discrete correctness signal
644
- // rather than skipping silently.
645
- recordConnectionDigestSkipped();
646
- logger.warn("Skipping connection digest; connection did not resolve", {
647
- connectionName: graph.connectionName,
648
- });
649
- continue;
650
- }
651
- if (!connectionDigests[graph.connectionName]) {
652
- // getDigest() is fingerprint-aware: a connection configured with an
653
- // API `fingerprint` returns it verbatim (applyConnectionFingerprint),
654
- // so a credential rotation does not re-address its sources.
655
- connectionDigests[graph.connectionName] = await conn.getDigest();
656
- }
657
- }
658
-
659
- return {
660
- graphs: allGraphs,
661
- sources: allSources,
662
- connectionDigests,
663
- connections,
664
- sourceModelPaths,
665
- droppedPersistSources,
666
- };
667
- }
668
-
669
- /** Project the Malloy build plan into the trimmed wire BuildPlan. */
670
- export function deriveBuildPlan(
671
- graphs: MalloyBuildGraph[],
672
- sources: Record<string, PersistSource>,
673
- connectionDigests: Record<string, string>,
674
- sourceNames?: string[],
675
- sourceModelPaths?: Record<string, string>,
676
- packageMaterialization?: WirePackageMaterialization | null,
677
- ): BuildPlan {
678
- const include = sourceNames ? new Set(sourceNames) : null;
679
-
680
- const wireGraphs: WireBuildGraph[] = graphs.map((graph) => ({
681
- connectionName: graph.connectionName,
682
- nodes: graph.nodes.map((level) =>
683
- level.map((node) => ({
684
- sourceID: node.sourceID,
685
- dependsOn: flattenDependsOn(node),
686
- })),
687
- ),
688
- }));
689
-
690
- const wireSources: Record<string, WirePersistSourcePlan> = {};
691
- for (const [sourceID, source] of Object.entries(sources)) {
692
- if (include && !include.has(source.name)) continue;
693
- const annotationFields = deriveAnnotationFields(source);
694
- // EFFECTIVE per-source freshness, resolved most-specific-wins
695
- // (source > model-file > package) and reported verbatim (null = unset at
696
- // every level). Freshness is a dotted/nested tag key, so it comes from
697
- // resolveFreshness rather than the scalar annotationFields map, and is
698
- // valid in both scope modes. Per-source `sharing`/`schedule` are NOT
699
- // emitted (retired from the contract); if a source declares either it is
700
- // rejected at publish (Package.persistencePolicyWarnings) — the raw keys
701
- // still ride `annotationFields` so the validator can detect them.
702
- wireSources[sourceID] = {
703
- name: source.name,
704
- sourceID: source.sourceID,
705
- connectionName: source.connectionName,
706
- dialect: source.dialectName,
707
- sourceEntityId: computeSourceEntityId(source, connectionDigests),
708
- sql: source.getSQL(),
709
- refresh: annotationFields.refresh ?? null,
710
- freshness: resolveFreshness(source, packageMaterialization),
711
- // EFFECTIVE per-source query metadata, resolved per property across the
712
- // same layer stack as freshness. A property collection rather than a
713
- // scalar, so it comes from resolveQueryMetadata rather than the
714
- // annotationFields map.
715
- queryMetadata: resolveQueryMetadata(source, packageMaterialization),
716
- columns: deriveColumns(source),
717
- annotationFields,
718
- modelPath: sourceModelPaths?.[sourceID],
719
- };
720
- }
721
-
722
- return { graphs: wireGraphs, sources: wireSources };
723
- }
724
-
725
- /**
726
- * Compile and project a package's build plan (null when the package declares no
727
- * materializable persist source), plus any `#@ persist` sources that were
728
- * silently dropped from the plan (see {@link detectDroppedPersistSources}) so
729
- * the caller can surface a load-time warning. A deterministic property of the
730
- * compiled package; feeds the read-only `Package.buildPlan` field.
731
- */
732
- export async function computePackageBuildPlan(
733
- pkg: BuildPlanPackage,
734
- signal?: AbortSignal,
735
- ): Promise<{
736
- plan: BuildPlan | null;
737
- droppedPersistSources: { name: string; modelPath: string }[];
738
- sourceEligibility: SourceEligibility;
739
- }> {
740
- const compiled = await compilePackageBuildPlan(pkg, signal);
741
- const droppedPersistSources = compiled.droppedPersistSources ?? [];
742
- const plan =
743
- compiled.graphs.length === 0
744
- ? null
745
- : deriveBuildPlan(
746
- compiled.graphs,
747
- compiled.sources,
748
- compiled.connectionDigests,
749
- undefined,
750
- compiled.sourceModelPaths,
751
- pkg.getMaterializationConfig?.() ?? null,
752
- );
753
- return {
754
- plan,
755
- droppedPersistSources,
756
- sourceEligibility: collectSourceEligibility(compiled.sources),
757
- };
758
- }
759
-
760
- /**
761
- * Which sources may be served from a materialized table, decided HERE because
762
- * this is where the compiled sources exist. The serve side needs the answer
763
- * without them: binding a host's manifest deliberately skips the recompile, so it
764
- * cannot re-derive eligibility and would otherwise have to take the host's word
765
- * for it.
766
- *
767
- * Carries the ELIGIBLE names, not just the refusals, so the serve side can
768
- * require a positive result. Absence is not eligibility: Malloy admits only
769
- * query-shaped sources as build roots, so a `#@ persist` on a filtered
770
- * pass-through (`X is <table> extend { where … }`, which stays type `table`)
771
- * never reaches this record at all — and that is the row-level-access shape. A
772
- * refusal-only view would read that silence as consent.
773
- *
774
- * Keyed by source NAME because that is what a serve binding carries.
775
- */
776
- export type SourceEligibility = {
777
- /** Sources compiled, examined, and found eligible. */
778
- eligible: string[];
779
- /** Source name -> why it was refused; for the operator-facing log. */
780
- refused: Record<string, string>;
781
- };
782
-
783
- function collectSourceEligibility(
784
- sources: Record<string, PersistSource>,
785
- ): SourceEligibility {
786
- const eligible: string[] = [];
787
- const refused: Record<string, string> = {};
788
- for (const source of Object.values(sources)) {
789
- try {
790
- assertMaterializationEligible(source);
791
- eligible.push(source.name);
792
- } catch (err) {
793
- refused[source.name] = errMessage(err);
794
- }
795
- }
796
- return { eligible, refused };
797
- }