@malloy-publisher/server 0.0.235 → 0.0.237

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