@malloy-publisher/server 0.0.235 → 0.0.236

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (301) hide show
  1. package/dist/app/api-doc.yaml +47 -17
  2. package/dist/package_load_worker.mjs +8 -1
  3. package/dist/server.mjs +471 -54
  4. package/package.json +5 -2
  5. package/.eslintrc.json +0 -44
  6. package/.prettierignore +0 -1
  7. package/.prettierrc +0 -4
  8. package/build.ts +0 -97
  9. package/eslint.config.mjs +0 -76
  10. package/k6-tests/README.md +0 -144
  11. package/k6-tests/bun.lock +0 -797
  12. package/k6-tests/load-test/load-test-crud-connections.ts +0 -263
  13. package/k6-tests/load-test/load-test-crud-packages.ts +0 -461
  14. package/k6-tests/load-test/load-test-crud-projects.ts +0 -160
  15. package/k6-tests/load-test/load-test-crud.ts +0 -142
  16. package/k6-tests/load-test/load-test.ts +0 -784
  17. package/k6-tests/package.json +0 -27
  18. package/k6-tests/scripts/run-with-otel.sh +0 -50
  19. package/k6-tests/smoke-test/smoke-test.ts +0 -158
  20. package/k6-tests/tsconfig.json +0 -29
  21. package/k6-tests/utils/client_factory.ts +0 -128
  22. package/k6-tests/utils/common.ts +0 -489
  23. package/k6-tests/utils/logger.ts +0 -62
  24. package/k6-tests/utils/types.d.ts +0 -35
  25. package/malloy_mcp.dxt +0 -0
  26. package/publisher.config.example.bigquery.json +0 -33
  27. package/publisher.config.example.duckdb.json +0 -23
  28. package/publisher.config.json +0 -23
  29. package/scripts/bake-duckdb-extensions.js +0 -110
  30. package/src/config.spec.ts +0 -1535
  31. package/src/config.theme.spec.ts +0 -178
  32. package/src/config.ts +0 -1142
  33. package/src/constants.ts +0 -105
  34. package/src/controller/compile.controller.ts +0 -40
  35. package/src/controller/connection.controller.spec.ts +0 -849
  36. package/src/controller/connection.controller.ts +0 -901
  37. package/src/controller/database.controller.ts +0 -24
  38. package/src/controller/materialization.controller.spec.ts +0 -293
  39. package/src/controller/materialization.controller.ts +0 -319
  40. package/src/controller/model.controller.ts +0 -164
  41. package/src/controller/package.controller.spec.ts +0 -273
  42. package/src/controller/package.controller.ts +0 -306
  43. package/src/controller/query.controller.ts +0 -152
  44. package/src/controller/theme.controller.ts +0 -83
  45. package/src/controller/watch-mode.controller.ts +0 -243
  46. package/src/data_styles.ts +0 -71
  47. package/src/default-publisher.config.json +0 -23
  48. package/src/ducklake_version.spec.ts +0 -163
  49. package/src/ducklake_version.ts +0 -153
  50. package/src/errors.spec.ts +0 -119
  51. package/src/errors.ts +0 -237
  52. package/src/filter_deprecation.spec.ts +0 -64
  53. package/src/filter_deprecation.ts +0 -42
  54. package/src/health.spec.ts +0 -123
  55. package/src/health.ts +0 -233
  56. package/src/heap_check.spec.ts +0 -144
  57. package/src/heap_check.ts +0 -144
  58. package/src/instrumentation.ts +0 -214
  59. package/src/json_utils.spec.ts +0 -51
  60. package/src/json_utils.ts +0 -33
  61. package/src/logger.spec.ts +0 -210
  62. package/src/logger.ts +0 -251
  63. package/src/malloy_pin_prereqs.spec.ts +0 -25
  64. package/src/materialization_metrics.spec.ts +0 -169
  65. package/src/materialization_metrics.ts +0 -346
  66. package/src/mcp/error_messages.spec.ts +0 -35
  67. package/src/mcp/error_messages.ts +0 -183
  68. package/src/mcp/handler_utils.spec.ts +0 -108
  69. package/src/mcp/handler_utils.ts +0 -251
  70. package/src/mcp/mcp_constants.ts +0 -6
  71. package/src/mcp/query_envelope.spec.ts +0 -229
  72. package/src/mcp/query_envelope.ts +0 -240
  73. package/src/mcp/server.protocol.spec.ts +0 -250
  74. package/src/mcp/server.ts +0 -87
  75. package/src/mcp/skills/build_skills_bundle.spec.ts +0 -51
  76. package/src/mcp/skills/build_skills_bundle.ts +0 -176
  77. package/src/mcp/skills/skills_bundle.json +0 -1
  78. package/src/mcp/skills/skills_bundle.spec.ts +0 -171
  79. package/src/mcp/tool_response.spec.ts +0 -108
  80. package/src/mcp/tool_response.ts +0 -138
  81. package/src/mcp/tools/compile_tool.spec.ts +0 -315
  82. package/src/mcp/tools/compile_tool.ts +0 -208
  83. package/src/mcp/tools/docs_search/build_docs_index.ts +0 -132
  84. package/src/mcp/tools/docs_search/malloy_docs_index.json +0 -1
  85. package/src/mcp/tools/docs_search_tool.spec.ts +0 -32
  86. package/src/mcp/tools/docs_search_tool.ts +0 -128
  87. package/src/mcp/tools/embedding_index.spec.ts +0 -1236
  88. package/src/mcp/tools/embedding_index.ts +0 -808
  89. package/src/mcp/tools/execute_query_tool.spec.ts +0 -294
  90. package/src/mcp/tools/execute_query_tool.ts +0 -316
  91. package/src/mcp/tools/get_context_eval.ts +0 -275
  92. package/src/mcp/tools/get_context_tool.spec.ts +0 -592
  93. package/src/mcp/tools/get_context_tool.ts +0 -585
  94. package/src/mcp/tools/reload_package_tool.spec.ts +0 -232
  95. package/src/mcp/tools/reload_package_tool.ts +0 -132
  96. package/src/mcp_config.spec.ts +0 -919
  97. package/src/mcp_config.ts +0 -425
  98. package/src/oom_guards.integration.spec.ts +0 -269
  99. package/src/package_load/package_load_pool.spec.ts +0 -252
  100. package/src/package_load/package_load_pool.ts +0 -931
  101. package/src/package_load/package_load_worker.ts +0 -1034
  102. package/src/package_load/protocol.ts +0 -364
  103. package/src/package_load/rpc_wait_accountant.spec.ts +0 -109
  104. package/src/package_load/rpc_wait_accountant.ts +0 -76
  105. package/src/package_load_metrics.spec.ts +0 -114
  106. package/src/package_load_metrics.ts +0 -127
  107. package/src/path_safety.spec.ts +0 -158
  108. package/src/path_safety.ts +0 -146
  109. package/src/pg_helpers.spec.ts +0 -223
  110. package/src/pg_helpers.ts +0 -52
  111. package/src/query_cap_metrics.spec.ts +0 -89
  112. package/src/query_cap_metrics.ts +0 -116
  113. package/src/query_concurrency.spec.ts +0 -247
  114. package/src/query_concurrency.ts +0 -237
  115. package/src/query_metadata_metrics.ts +0 -49
  116. package/src/query_param_utils.ts +0 -29
  117. package/src/query_timeout.spec.ts +0 -224
  118. package/src/query_timeout.ts +0 -179
  119. package/src/runtime/publisher.js +0 -328
  120. package/src/server-old.ts +0 -988
  121. package/src/server.ts +0 -2059
  122. package/src/service/annotations.spec.ts +0 -118
  123. package/src/service/annotations.ts +0 -91
  124. package/src/service/authorize.spec.ts +0 -154
  125. package/src/service/authorize.ts +0 -498
  126. package/src/service/authorize_integration.spec.ts +0 -2041
  127. package/src/service/build_plan.spec.ts +0 -685
  128. package/src/service/build_plan.ts +0 -797
  129. package/src/service/compile_authorize.spec.ts +0 -85
  130. package/src/service/compile_fragment_techniques.spec.ts +0 -156
  131. package/src/service/connection.spec.ts +0 -2360
  132. package/src/service/connection.ts +0 -2187
  133. package/src/service/connection_config.spec.ts +0 -734
  134. package/src/service/connection_config.ts +0 -899
  135. package/src/service/connection_federation.spec.ts +0 -184
  136. package/src/service/connection_fingerprint.spec.ts +0 -102
  137. package/src/service/connection_service.spec.ts +0 -711
  138. package/src/service/connection_service.ts +0 -291
  139. package/src/service/cron_evaluator.spec.ts +0 -79
  140. package/src/service/cron_evaluator.ts +0 -105
  141. package/src/service/db_utils.spec.ts +0 -1085
  142. package/src/service/db_utils.ts +0 -1502
  143. package/src/service/duckdb_instance_isolation.spec.ts +0 -137
  144. package/src/service/ducklake_lazy_attach.spec.ts +0 -110
  145. package/src/service/embedding_provider.spec.ts +0 -329
  146. package/src/service/embedding_provider.ts +0 -236
  147. package/src/service/environment.ts +0 -2191
  148. package/src/service/environment_admission.spec.ts +0 -344
  149. package/src/service/environment_compile.spec.ts +0 -197
  150. package/src/service/environment_store.spec.ts +0 -2274
  151. package/src/service/environment_store.ts +0 -2434
  152. package/src/service/environment_store_anchoring.spec.ts +0 -107
  153. package/src/service/environment_store_clone.spec.ts +0 -350
  154. package/src/service/explore_visibility.spec.ts +0 -434
  155. package/src/service/exports_probe.spec.ts +0 -107
  156. package/src/service/extension_fetch_policy.spec.ts +0 -256
  157. package/src/service/filter.spec.ts +0 -458
  158. package/src/service/filter.ts +0 -341
  159. package/src/service/filter_bypass.spec.ts +0 -418
  160. package/src/service/filter_integration.spec.ts +0 -1004
  161. package/src/service/freshness.spec.ts +0 -183
  162. package/src/service/freshness.ts +0 -112
  163. package/src/service/gcs_s3_utils.ts +0 -427
  164. package/src/service/given.ts +0 -105
  165. package/src/service/givens_integration.spec.ts +0 -219
  166. package/src/service/manifest_loader.spec.ts +0 -189
  167. package/src/service/manifest_loader.ts +0 -156
  168. package/src/service/materialization_build_session.spec.ts +0 -435
  169. package/src/service/materialization_build_session.ts +0 -681
  170. package/src/service/materialization_config_validation.spec.ts +0 -99
  171. package/src/service/materialization_config_validation.ts +0 -120
  172. package/src/service/materialization_eligibility.spec.ts +0 -158
  173. package/src/service/materialization_eligibility.ts +0 -305
  174. package/src/service/materialization_schedule_surface.spec.ts +0 -256
  175. package/src/service/materialization_scheduler.spec.ts +0 -289
  176. package/src/service/materialization_scheduler.ts +0 -304
  177. package/src/service/materialization_serve_transform.spec.ts +0 -1003
  178. package/src/service/materialization_serve_transform.ts +0 -779
  179. package/src/service/materialization_service.spec.ts +0 -2373
  180. package/src/service/materialization_service.ts +0 -2342
  181. package/src/service/materialization_test_fixtures.ts +0 -249
  182. package/src/service/model.spec.ts +0 -1157
  183. package/src/service/model.ts +0 -3370
  184. package/src/service/model_limits.spec.ts +0 -209
  185. package/src/service/model_limits.ts +0 -131
  186. package/src/service/model_storage_serve.spec.ts +0 -193
  187. package/src/service/model_storage_serve_joins.spec.ts +0 -193
  188. package/src/service/package.spec.ts +0 -612
  189. package/src/service/package.ts +0 -1799
  190. package/src/service/package_manifest.spec.ts +0 -230
  191. package/src/service/package_manifest.ts +0 -241
  192. package/src/service/package_memory_governor.spec.ts +0 -173
  193. package/src/service/package_memory_governor.ts +0 -233
  194. package/src/service/package_quote_bound_tables.spec.ts +0 -171
  195. package/src/service/package_race.spec.ts +0 -208
  196. package/src/service/package_reload_safety.spec.ts +0 -193
  197. package/src/service/package_rollback.spec.ts +0 -190
  198. package/src/service/package_worker_path.spec.ts +0 -645
  199. package/src/service/path_injection.spec.ts +0 -39
  200. package/src/service/persist_annotation_validation.spec.ts +0 -89
  201. package/src/service/persist_annotation_validation.ts +0 -59
  202. package/src/service/persistence_policy.spec.ts +0 -374
  203. package/src/service/proxy.spec.ts +0 -414
  204. package/src/service/proxy.ts +0 -248
  205. package/src/service/query_boundary.spec.ts +0 -470
  206. package/src/service/query_metadata.spec.ts +0 -408
  207. package/src/service/query_metadata.ts +0 -492
  208. package/src/service/query_metadata_identity.spec.ts +0 -149
  209. package/src/service/query_text.spec.ts +0 -79
  210. package/src/service/query_text.ts +0 -43
  211. package/src/service/quoting.spec.ts +0 -135
  212. package/src/service/quoting.ts +0 -78
  213. package/src/service/resolve_environment.ts +0 -15
  214. package/src/service/restricted_mode.spec.ts +0 -299
  215. package/src/service/source_extraction.ts +0 -225
  216. package/src/service/theme_store.ts +0 -199
  217. package/src/storage/DatabaseInterface.ts +0 -198
  218. package/src/storage/StorageManager.ts +0 -113
  219. package/src/storage/duckdb/ConnectionRepository.ts +0 -143
  220. package/src/storage/duckdb/DuckDBConnection.ts +0 -123
  221. package/src/storage/duckdb/DuckDBRepository.ts +0 -208
  222. package/src/storage/duckdb/EnvironmentRepository.ts +0 -148
  223. package/src/storage/duckdb/MaterializationRepository.spec.ts +0 -191
  224. package/src/storage/duckdb/MaterializationRepository.ts +0 -336
  225. package/src/storage/duckdb/PackageRepository.ts +0 -135
  226. package/src/storage/duckdb/schema.ts +0 -229
  227. package/src/stream_helpers.spec.ts +0 -280
  228. package/src/stream_helpers.ts +0 -162
  229. package/src/telemetry.ts +0 -20
  230. package/src/test_helpers/metrics_harness.ts +0 -166
  231. package/src/theme_key_parity.spec.ts +0 -57
  232. package/src/utils.ts +0 -29
  233. package/tests/fixtures/authorize-compile/model.malloy +0 -9
  234. package/tests/fixtures/authorize-compile/publisher.json +0 -4
  235. package/tests/fixtures/html-pages-nopublic/model.malloy +0 -1
  236. package/tests/fixtures/html-pages-nopublic/publisher.json +0 -5
  237. package/tests/fixtures/html-pages-test/data.csv +0 -3
  238. package/tests/fixtures/html-pages-test/public/assets/app.css +0 -3
  239. package/tests/fixtures/html-pages-test/public/barehtml.html +0 -4
  240. package/tests/fixtures/html-pages-test/public/bodymeta.html +0 -8
  241. package/tests/fixtures/html-pages-test/public/data.json +0 -1
  242. package/tests/fixtures/html-pages-test/public/fitcomment.html +0 -11
  243. package/tests/fixtures/html-pages-test/public/index.html +0 -9
  244. package/tests/fixtures/html-pages-test/public/nohead.html +0 -8
  245. package/tests/fixtures/html-pages-test/public/notfit.html +0 -13
  246. package/tests/fixtures/html-pages-test/public/slides.html +0 -12
  247. package/tests/fixtures/html-pages-test/public/sub/page2.html +0 -9
  248. package/tests/fixtures/html-pages-test/public/unterminated.html +0 -10
  249. package/tests/fixtures/html-pages-test/publisher.json +0 -5
  250. package/tests/fixtures/html-pages-test/report.malloy +0 -1
  251. package/tests/fixtures/persist-multi-level/data/orders.csv +0 -5
  252. package/tests/fixtures/persist-multi-level/multi_level.malloy +0 -18
  253. package/tests/fixtures/persist-multi-level/publisher.json +0 -5
  254. package/tests/fixtures/persist-schedule-test/data/orders.csv +0 -5
  255. package/tests/fixtures/persist-schedule-test/persist_schedule_test.malloy +0 -11
  256. package/tests/fixtures/persist-schedule-test/publisher.json +0 -7
  257. package/tests/fixtures/persist-schedule-warn-test/data/orders.csv +0 -5
  258. package/tests/fixtures/persist-schedule-warn-test/persist_warn_test.malloy +0 -11
  259. package/tests/fixtures/persist-schedule-warn-test/publisher.json +0 -6
  260. package/tests/fixtures/persist-test/data/orders.csv +0 -5
  261. package/tests/fixtures/persist-test/persist_test.malloy +0 -11
  262. package/tests/fixtures/persist-test/publisher.json +0 -5
  263. package/tests/fixtures/publisher.config.json +0 -15
  264. package/tests/fixtures/query-givens/data/orders.csv +0 -7
  265. package/tests/fixtures/query-givens/model.malloy +0 -34
  266. package/tests/fixtures/query-givens/publisher.json +0 -5
  267. package/tests/fixtures/xlsx/database.xlsx +0 -0
  268. package/tests/harness/mcp_test_setup.ts +0 -332
  269. package/tests/harness/rest_e2e.ts +0 -68
  270. package/tests/integration/authorize/compile_authorize_http.integration.spec.ts +0 -92
  271. package/tests/integration/concurrent_package/concurrent_package.integration.spec.ts +0 -280
  272. package/tests/integration/duckdb_storage/duckdb_storage.integration.spec.ts +0 -138
  273. package/tests/integration/first_boot/readiness_line.integration.spec.ts +0 -177
  274. package/tests/integration/html_pages/html_pages.integration.spec.ts +0 -440
  275. package/tests/integration/legacy_routes/legacy_routes.integration.spec.ts +0 -259
  276. package/tests/integration/materialization/environment_list.integration.spec.ts +0 -133
  277. package/tests/integration/materialization/freshness_gate.integration.spec.ts +0 -292
  278. package/tests/integration/materialization/manifest_binding.integration.spec.ts +0 -430
  279. package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +0 -441
  280. package/tests/integration/materialization/orchestrated_rebind.integration.spec.ts +0 -150
  281. package/tests/integration/materialization/reference_manifest.integration.spec.ts +0 -251
  282. package/tests/integration/materialization/schedule_edit.integration.spec.ts +0 -126
  283. package/tests/integration/materialization/scheduler.integration.spec.ts +0 -200
  284. package/tests/integration/materialization/scheduler_recovery.integration.spec.ts +0 -172
  285. package/tests/integration/materialization/scheduler_transitions.integration.spec.ts +0 -256
  286. package/tests/integration/materialization/scheduler_wiring.integration.spec.ts +0 -143
  287. package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +0 -438
  288. package/tests/integration/mcp/mcp_get_context_semantic.integration.spec.ts +0 -235
  289. package/tests/integration/mcp/mcp_transport.integration.spec.ts +0 -98
  290. package/tests/integration/query_givens/query_givens.integration.spec.ts +0 -146
  291. package/tests/integration/query_givens/query_givens_authorize.integration.spec.ts +0 -121
  292. package/tests/integration/sdk_givens/sdk_givens.integration.spec.ts +0 -110
  293. package/tests/integration/watch-mode/watch_mode.integration.spec.ts +0 -421
  294. package/tests/unit/controller/theme.controller.spec.ts +0 -141
  295. package/tests/unit/duckdb/attached_databases.test.ts +0 -1243
  296. package/tests/unit/duckdb/duckdb_connection.test.ts +0 -181
  297. package/tests/unit/duckdb/legacy_schema_migration.test.ts +0 -197
  298. package/tests/unit/duckdb/repositories.test.ts +0 -208
  299. package/tests/unit/ducklake/ducklake.test.ts +0 -952
  300. package/tests/unit/service/theme_store.spec.ts +0 -139
  301. package/tsconfig.json +0 -32
