@malloy-publisher/server 0.0.234 → 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 (318) hide show
  1. package/dist/app/api-doc.yaml +47 -17
  2. package/dist/app/assets/{EnvironmentPage-DTZQ4Gxc.js → EnvironmentPage-BsAnavYN.js} +1 -1
  3. package/dist/app/assets/{HomePage-C5mlDPXK.js → HomePage-CADE138j.js} +1 -1
  4. package/dist/app/assets/{LightMode-DGNmhG0u.js → LightMode-Cfh7KzN8.js} +1 -1
  5. package/dist/app/assets/{MainPage-CVL_wmP4.js → MainPage-CO3pRlnV.js} +1 -1
  6. package/dist/app/assets/{MaterializationsPage-DmzMBCpy.js → MaterializationsPage-p9YjkRXZ.js} +1 -1
  7. package/dist/app/assets/{ModelPage-Dbvf4QbB.js → ModelPage-C1OSTv-x.js} +1 -1
  8. package/dist/app/assets/{PackagePage-DxdHc2Qs.js → PackagePage-e4kN75YR.js} +1 -1
  9. package/dist/app/assets/{RouteError-OJdT4tCd.js → RouteError-CzbfOkng.js} +1 -1
  10. package/dist/app/assets/{ThemeEditorPage-Bk7s0KXY.js → ThemeEditorPage-CciagFTq.js} +1 -1
  11. package/dist/app/assets/{WorkbookPage-j_vCWdN3.js → WorkbookPage-DNWmkCXa.js} +1 -1
  12. package/dist/app/assets/{core-Rj_4rRnA.es-DoIfLxDJ.js → core-Rj_4rRnA.es-BZyvITuO.js} +1 -1
  13. package/dist/app/assets/{index-B_jKMR35.js → index-CH2AcDzc.js} +4 -4
  14. package/dist/app/assets/{index-hw-xn0X7.js → index-DQa463gC.js} +2 -2
  15. package/dist/app/assets/{index-DWIe_hK0.js → index-DQpV7MyA.js} +1 -1
  16. package/dist/app/assets/{index-BabP-V-S.js → index-DzaYbhnD.js} +1 -1
  17. package/dist/app/assets/{index-D-rDyK11.js → index-VBbcc8s6.js} +1 -1
  18. package/dist/app/index.html +1 -1
  19. package/dist/package_load_worker.mjs +8 -1
  20. package/dist/server.mjs +471 -54
  21. package/package.json +16 -13
  22. package/.eslintrc.json +0 -44
  23. package/.prettierignore +0 -1
  24. package/.prettierrc +0 -4
  25. package/build.ts +0 -97
  26. package/eslint.config.mjs +0 -76
  27. package/k6-tests/README.md +0 -144
  28. package/k6-tests/bun.lock +0 -797
  29. package/k6-tests/load-test/load-test-crud-connections.ts +0 -263
  30. package/k6-tests/load-test/load-test-crud-packages.ts +0 -461
  31. package/k6-tests/load-test/load-test-crud-projects.ts +0 -160
  32. package/k6-tests/load-test/load-test-crud.ts +0 -142
  33. package/k6-tests/load-test/load-test.ts +0 -784
  34. package/k6-tests/package.json +0 -27
  35. package/k6-tests/scripts/run-with-otel.sh +0 -50
  36. package/k6-tests/smoke-test/smoke-test.ts +0 -158
  37. package/k6-tests/tsconfig.json +0 -29
  38. package/k6-tests/utils/client_factory.ts +0 -128
  39. package/k6-tests/utils/common.ts +0 -489
  40. package/k6-tests/utils/logger.ts +0 -62
  41. package/k6-tests/utils/types.d.ts +0 -35
  42. package/malloy_mcp.dxt +0 -0
  43. package/publisher.config.example.bigquery.json +0 -33
  44. package/publisher.config.example.duckdb.json +0 -23
  45. package/publisher.config.json +0 -23
  46. package/scripts/bake-duckdb-extensions.js +0 -110
  47. package/src/config.spec.ts +0 -1535
  48. package/src/config.theme.spec.ts +0 -178
  49. package/src/config.ts +0 -1142
  50. package/src/constants.ts +0 -105
  51. package/src/controller/compile.controller.ts +0 -40
  52. package/src/controller/connection.controller.spec.ts +0 -849
  53. package/src/controller/connection.controller.ts +0 -901
  54. package/src/controller/database.controller.ts +0 -24
  55. package/src/controller/materialization.controller.spec.ts +0 -293
  56. package/src/controller/materialization.controller.ts +0 -319
  57. package/src/controller/model.controller.ts +0 -164
  58. package/src/controller/package.controller.spec.ts +0 -273
  59. package/src/controller/package.controller.ts +0 -306
  60. package/src/controller/query.controller.ts +0 -152
  61. package/src/controller/theme.controller.ts +0 -83
  62. package/src/controller/watch-mode.controller.ts +0 -243
  63. package/src/data_styles.ts +0 -71
  64. package/src/default-publisher.config.json +0 -23
  65. package/src/ducklake_version.spec.ts +0 -163
  66. package/src/ducklake_version.ts +0 -153
  67. package/src/errors.spec.ts +0 -119
  68. package/src/errors.ts +0 -237
  69. package/src/filter_deprecation.spec.ts +0 -64
  70. package/src/filter_deprecation.ts +0 -42
  71. package/src/health.spec.ts +0 -123
  72. package/src/health.ts +0 -233
  73. package/src/heap_check.spec.ts +0 -144
  74. package/src/heap_check.ts +0 -144
  75. package/src/instrumentation.ts +0 -214
  76. package/src/json_utils.spec.ts +0 -51
  77. package/src/json_utils.ts +0 -33
  78. package/src/logger.spec.ts +0 -210
  79. package/src/logger.ts +0 -251
  80. package/src/malloy_pin_prereqs.spec.ts +0 -25
  81. package/src/materialization_metrics.spec.ts +0 -169
  82. package/src/materialization_metrics.ts +0 -346
  83. package/src/mcp/error_messages.spec.ts +0 -35
  84. package/src/mcp/error_messages.ts +0 -183
  85. package/src/mcp/handler_utils.spec.ts +0 -108
  86. package/src/mcp/handler_utils.ts +0 -251
  87. package/src/mcp/mcp_constants.ts +0 -6
  88. package/src/mcp/query_envelope.spec.ts +0 -229
  89. package/src/mcp/query_envelope.ts +0 -240
  90. package/src/mcp/server.protocol.spec.ts +0 -250
  91. package/src/mcp/server.ts +0 -87
  92. package/src/mcp/skills/build_skills_bundle.spec.ts +0 -51
  93. package/src/mcp/skills/build_skills_bundle.ts +0 -176
  94. package/src/mcp/skills/skills_bundle.json +0 -1
  95. package/src/mcp/skills/skills_bundle.spec.ts +0 -171
  96. package/src/mcp/tool_response.spec.ts +0 -108
  97. package/src/mcp/tool_response.ts +0 -138
  98. package/src/mcp/tools/compile_tool.spec.ts +0 -315
  99. package/src/mcp/tools/compile_tool.ts +0 -208
  100. package/src/mcp/tools/docs_search/build_docs_index.ts +0 -132
  101. package/src/mcp/tools/docs_search/malloy_docs_index.json +0 -1
  102. package/src/mcp/tools/docs_search_tool.spec.ts +0 -32
  103. package/src/mcp/tools/docs_search_tool.ts +0 -128
  104. package/src/mcp/tools/embedding_index.spec.ts +0 -1236
  105. package/src/mcp/tools/embedding_index.ts +0 -808
  106. package/src/mcp/tools/execute_query_tool.spec.ts +0 -294
  107. package/src/mcp/tools/execute_query_tool.ts +0 -316
  108. package/src/mcp/tools/get_context_eval.ts +0 -275
  109. package/src/mcp/tools/get_context_tool.spec.ts +0 -592
  110. package/src/mcp/tools/get_context_tool.ts +0 -585
  111. package/src/mcp/tools/reload_package_tool.spec.ts +0 -232
  112. package/src/mcp/tools/reload_package_tool.ts +0 -132
  113. package/src/mcp_config.spec.ts +0 -919
  114. package/src/mcp_config.ts +0 -425
  115. package/src/oom_guards.integration.spec.ts +0 -269
  116. package/src/package_load/package_load_pool.spec.ts +0 -252
  117. package/src/package_load/package_load_pool.ts +0 -931
  118. package/src/package_load/package_load_worker.ts +0 -1034
  119. package/src/package_load/protocol.ts +0 -364
  120. package/src/package_load/rpc_wait_accountant.spec.ts +0 -109
  121. package/src/package_load/rpc_wait_accountant.ts +0 -76
  122. package/src/package_load_metrics.spec.ts +0 -114
  123. package/src/package_load_metrics.ts +0 -127
  124. package/src/path_safety.spec.ts +0 -158
  125. package/src/path_safety.ts +0 -146
  126. package/src/pg_helpers.spec.ts +0 -223
  127. package/src/pg_helpers.ts +0 -52
  128. package/src/query_cap_metrics.spec.ts +0 -89
  129. package/src/query_cap_metrics.ts +0 -116
  130. package/src/query_concurrency.spec.ts +0 -247
  131. package/src/query_concurrency.ts +0 -237
  132. package/src/query_metadata_metrics.ts +0 -49
  133. package/src/query_param_utils.ts +0 -29
  134. package/src/query_timeout.spec.ts +0 -224
  135. package/src/query_timeout.ts +0 -179
  136. package/src/runtime/publisher.js +0 -328
  137. package/src/server-old.ts +0 -988
  138. package/src/server.ts +0 -2059
  139. package/src/service/annotations.spec.ts +0 -118
  140. package/src/service/annotations.ts +0 -91
  141. package/src/service/authorize.spec.ts +0 -154
  142. package/src/service/authorize.ts +0 -498
  143. package/src/service/authorize_integration.spec.ts +0 -2041
  144. package/src/service/build_plan.spec.ts +0 -685
  145. package/src/service/build_plan.ts +0 -797
  146. package/src/service/compile_authorize.spec.ts +0 -85
  147. package/src/service/compile_fragment_techniques.spec.ts +0 -156
  148. package/src/service/connection.spec.ts +0 -2360
  149. package/src/service/connection.ts +0 -2187
  150. package/src/service/connection_config.spec.ts +0 -734
  151. package/src/service/connection_config.ts +0 -899
  152. package/src/service/connection_federation.spec.ts +0 -184
  153. package/src/service/connection_fingerprint.spec.ts +0 -102
  154. package/src/service/connection_service.spec.ts +0 -711
  155. package/src/service/connection_service.ts +0 -291
  156. package/src/service/cron_evaluator.spec.ts +0 -79
  157. package/src/service/cron_evaluator.ts +0 -105
  158. package/src/service/db_utils.spec.ts +0 -1085
  159. package/src/service/db_utils.ts +0 -1502
  160. package/src/service/duckdb_instance_isolation.spec.ts +0 -137
  161. package/src/service/ducklake_lazy_attach.spec.ts +0 -110
  162. package/src/service/embedding_provider.spec.ts +0 -329
  163. package/src/service/embedding_provider.ts +0 -236
  164. package/src/service/environment.ts +0 -2191
  165. package/src/service/environment_admission.spec.ts +0 -344
  166. package/src/service/environment_compile.spec.ts +0 -197
  167. package/src/service/environment_store.spec.ts +0 -2274
  168. package/src/service/environment_store.ts +0 -2434
  169. package/src/service/environment_store_anchoring.spec.ts +0 -107
  170. package/src/service/environment_store_clone.spec.ts +0 -350
  171. package/src/service/explore_visibility.spec.ts +0 -434
  172. package/src/service/exports_probe.spec.ts +0 -107
  173. package/src/service/extension_fetch_policy.spec.ts +0 -256
  174. package/src/service/filter.spec.ts +0 -458
  175. package/src/service/filter.ts +0 -341
  176. package/src/service/filter_bypass.spec.ts +0 -418
  177. package/src/service/filter_integration.spec.ts +0 -1004
  178. package/src/service/freshness.spec.ts +0 -183
  179. package/src/service/freshness.ts +0 -112
  180. package/src/service/gcs_s3_utils.ts +0 -427
  181. package/src/service/given.ts +0 -105
  182. package/src/service/givens_integration.spec.ts +0 -219
  183. package/src/service/manifest_loader.spec.ts +0 -189
  184. package/src/service/manifest_loader.ts +0 -156
  185. package/src/service/materialization_build_session.spec.ts +0 -435
  186. package/src/service/materialization_build_session.ts +0 -681
  187. package/src/service/materialization_config_validation.spec.ts +0 -99
  188. package/src/service/materialization_config_validation.ts +0 -120
  189. package/src/service/materialization_eligibility.spec.ts +0 -158
  190. package/src/service/materialization_eligibility.ts +0 -305
  191. package/src/service/materialization_schedule_surface.spec.ts +0 -256
  192. package/src/service/materialization_scheduler.spec.ts +0 -289
  193. package/src/service/materialization_scheduler.ts +0 -304
  194. package/src/service/materialization_serve_transform.spec.ts +0 -1003
  195. package/src/service/materialization_serve_transform.ts +0 -779
  196. package/src/service/materialization_service.spec.ts +0 -2373
  197. package/src/service/materialization_service.ts +0 -2342
  198. package/src/service/materialization_test_fixtures.ts +0 -249
  199. package/src/service/model.spec.ts +0 -1157
  200. package/src/service/model.ts +0 -3370
  201. package/src/service/model_limits.spec.ts +0 -209
  202. package/src/service/model_limits.ts +0 -131
  203. package/src/service/model_storage_serve.spec.ts +0 -193
  204. package/src/service/model_storage_serve_joins.spec.ts +0 -193
  205. package/src/service/package.spec.ts +0 -612
  206. package/src/service/package.ts +0 -1799
  207. package/src/service/package_manifest.spec.ts +0 -230
  208. package/src/service/package_manifest.ts +0 -241
  209. package/src/service/package_memory_governor.spec.ts +0 -173
  210. package/src/service/package_memory_governor.ts +0 -233
  211. package/src/service/package_quote_bound_tables.spec.ts +0 -171
  212. package/src/service/package_race.spec.ts +0 -208
  213. package/src/service/package_reload_safety.spec.ts +0 -193
  214. package/src/service/package_rollback.spec.ts +0 -190
  215. package/src/service/package_worker_path.spec.ts +0 -645
  216. package/src/service/path_injection.spec.ts +0 -39
  217. package/src/service/persist_annotation_validation.spec.ts +0 -89
  218. package/src/service/persist_annotation_validation.ts +0 -59
  219. package/src/service/persistence_policy.spec.ts +0 -374
  220. package/src/service/proxy.spec.ts +0 -414
  221. package/src/service/proxy.ts +0 -248
  222. package/src/service/query_boundary.spec.ts +0 -470
  223. package/src/service/query_metadata.spec.ts +0 -408
  224. package/src/service/query_metadata.ts +0 -492
  225. package/src/service/query_metadata_identity.spec.ts +0 -149
  226. package/src/service/query_text.spec.ts +0 -79
  227. package/src/service/query_text.ts +0 -43
  228. package/src/service/quoting.spec.ts +0 -135
  229. package/src/service/quoting.ts +0 -78
  230. package/src/service/resolve_environment.ts +0 -15
  231. package/src/service/restricted_mode.spec.ts +0 -299
  232. package/src/service/source_extraction.ts +0 -225
  233. package/src/service/theme_store.ts +0 -199
  234. package/src/storage/DatabaseInterface.ts +0 -198
  235. package/src/storage/StorageManager.ts +0 -113
  236. package/src/storage/duckdb/ConnectionRepository.ts +0 -143
  237. package/src/storage/duckdb/DuckDBConnection.ts +0 -123
  238. package/src/storage/duckdb/DuckDBRepository.ts +0 -208
  239. package/src/storage/duckdb/EnvironmentRepository.ts +0 -148
  240. package/src/storage/duckdb/MaterializationRepository.spec.ts +0 -191
  241. package/src/storage/duckdb/MaterializationRepository.ts +0 -336
  242. package/src/storage/duckdb/PackageRepository.ts +0 -135
  243. package/src/storage/duckdb/schema.ts +0 -229
  244. package/src/stream_helpers.spec.ts +0 -280
  245. package/src/stream_helpers.ts +0 -162
  246. package/src/telemetry.ts +0 -20
  247. package/src/test_helpers/metrics_harness.ts +0 -166
  248. package/src/theme_key_parity.spec.ts +0 -57
  249. package/src/utils.ts +0 -29
  250. package/tests/fixtures/authorize-compile/model.malloy +0 -9
  251. package/tests/fixtures/authorize-compile/publisher.json +0 -4
  252. package/tests/fixtures/html-pages-nopublic/model.malloy +0 -1
  253. package/tests/fixtures/html-pages-nopublic/publisher.json +0 -5
  254. package/tests/fixtures/html-pages-test/data.csv +0 -3
  255. package/tests/fixtures/html-pages-test/public/assets/app.css +0 -3
  256. package/tests/fixtures/html-pages-test/public/barehtml.html +0 -4
  257. package/tests/fixtures/html-pages-test/public/bodymeta.html +0 -8
  258. package/tests/fixtures/html-pages-test/public/data.json +0 -1
  259. package/tests/fixtures/html-pages-test/public/fitcomment.html +0 -11
  260. package/tests/fixtures/html-pages-test/public/index.html +0 -9
  261. package/tests/fixtures/html-pages-test/public/nohead.html +0 -8
  262. package/tests/fixtures/html-pages-test/public/notfit.html +0 -13
  263. package/tests/fixtures/html-pages-test/public/slides.html +0 -12
  264. package/tests/fixtures/html-pages-test/public/sub/page2.html +0 -9
  265. package/tests/fixtures/html-pages-test/public/unterminated.html +0 -10
  266. package/tests/fixtures/html-pages-test/publisher.json +0 -5
  267. package/tests/fixtures/html-pages-test/report.malloy +0 -1
  268. package/tests/fixtures/persist-multi-level/data/orders.csv +0 -5
  269. package/tests/fixtures/persist-multi-level/multi_level.malloy +0 -18
  270. package/tests/fixtures/persist-multi-level/publisher.json +0 -5
  271. package/tests/fixtures/persist-schedule-test/data/orders.csv +0 -5
  272. package/tests/fixtures/persist-schedule-test/persist_schedule_test.malloy +0 -11
  273. package/tests/fixtures/persist-schedule-test/publisher.json +0 -7
  274. package/tests/fixtures/persist-schedule-warn-test/data/orders.csv +0 -5
  275. package/tests/fixtures/persist-schedule-warn-test/persist_warn_test.malloy +0 -11
  276. package/tests/fixtures/persist-schedule-warn-test/publisher.json +0 -6
  277. package/tests/fixtures/persist-test/data/orders.csv +0 -5
  278. package/tests/fixtures/persist-test/persist_test.malloy +0 -11
  279. package/tests/fixtures/persist-test/publisher.json +0 -5
  280. package/tests/fixtures/publisher.config.json +0 -15
  281. package/tests/fixtures/query-givens/data/orders.csv +0 -7
  282. package/tests/fixtures/query-givens/model.malloy +0 -34
  283. package/tests/fixtures/query-givens/publisher.json +0 -5
  284. package/tests/fixtures/xlsx/database.xlsx +0 -0
  285. package/tests/harness/mcp_test_setup.ts +0 -332
  286. package/tests/harness/rest_e2e.ts +0 -68
  287. package/tests/integration/authorize/compile_authorize_http.integration.spec.ts +0 -92
  288. package/tests/integration/concurrent_package/concurrent_package.integration.spec.ts +0 -280
  289. package/tests/integration/duckdb_storage/duckdb_storage.integration.spec.ts +0 -138
  290. package/tests/integration/first_boot/readiness_line.integration.spec.ts +0 -177
  291. package/tests/integration/html_pages/html_pages.integration.spec.ts +0 -440
  292. package/tests/integration/legacy_routes/legacy_routes.integration.spec.ts +0 -259
  293. package/tests/integration/materialization/environment_list.integration.spec.ts +0 -133
  294. package/tests/integration/materialization/freshness_gate.integration.spec.ts +0 -292
  295. package/tests/integration/materialization/manifest_binding.integration.spec.ts +0 -430
  296. package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +0 -441
  297. package/tests/integration/materialization/orchestrated_rebind.integration.spec.ts +0 -150
  298. package/tests/integration/materialization/reference_manifest.integration.spec.ts +0 -251
  299. package/tests/integration/materialization/schedule_edit.integration.spec.ts +0 -126
  300. package/tests/integration/materialization/scheduler.integration.spec.ts +0 -200
  301. package/tests/integration/materialization/scheduler_recovery.integration.spec.ts +0 -172
  302. package/tests/integration/materialization/scheduler_transitions.integration.spec.ts +0 -256
  303. package/tests/integration/materialization/scheduler_wiring.integration.spec.ts +0 -143
  304. package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +0 -438
  305. package/tests/integration/mcp/mcp_get_context_semantic.integration.spec.ts +0 -235
  306. package/tests/integration/mcp/mcp_transport.integration.spec.ts +0 -98
  307. package/tests/integration/query_givens/query_givens.integration.spec.ts +0 -146
  308. package/tests/integration/query_givens/query_givens_authorize.integration.spec.ts +0 -121
  309. package/tests/integration/sdk_givens/sdk_givens.integration.spec.ts +0 -110
  310. package/tests/integration/watch-mode/watch_mode.integration.spec.ts +0 -421
  311. package/tests/unit/controller/theme.controller.spec.ts +0 -141
  312. package/tests/unit/duckdb/attached_databases.test.ts +0 -1243
  313. package/tests/unit/duckdb/duckdb_connection.test.ts +0 -181
  314. package/tests/unit/duckdb/legacy_schema_migration.test.ts +0 -197
  315. package/tests/unit/duckdb/repositories.test.ts +0 -208
  316. package/tests/unit/ducklake/ducklake.test.ts +0 -952
  317. package/tests/unit/service/theme_store.spec.ts +0 -139
  318. 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
- }