@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,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
- }