@@ -1,681 +0,0 @@
1
- import { DuckDBConnection } from "@malloydata/db-duckdb";
2
- import {
3
- FixedConnectionMap,
4
- InMemoryURLReader,
5
- type PersistSource,
6
- Runtime,
7
- } from "@malloydata/malloy";
8
- import { mkdtempSync, rmSync } from "node:fs";
9
- import os from "node:os";
10
- import path from "node:path";
11
- import type { components } from "../api";
12
- import { BadRequestError, MaterializationEligibilityError } from "../errors";
13
- import { logger } from "../logger";
14
- import { errMessage } from "../utils";
15
- import { quoteIdentifier, quoteManifestTablePath } from "./quoting";
16
- import { projectToPublicColumns } from "./build_plan";
17
- import {
18
- attachDuckLakeReadWrite,
19
- escapeSQL,
20
- federateSourceForPassthrough,
21
- type FederatedSourceType,
22
- } from "./connection";
23
- import {
24
- assertServesInDuckDB,
25
- type ServeBinding,
26
- } from "./materialization_serve_transform";
27
-
28
- type ApiConnection = components["schemas"]["Connection"];
29
-
30
- /**
31
- * The process-wide session the build-time servability gate compiles against.
32
- * Created on first use and deliberately never disposed: it holds no attach and
33
- * no credentials, and recreating it per build was the single largest source of
34
- * per-build memory growth.
35
- */
36
- let sharedGateSession: DuckDBConnection | undefined;
37
- type WireColumn = components["schemas"]["Column"];
38
-
39
- /** Source warehouse types the native query-passthrough build supports. */
40
- const PASSTHROUGH_SOURCE_TYPES: readonly FederatedSourceType[] = [
41
- "bigquery",
42
- "snowflake",
43
- "postgres",
44
- ];
45
-
46
- /** Destination (storage) connection types the build can materialize INTO. */
47
- const STORAGE_DESTINATION_TYPES = ["ducklake", "duckdb"] as const;
48
-
49
- /**
50
- * Wrap a source-dialect SELECT as a native per-engine query-passthrough call so
51
- * a build-scoped DuckDB session can execute it against the source warehouse and
52
- * stream back only result-sized rows. The compiled SELECT is embedded verbatim
53
- * as a SQL string literal (single-quote-escaped) — the warehouse computes; the
54
- * base tables never enter DuckDB. `handle` is the per-engine reference
55
- * {@link federateSourceForPassthrough} established on the session (a projectId,
56
- * a secret name, or an ATTACH alias). Argument ORDER differs per engine, which
57
- * is the whole reason this is centralized.
58
- */
59
- export function wrapPassthrough(
60
- sourceType: FederatedSourceType,
61
- handle: string,
62
- innerSQL: string,
63
- ): string {
64
- const sql = `'${escapeSQL(innerSQL)}'`;
65
- const h = `'${escapeSQL(handle)}'`;
66
- switch (sourceType) {
67
- case "bigquery":
68
- // bigquery_query(<project/db>, <sql>) — secret resolved by bq:// scope.
69
- return `SELECT * FROM bigquery_query(${h}, ${sql})`;
70
- case "postgres":
71
- // postgres_query(<attached-db alias>, <sql>).
72
- return `SELECT * FROM postgres_query(${h}, ${sql})`;
73
- case "snowflake":
74
- // snowflake_query(<sql>, <secret name>) — SQL FIRST for this engine.
75
- return `SELECT * FROM snowflake_query(${sql}, ${h})`;
76
- default: {
77
- // Exhaustiveness: a new FederatedSourceType must add a case above.
78
- const exhaustive: never = sourceType;
79
- throw new BadRequestError(
80
- `Unsupported passthrough source type: ${String(exhaustive)}`,
81
- );
82
- }
83
- }
84
- }
85
-
86
- /** Narrow a connection's declared type to a supported passthrough source type. */
87
- export function passthroughSourceType(
88
- sourceConnection: ApiConnection,
89
- ): FederatedSourceType {
90
- const type = sourceConnection.type;
91
- if ((PASSTHROUGH_SOURCE_TYPES as readonly string[]).includes(type ?? "")) {
92
- return type as FederatedSourceType;
93
- }
94
- throw new BadRequestError(
95
- `Cannot materialize a '${type}' source into a storage destination: the ` +
96
- `native query-passthrough build supports source connections of type ` +
97
- `${PASSTHROUGH_SOURCE_TYPES.join(", ")} only.`,
98
- );
99
- }
100
-
101
- /**
102
- * A build-scoped DuckDB session on its OWN in-memory instance, plus a disposer
103
- * that closes it and removes the throwaway working directory that pins its
104
- * instance identity.
105
- *
106
- * Isolation is load-bearing for multi-tenant safety. `@malloydata/db-duckdb`
107
- * pools DuckDB instances in a process-global cache keyed by a "share key" that
108
- * deliberately EXCLUDES the connection name, and it never gives a `:memory:`
109
- * primary a private instance — so build sessions built with identical default
110
- * config get pooled onto ONE shared in-memory instance whenever their lifetimes
111
- * overlap (notably with the long-lived serve connection). Pooled together, they
112
- * collide on their transient ATTACH aliases: two builds (or two environments
113
- * with same-named-but-different-credential source/destination connections) both
114
- * `ATTACH … AS orders_pg` / `AS lake` on the shared instance and one clobbers
115
- * the other — a cross-tenant read (source) or WRITE (read-write destination).
116
- * DuckDB itself isolates SEPARATE instances cleanly; the collision is purely an
117
- * artifact of the shared pool.
118
- *
119
- * Fix: give each session a UNIQUE `workingDirectory`, which is part of the share
120
- * key, so it lands its OWN in-memory instance — its own catalog, secrets, and
121
- * attaches, torn down on close. Crucially `databasePath` stays exactly
122
- * `:memory:` (NOT a temp file): a `:memory:` primary lets a DuckLake attach
123
- * auto-initialize a fresh catalog, whereas a file primary does not. The working
124
- * directory is only there to make the share key unique — nothing is written to
125
- * it (every real path the build uses is absolute), so it stays empty and is
126
- * removed on dispose (best-effort; a leftover empty dir is benign).
127
- */
128
- export function createIsolatedBuildSession(sessionName: string): {
129
- session: DuckDBConnection;
130
- dispose: () => Promise<void>;
131
- } {
132
- const workDir = mkdtempSync(path.join(os.tmpdir(), "malloy-build-"));
133
- // The disposer that owns removing workDir does not exist until this function
134
- // returns, so a throw from the constructor would strand the directory with
135
- // nothing left holding a reference to it.
136
- let session: DuckDBConnection;
137
- try {
138
- session = new DuckDBConnection(sessionName, ":memory:", workDir);
139
- } catch (err) {
140
- rmSync(workDir, { recursive: true, force: true });
141
- throw err;
142
- }
143
- const dispose = async () => {
144
- // `close()` alone does not end the session. It refcount-decrements the
145
- // DuckDBInstance, but the node-api Connection holds a C++ ClientContext that
146
- // keeps the refcount above zero — so the in-memory database survives, and with
147
- // it the destination's ATTACH and the source's federated credentials. Measured:
148
- // 2 idle Postgres backends stranded per build, freed only at process exit,
149
- // where the whole point of a per-build session is that they exist only for
150
- // the build.
151
- //
152
- // db-duckdb cannot do this itself: 0.0.389 disconnected unconditionally and
153
- // had to be reverted (PR #2793) because malloy's translate layer holds
154
- // weak_ptrs to the C++ Connection and the language server segfaulted. A build
155
- // session is different in the way that matters — server-owned, single-use, and
156
- // nothing outside this function ever sees it — so it can release what a
157
- // general-purpose connection cannot. Reached through a cast because the handle
158
- // is library-internal; if db-duckdb ever grows an explicit hard-close, use it.
159
- const nodeConnection = (
160
- session as unknown as { connection?: { disconnectSync?: () => void } }
161
- ).connection;
162
- try {
163
- await session.close();
164
- } catch (err) {
165
- logger.warn("Failed to close build session (leaked session)", {
166
- sessionName,
167
- error: err instanceof Error ? err.message : String(err),
168
- });
169
- }
170
- try {
171
- nodeConnection?.disconnectSync?.();
172
- } catch (err) {
173
- logger.warn("Failed to disconnect build session (leaked connection)", {
174
- sessionName,
175
- error: err instanceof Error ? err.message : String(err),
176
- });
177
- }
178
- try {
179
- rmSync(workDir, { recursive: true, force: true });
180
- } catch (err) {
181
- logger.warn("Failed to remove build session working directory", {
182
- sessionName,
183
- workDir,
184
- error: err instanceof Error ? err.message : String(err),
185
- });
186
- }
187
- };
188
- return { session, dispose };
189
- }
190
-
191
- /** Result of building one source into a storage destination. */
192
- export interface StorageBuildResult {
193
- /** The connection the physical table now lives in (the destination). */
194
- storageConnectionName: string;
195
- /** Authoritative DuckDB column schema, captured post-build via DESCRIBE. */
196
- schema: WireColumn[];
197
- }
198
-
199
- /**
200
- * Build one persist source into a `storage=` destination via native
201
- * query-passthrough CTAS, on a *build-scoped* DuckDB session that is created,
202
- * used, and disposed here — so the read-write destination attach and the source
203
- * credential federation exist ONLY for the build's duration and NEVER on the
204
- * serve connection (the least-privilege boundary the design requires).
205
- *
206
- * Steps: create a private build session → attach the destination read-write
207
- * → federate the source's credentials on demand → `CREATE OR REPLACE TABLE`
208
- * (qualified with the destination catalog) whose FROM is the source passthrough
209
- * → capture the built table's authoritative schema (DESCRIBE) → dispose the
210
- * session (releasing every credential/attach). DuckLake's catalog swap is
211
- * transactional, so the replace is atomic. The compiled SELECT (source dialect)
212
- * is unchanged; only where it executes and where the result lands move.
213
- *
214
- * @returns the destination connection name and the captured authoritative
215
- * schema, both recorded on the manifest entry for the serve transform.
216
- */
217
- export async function buildSourceIntoStorage(params: {
218
- destinationName: string;
219
- destinationConnection: ApiConnection;
220
- sourceConnection: ApiConnection;
221
- /** The source-dialect compiled SELECT (v0's getSQL output), verbatim. */
222
- buildSQL: string;
223
- /** Logical, unquoted physical table path (may carry a container path). */
224
- physicalTableName: string;
225
- environmentPath: string;
226
- }): Promise<StorageBuildResult> {
227
- const {
228
- destinationName,
229
- destinationConnection,
230
- sourceConnection,
231
- buildSQL,
232
- physicalTableName,
233
- environmentPath,
234
- } = params;
235
-
236
- assertSupportedDestination(destinationName, destinationConnection);
237
- const sourceType = passthroughSourceType(sourceConnection);
238
-
239
- // A dedicated, disposable build session on its OWN in-memory instance, so its
240
- // read-write destination attach and federated source credentials cannot be
241
- // pooled onto — or collide with — any other build/serve connection (see
242
- // createIsolatedBuildSession).
243
- const { session, dispose } = createIsolatedBuildSession(
244
- `build_${destinationName}`,
245
- );
246
- try {
247
- await attachDestinationReadWrite(
248
- session,
249
- destinationName,
250
- destinationConnection,
251
- environmentPath,
252
- );
253
-
254
- if (destinationConnection.type === "ducklake") {
255
- // Write-path: disable DuckLake row inlining for this build session so
256
- // materialized rows land as Parquet in the data store, NOT inlined into
257
- // the catalog database. Materializations are generally large, and in a
258
- // shared/multitenant catalog inlining would push tenant data into the
259
- // catalog Postgres. Session-scoped (this build only) — it does not
260
- // mutate the shared catalog's persisted options.
261
- await session.runSQL("SET ducklake_default_data_inlining_row_limit=0");
262
- }
263
-
264
- const federated = await federateSourceForPassthrough(
265
- session,
266
- sourceType,
267
- sourceFederationConfig(sourceConnection),
268
- );
269
-
270
- // The CTAS target MUST be qualified with the destination catalog (the
271
- // attach alias) — an unqualified name lands in the build session's own
272
- // (private, throwaway) in-memory catalog, not the DuckLake/DuckDB store,
273
- // so the rows would be captured for the schema and then vanish with the
274
- // session. DuckLake's
275
- // catalog swap is transactional, so CREATE OR REPLACE is atomic — no
276
- // staging/rename dance needed on this path.
277
- // quoteManifestTablePath, NOT quoteTablePath: the serve side renders this same
278
- // path with the manifest rule, which passes an already-quoted name through
279
- // unchanged. Quoting unconditionally here would create a table whose NAME
280
- // contains the author's quote characters while the serve side asks for the
281
- // unquoted-inner form — and because that mismatch surfaces at shape RUN time,
282
- // past the compile-time live fallback, every query on the source 400s. For an
283
- // unquoted name the two functions are identical, so nothing else moves.
284
- const target = quoteManifestTablePath(
285
- `${destinationName}.${physicalTableName}`,
286
- "duckdb",
287
- );
288
- const passthrough = wrapPassthrough(
289
- sourceType,
290
- federated.handle,
291
- buildSQL,
292
- );
293
-
294
- // Capture the authoritative schema from the freshly-built table — the
295
- // serve transform declares exactly this, and the compiler does not
296
- // type-check a virtual source's declared columns.
297
- const schema = await createTableAndDescribe(session, target, passthrough);
298
-
299
- return { storageConnectionName: destinationName, schema };
300
- } finally {
301
- // Dispose closes the private instance (releasing every secret + attach —
302
- // nothing federated or read-write survives the build) and removes its
303
- // throwaway working directory.
304
- await dispose();
305
- }
306
- }
307
-
308
- /**
309
- * Build a CHAINED `storage=` source by reading its already-materialized
310
- * upstream(s) from the destination store — the "stack on the parent" path —
311
- * instead of recomputing the upstream from raw against the source
312
- * warehouse. Reuses the parent's work and is consistent-by-construction
313
- * (the downstream is a pure function of the parent's STORED rows).
314
- *
315
- * The `transientModel` (assembled by the caller from the serve-shape rebind
316
- * machinery, see {@link buildChainedStorageBuildModel}) rebinds every upstream to
317
- * a virtual source on THIS destination and re-declares the downstream as a
318
- * persist source over them. We compile it against the build session (the only
319
- * connection it references is the attached destination), read the downstream
320
- * persist source's `getSQL({ virtualMap })` — the exact mirror of the warehouse
321
- * build's `getSQL`, only the SQL now reads the attached lake tables — and CTAS
322
- * the result into the destination. No source federation, no passthrough: nothing
323
- * leaves DuckDB. The `virtualMap` maps each upstream's handle to its (quoted)
324
- * physical lake path.
325
- *
326
- * Session lifecycle mirrors {@link buildSourceIntoStorage} exactly (create →
327
- * attach read-write → build → dispose), so no read-write attach survives the
328
- * build, and the same authoritative-schema capture applies.
329
- *
330
- * @returns the destination connection name and the captured authoritative schema.
331
- */
332
- export async function buildDownstreamIntoStorage(params: {
333
- destinationName: string;
334
- destinationConnection: ApiConnection;
335
- /** Transient rebind model: upstream virtuals + persist-annotated downstream. */
336
- transientModel: string;
337
- /** The downstream source's Malloy name, to locate it in the transient plan. */
338
- downstreamName: string;
339
- /** connectionName → handle → quoted physical path for the upstream virtuals. */
340
- virtualMap: Map<string, Map<string, string>>;
341
- /** Logical, unquoted physical table path for the downstream's own table. */
342
- physicalTableName: string;
343
- environmentPath: string;
344
- }): Promise<StorageBuildResult> {
345
- const {
346
- destinationName,
347
- destinationConnection,
348
- transientModel,
349
- downstreamName,
350
- virtualMap,
351
- physicalTableName,
352
- environmentPath,
353
- } = params;
354
-
355
- assertSupportedDestination(destinationName, destinationConnection);
356
-
357
- const { session, dispose } = createIsolatedBuildSession(
358
- `build_${destinationName}`,
359
- );
360
- try {
361
- await attachDestinationReadWrite(
362
- session,
363
- destinationName,
364
- destinationConnection,
365
- environmentPath,
366
- );
367
- if (destinationConnection.type === "ducklake") {
368
- // Same rationale as buildSourceIntoStorage: keep materialized rows out
369
- // of the shared catalog database.
370
- await session.runSQL("SET ducklake_default_data_inlining_row_limit=0");
371
- }
372
-
373
- // Compile the transient rebind model against the build session. Its only
374
- // connection is the attached destination; the upstream virtual sources
375
- // declare their captured schema, so compiling reads no tables (schema-on-
376
- // faith) — the CTAS below runs the real SQL against the attached lake.
377
- const root = "file:///chained-build/";
378
- const url = `${root}m.malloy`;
379
- const runtime = new Runtime({
380
- urlReader: new InMemoryURLReader(new Map([[url, transientModel]])),
381
- connections: new FixedConnectionMap(
382
- new Map([[destinationName, session]]),
383
- destinationName,
384
- ),
385
- });
386
- // Compiling the transient model is the only SHAPE step in this function:
387
- // a failure means the downstream cannot be expressed over its rebound
388
- // parents, which is a legitimate reason to fall back and recompute from
389
- // raw. Everything around it — the attach, the CTAS, the DESCRIBE — is
390
- // infrastructure, where falling back would just fail the same way against
391
- // the same destination. Marking the shape failures lets the caller tell
392
- // them apart.
393
- let model;
394
- try {
395
- model = await runtime
396
- .loadModel(new URL(url), { importBaseURL: new URL(root) })
397
- .getModel();
398
- } catch (err) {
399
- throw new MaterializationEligibilityError({
400
- message: `Chained build model did not compile over the rebound parents: ${errMessage(err)}`,
401
- });
402
- }
403
- const plan = model.getBuildPlan();
404
- let downstream: PersistSource | undefined;
405
- for (const ps of Object.values(plan.sources)) {
406
- if (ps.name === downstreamName) {
407
- downstream = ps;
408
- break;
409
- }
410
- }
411
- if (!downstream) {
412
- // The downstream didn't survive as a persist source — its definition
413
- // references something the rebind model doesn't provide (a parent
414
- // refinement not carried, a live leaf). The caller falls back.
415
- throw new MaterializationEligibilityError({
416
- message:
417
- `Chained build model did not yield a persist source named ` +
418
- `'${downstreamName}' (the downstream references something the ` +
419
- `rebound parents don't provide).`,
420
- });
421
- }
422
- // The downstream's materialization SQL, over the rebound parents — DuckDB
423
- // dialect, reading the attached lake tables via the virtualMap. Project to
424
- // the public columns so a hidden (`except:` / access-restricted) downstream
425
- // column is not materialized — same rationale as the single-source build:
426
- // keeping hidden values out of the store at rest.
427
- const sql = projectToPublicColumns(
428
- downstream,
429
- downstream.getSQL({ virtualMap }),
430
- );
431
-
432
- // Same write/read mirror as the single-source build above.
433
- const target = quoteManifestTablePath(
434
- `${destinationName}.${physicalTableName}`,
435
- "duckdb",
436
- );
437
- const schema = await createTableAndDescribe(session, target, sql);
438
-
439
- return { storageConnectionName: destinationName, schema };
440
- } finally {
441
- await dispose();
442
- }
443
- }
444
-
445
- /**
446
- * Build-time servability gate (the portable-DuckDB eligibility check, deferred
447
- * from the pre-build pass because it needs the POST-build schema): compile the
448
- * source's serve-shape in DuckDB against the captured schema and REFUSE the
449
- * build if it does not compile. The served table lives in DuckDB, so a source
450
- * authored against a warehouse must have a DuckDB-portable served shape — this
451
- * turns a serve-time execution error into a build-time refusal (HTTP 422),
452
- * running as part of stage→validate, the same step that captured the schema.
453
- *
454
- * The compile is schema-on-faith (the compiler does not type-check a virtual
455
- * source's columns and no SQL runs), so a throwaway in-memory DuckDB connection
456
- * suffices — no attach, no table read, nothing federated.
457
- *
458
- * @throws {MaterializationEligibilityError} (HTTP 422) if the shape can't compile.
459
- */
460
- export async function assertStorageServeShapeCompiles(params: {
461
- destinationName: string;
462
- sourceName: string;
463
- virtualHandle: string;
464
- physicalTableName: string;
465
- schema: WireColumn[];
466
- }): Promise<void> {
467
- const { destinationName, sourceName, virtualHandle, physicalTableName } =
468
- params;
469
- const binding: ServeBinding = {
470
- sourceName,
471
- connectionName: destinationName,
472
- virtualHandle,
473
- tablePath: `${destinationName}.${physicalTableName}`,
474
- schema: params.schema
475
- .filter((c) => c.name && c.type)
476
- .map((c) => ({ name: c.name as string, type: c.type as string })),
477
- };
478
- // ONE session for the process, not one per build. The build and GC sessions
479
- // need their own instance because they ATTACH a destination read-write and
480
- // federate customer credentials; this gate does neither — it compiles a
481
- // throwaway serve shape against a captured schema — so it has no cross-tenant
482
- // collision surface to isolate. A fresh instance per build cost ~5.9MB of RSS
483
- // per build on the production image, roughly three quarters of the build
484
- // path's total growth, and it is never reclaimed.
485
- //
486
- // The risk of sharing is a shared CATALOG: every compile declares a virtual
487
- // source into it, so a later shape could in principle pass on declarations
488
- // left by an earlier one. Pinned in the spec — refusals still refuse after 25
489
- // successful compiles, a refusal does not poison the session, and the same
490
- // handle recompiled with a different schema sees the new one.
491
- sharedGateSession ??= createIsolatedBuildSession("gate_shared").session;
492
- await assertServesInDuckDB(
493
- sourceName,
494
- binding,
495
- new FixedConnectionMap(
496
- new Map([[destinationName, sharedGateSession]]),
497
- destinationName,
498
- ),
499
- );
500
- }
501
-
502
- /** DDL to drop a storage table by its recorded name, catalog-qualified for DuckDB. */
503
- export function dropStorageTableSql(
504
- destinationName: string,
505
- physicalTableName: string,
506
- ): string {
507
- // Must name the table the way the build CREATEd it (see the CTAS sites).
508
- return `DROP TABLE IF EXISTS ${quoteManifestTablePath(
509
- `${destinationName}.${physicalTableName}`,
510
- "duckdb",
511
- )}`;
512
- }
513
-
514
- /**
515
- * Drop one materialized table from a `storage=` destination, on a build-scoped
516
- * read-write session with its OWN in-memory instance (the SERVE attach is
517
- * read-only, so GC cannot run there; and an isolated instance keeps this GC's
518
- * read-write destination attach from colliding with another tenant's same-named
519
- * destination — see {@link createIsolatedBuildSession}). Mirrors
520
- * {@link buildSourceIntoStorage}'s lifecycle: attach read-write → drop → dispose.
521
- *
522
- * Only the recorded physical table (`physicalTableName`) is dropped — a
523
- * destination-aware drop of a name the publisher recorded building, never a
524
- * catalog scan — so GC can never take out a table it did not create.
525
- */
526
- export async function dropStorageTable(params: {
527
- destinationName: string;
528
- destinationConnection: ApiConnection;
529
- physicalTableName: string;
530
- environmentPath: string;
531
- }): Promise<void> {
532
- const {
533
- destinationName,
534
- destinationConnection,
535
- physicalTableName,
536
- environmentPath,
537
- } = params;
538
- // Fail fast (pre-session, pre-attach) on a destination the build can't target.
539
- assertSupportedDestination(destinationName, destinationConnection);
540
-
541
- const { session, dispose } = createIsolatedBuildSession(
542
- `gc_${destinationName}`,
543
- );
544
- try {
545
- await attachDestinationReadWrite(
546
- session,
547
- destinationName,
548
- destinationConnection,
549
- environmentPath,
550
- );
551
- await session.runSQL(
552
- dropStorageTableSql(destinationName, physicalTableName),
553
- );
554
- } finally {
555
- await dispose();
556
- }
557
- }
558
-
559
- /** Attach the destination connection read-write on the build session. */
560
- async function attachDestinationReadWrite(
561
- session: DuckDBConnection,
562
- destinationName: string,
563
- destinationConnection: ApiConnection,
564
- environmentPath: string,
565
- ): Promise<void> {
566
- if (destinationConnection.type === "ducklake") {
567
- const cfg = destinationConnection.ducklakeConnection;
568
- if (!cfg) {
569
- throw new BadRequestError(
570
- `Storage destination '${destinationName}' is type 'ducklake' but ` +
571
- `has no ducklakeConnection config.`,
572
- );
573
- }
574
- await attachDuckLakeReadWrite(session, destinationName, cfg);
575
- return;
576
- }
577
- // Plain DuckDB destination: attach its database file read-write. The file
578
- // path is derived the same way connection assembly derives it.
579
- const dbPath = path.join(environmentPath, `${destinationName}.duckdb`);
580
- await session.runSQL(
581
- `ATTACH '${escapeSQL(dbPath)}' AS ${quoteIdentifier(destinationName, "duckdb")}`,
582
- );
583
- }
584
-
585
- /** Throw a clean 422 unless the destination is a supported storage type. */
586
- function assertSupportedDestination(
587
- destinationName: string,
588
- destinationConnection: ApiConnection,
589
- ): void {
590
- const type = destinationConnection.type ?? "";
591
- if (!(STORAGE_DESTINATION_TYPES as readonly string[]).includes(type)) {
592
- throw new BadRequestError(
593
- `Storage destination '${destinationName}' is type '${type}', but ` +
594
- `'storage=' destinations must be one of ` +
595
- `${STORAGE_DESTINATION_TYPES.join(", ")}.`,
596
- );
597
- }
598
- }
599
-
600
- /**
601
- * The subset of a source connection's config the passthrough federation needs,
602
- * plus a sanitized `name` base for the secret/alias it creates on the session.
603
- */
604
- function sourceFederationConfig(sourceConnection: ApiConnection): {
605
- name: string;
606
- bigqueryConnection?: components["schemas"]["BigqueryConnection"];
607
- snowflakeConnection?: components["schemas"]["SnowflakeConnection"];
608
- postgresConnection?: components["schemas"]["PostgresConnection"];
609
- } {
610
- return {
611
- name: sourceConnection.name ?? "src",
612
- bigqueryConnection: sourceConnection.bigqueryConnection,
613
- snowflakeConnection: sourceConnection.snowflakeConnection,
614
- postgresConnection: sourceConnection.postgresConnection,
615
- };
616
- }
617
-
618
- /**
619
- * Read the authoritative column schema of a just-built table with `DESCRIBE`,
620
- * mapping DuckDB's `column_name`/`column_type` rows to the wire {@link Column}
621
- * shape the manifest carries. This is the schema the serve transform declares.
622
- */
623
- /**
624
- * CTAS the table, then read back its authoritative schema — dropping the table
625
- * again if that read-back fails.
626
- *
627
- * The window this closes: the CTAS has committed by the time DESCRIBE runs, and
628
- * the caller records nothing until this function RETURNS. So a DESCRIBE failure
629
- * propagates past a committed table that no manifest entry names and that
630
- * `builtThisRun` never saw — leaving it unreachable by the failed-run reclaim
631
- * and by manifest-driven GC alike, which only drop names they recorded building.
632
- * Same class the post-build serve-shape gate already closes with its own
633
- * targeted drop (see materialization_service), and closed the same way.
634
- *
635
- * Best-effort: a failed drop is logged, never raised, and never replaces the
636
- * DESCRIBE error that is the actual failure. The drop runs on the session that
637
- * created the table, whose read-write attach is still open.
638
- */
639
- export async function createTableAndDescribe(
640
- session: DuckDBConnection,
641
- quotedTablePath: string,
642
- selectSQL: string,
643
- ): Promise<WireColumn[]> {
644
- await session.runSQL(
645
- `CREATE OR REPLACE TABLE ${quotedTablePath} AS (${selectSQL})`,
646
- );
647
- try {
648
- return await describeTable(session, quotedTablePath);
649
- } catch (describeErr) {
650
- try {
651
- await session.runSQL(`DROP TABLE IF EXISTS ${quotedTablePath}`);
652
- } catch (dropErr) {
653
- logger.warn(
654
- "Failed to drop a storage table stranded by a schema read-back " +
655
- "failure (physical leak)",
656
- {
657
- table: quotedTablePath,
658
- error: errMessage(dropErr),
659
- },
660
- );
661
- }
662
- throw describeErr;
663
- }
664
- }
665
-
666
- async function describeTable(
667
- session: DuckDBConnection,
668
- quotedTablePath: string,
669
- ): Promise<WireColumn[]> {
670
- const result = await session.runSQL(`DESCRIBE ${quotedTablePath}`);
671
- const rows = Array.isArray(result) ? result : (result.rows ?? []);
672
- const columns: WireColumn[] = [];
673
- for (const row of rows as Record<string, unknown>[]) {
674
- const name = row.column_name;
675
- const type = row.column_type;
676
- if (typeof name === "string" && typeof type === "string") {
677
- columns.push({ name, type });
678
- }
679
- }
680
- return columns;
681
- }