@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,209 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
-
3
- import { PayloadTooLargeError } from "../errors";
4
- import {
5
- assertWithinModelResponseLimits,
6
- queryRowLimitSource,
7
- resolveModelQueryRowLimit,
8
- } from "./model_limits";
9
-
10
- describe("resolveModelQueryRowLimit", () => {
11
- it("uses the user's LIMIT when set and below the maxRows ceiling", () => {
12
- expect(
13
- resolveModelQueryRowLimit(500, {
14
- defaultLimit: 1000,
15
- maxRows: 100_000,
16
- }),
17
- ).toBe(500);
18
- });
19
-
20
- it("falls back to defaultLimit when the user's LIMIT is undefined", () => {
21
- expect(
22
- resolveModelQueryRowLimit(undefined, {
23
- defaultLimit: 1000,
24
- maxRows: 100_000,
25
- }),
26
- ).toBe(1000);
27
- });
28
-
29
- it("falls back to defaultLimit when the user's LIMIT is 0 (Malloy returns 0 for 'no limit')", () => {
30
- // Malloy's PreparedResult returns 0 from `resultExplore.limit` when the
31
- // query has no LIMIT clause; treat that as "no user limit".
32
- expect(
33
- resolveModelQueryRowLimit(0, {
34
- defaultLimit: 1000,
35
- maxRows: 100_000,
36
- }),
37
- ).toBe(1000);
38
- });
39
-
40
- it("clamps a too-high user LIMIT to the maxRows + 1 sentinel", () => {
41
- expect(
42
- resolveModelQueryRowLimit(1_000_000, {
43
- defaultLimit: 1000,
44
- maxRows: 100_000,
45
- }),
46
- ).toBe(100_001);
47
- });
48
-
49
- it("clamps a too-high defaultLimit to the maxRows + 1 sentinel", () => {
50
- // Operator misconfigured DEFAULT > MAX; the hard cap still wins.
51
- expect(
52
- resolveModelQueryRowLimit(undefined, {
53
- defaultLimit: 1_000_000,
54
- maxRows: 50,
55
- }),
56
- ).toBe(51);
57
- });
58
-
59
- it("returns the requested limit unchanged when maxRows is 0 (cap disabled)", () => {
60
- expect(
61
- resolveModelQueryRowLimit(1_000_000, {
62
- defaultLimit: 1000,
63
- maxRows: 0,
64
- }),
65
- ).toBe(1_000_000);
66
- });
67
-
68
- it("returns the default unchanged when maxRows is 0 and no user limit", () => {
69
- expect(
70
- resolveModelQueryRowLimit(undefined, {
71
- defaultLimit: 1000,
72
- maxRows: 0,
73
- }),
74
- ).toBe(1000);
75
- });
76
-
77
- it("rejects negative user limits by treating them as 'no limit'", () => {
78
- // Defensive — shouldn't happen in practice, but a -1 from a malformed
79
- // PreparedResult shouldn't propagate as a negative rowLimit to the driver.
80
- expect(
81
- resolveModelQueryRowLimit(-1, {
82
- defaultLimit: 1000,
83
- maxRows: 100_000,
84
- }),
85
- ).toBe(1000);
86
- });
87
- });
88
-
89
- describe("assertWithinModelResponseLimits", () => {
90
- it("does not throw when both counts are below their caps", () => {
91
- expect(() =>
92
- assertWithinModelResponseLimits(
93
- 500,
94
- 1_000,
95
- { maxRows: 1000, maxBytes: 10_000 },
96
- "model_query",
97
- ),
98
- ).not.toThrow();
99
- });
100
-
101
- it("does not throw when row count equals the cap exactly (sentinel hasn't fired)", () => {
102
- expect(() =>
103
- assertWithinModelResponseLimits(
104
- 1000,
105
- 1_000,
106
- { maxRows: 1000, maxBytes: 10_000 },
107
- "model_query",
108
- ),
109
- ).not.toThrow();
110
- });
111
-
112
- it("throws PayloadTooLargeError with the row-cap message on row overflow", () => {
113
- expect(() =>
114
- assertWithinModelResponseLimits(
115
- 1001,
116
- 1_000,
117
- { maxRows: 1000, maxBytes: 10_000 },
118
- "model_query",
119
- ),
120
- ).toThrow(PayloadTooLargeError);
121
- expect(() =>
122
- assertWithinModelResponseLimits(
123
- 1001,
124
- 1_000,
125
- { maxRows: 1000, maxBytes: 10_000 },
126
- "model_query",
127
- ),
128
- ).toThrow("more than 1000 rows");
129
- });
130
-
131
- it("throws PayloadTooLargeError with the byte-cap message on byte overflow", () => {
132
- expect(() =>
133
- assertWithinModelResponseLimits(
134
- 10,
135
- 50_000,
136
- { maxRows: 1000, maxBytes: 10_000 },
137
- "model_query",
138
- ),
139
- ).toThrow(PayloadTooLargeError);
140
- expect(() =>
141
- assertWithinModelResponseLimits(
142
- 10,
143
- 50_000,
144
- { maxRows: 1000, maxBytes: 10_000 },
145
- "model_query",
146
- ),
147
- ).toThrow("exceeded 10000 bytes");
148
- });
149
-
150
- it("prefers the row-cap message when both caps would have fired (row check runs first)", () => {
151
- expect(() =>
152
- assertWithinModelResponseLimits(
153
- 2000,
154
- 50_000,
155
- { maxRows: 1000, maxBytes: 10_000 },
156
- "model_query",
157
- ),
158
- ).toThrow("more than 1000 rows");
159
- });
160
-
161
- it("disables row cap when maxRows is 0", () => {
162
- expect(() =>
163
- assertWithinModelResponseLimits(
164
- 1_000_000,
165
- 1_000,
166
- { maxRows: 0, maxBytes: 10_000 },
167
- "model_query",
168
- ),
169
- ).not.toThrow();
170
- });
171
-
172
- it("disables byte cap when maxBytes is 0", () => {
173
- expect(() =>
174
- assertWithinModelResponseLimits(
175
- 10,
176
- 1_000_000_000,
177
- { maxRows: 1000, maxBytes: 0 },
178
- "model_query",
179
- ),
180
- ).not.toThrow();
181
- });
182
- });
183
-
184
- describe("queryRowLimitSource", () => {
185
- /**
186
- * Must mirror resolveModelQueryRowLimit's own `requested` condition: if the
187
- * two ever disagree, the reported source describes a different limit than
188
- * the one actually pushed into the SQL.
189
- */
190
- it("reports the query when it carried its own positive limit", () => {
191
- expect(queryRowLimitSource(10)).toBe("query");
192
- expect(queryRowLimitSource(1)).toBe("query");
193
- });
194
-
195
- it("reports the server default when the query carried none", () => {
196
- expect(queryRowLimitSource(undefined)).toBe("server_default");
197
- expect(queryRowLimitSource(0)).toBe("server_default");
198
- expect(queryRowLimitSource(-1)).toBe("server_default");
199
- });
200
-
201
- it("agrees with which limit resolveModelQueryRowLimit actually used", () => {
202
- const config = { defaultLimit: 1000, maxRows: 0 };
203
- for (const userLimit of [undefined, 0, -1, 1, 10, 5000]) {
204
- const used = resolveModelQueryRowLimit(userLimit, config);
205
- const cameFromQuery = queryRowLimitSource(userLimit) === "query";
206
- expect(cameFromQuery).toBe(used !== config.defaultLimit);
207
- }
208
- });
209
- });
@@ -1,131 +0,0 @@
1
- /**
2
- * Memory guards for the Malloy model-query path (the `runnable.run`
3
- * flow used by `getQueryResults` and notebook cell execution).
4
- *
5
- * Two layered defenses:
6
- *
7
- * 1. {@link resolveModelQueryRowLimit} — compute the effective
8
- * `rowLimit` to push down to `runnable.run`. The user's Malloy
9
- * `LIMIT` clause wins when present; otherwise the operator-
10
- * tunable default ({@link getDefaultQueryRowLimit}) fills in.
11
- * Either way the result is clamped to `maxRows + 1` so the
12
- * database itself stops producing rows when a user-supplied
13
- * `LIMIT 1_000_000` would otherwise blow up the process.
14
- *
15
- * 2. {@link assertWithinModelResponseLimits} — post-run overflow
16
- * detection. If the connector returned `maxRows + 1` rows
17
- * (the sentinel) or the JSON-serialized response exceeds the
18
- * byte cap, throw `PayloadTooLargeError` so the caller sees a
19
- * clean HTTP 413.
20
- *
21
- * Caveat on the byte cap: this path runs `runnable.run` (buffered),
22
- * not `runStream`, so by the time we measure bytes the result has
23
- * already been materialized in memory. The byte cap here is loud-
24
- * failure detection — it surfaces oversize responses with a 413
25
- * instead of letting the client receive a half-transmitted payload
26
- * — not OOM prevention. True prevention requires streaming +
27
- * `Result` reconstruction from `DataRecord`s, which is out of scope
28
- * for this step (the model-query streaming path entangles with
29
- * Malloy's `Result` schema metadata in non-trivial ways).
30
- *
31
- * Both helpers are pure so they can be unit-tested without spinning
32
- * up a model runtime; the caller injects the env-derived limits.
33
- */
34
-
35
- import { PayloadTooLargeError } from "../errors";
36
- import {
37
- recordQueryCapExceeded,
38
- type QueryCapSource,
39
- } from "../query_cap_metrics";
40
-
41
- export interface ResolveRowLimitConfig {
42
- /**
43
- * Result of {@link getDefaultQueryRowLimit}. Applied when the
44
- * user's Malloy query doesn't carry a `LIMIT` clause.
45
- */
46
- defaultLimit: number;
47
- /**
48
- * Result of {@link getMaxQueryRows}. The effective row limit is
49
- * clamped to `maxRows + 1` so a sentinel-count overflow check can
50
- * distinguish "ran right up to the cap" from "would have
51
- * overflowed". A value of `0` disables the cap.
52
- */
53
- maxRows: number;
54
- }
55
-
56
- /**
57
- * Compute the `rowLimit` to pass to `runnable.run`. The +1 sentinel
58
- * mirrors the Step 1 / Step 2 patterns on the connection-query path
59
- * so behavior is uniform across all query surfaces.
60
- */
61
- export function resolveModelQueryRowLimit(
62
- userLimit: number | undefined,
63
- { defaultLimit, maxRows }: ResolveRowLimitConfig,
64
- ): number {
65
- const requested = userLimit && userLimit > 0 ? userLimit : defaultLimit;
66
- if (maxRows <= 0) return requested;
67
- return Math.min(requested, maxRows + 1);
68
- }
69
-
70
- /** Whether the cap came from the query itself or from the server default. */
71
- export type QueryRowLimitSource = "query" | "server_default";
72
-
73
- /**
74
- * Which of the two the cap in {@link resolveModelQueryRowLimit} came from.
75
- *
76
- * The distinction is the whole difference between "the database cut this off
77
- * and you were not told" and "you asked for exactly this many". A `limit:` or
78
- * `top:` the author wrote is deliberate and complete for what it asked; only the
79
- * silently-applied default means rows were probably left behind.
80
- *
81
- * The condition deliberately mirrors `requested` above and must stay in step
82
- * with it, so a change to which limit wins cannot leave the reported source
83
- * describing the other one.
84
- */
85
- export function queryRowLimitSource(
86
- userLimit: number | undefined,
87
- ): QueryRowLimitSource {
88
- return userLimit && userLimit > 0 ? "query" : "server_default";
89
- }
90
-
91
- export interface ModelResponseLimitsConfig {
92
- /** Result of {@link getMaxQueryRows}. `0` disables the row cap. */
93
- maxRows: number;
94
- /** Result of {@link getMaxResponseBytes}. `0` disables the byte cap. */
95
- maxBytes: number;
96
- }
97
-
98
- /**
99
- * Throw {@link PayloadTooLargeError} (HTTP 413) when a model-query
100
- * response exceeds either configured cap. `rowCount` should be the
101
- * raw row count Malloy actually fetched (typically
102
- * `result._queryResult.data.rawData.length`); `serializedBytes`
103
- * should be the byte length of the JSON-stringified response that
104
- * would otherwise be returned to the client.
105
- *
106
- * Row check uses the `> maxRows` sentinel (not `>= maxRows`), since
107
- * {@link resolveModelQueryRowLimit} asked the connector for
108
- * `maxRows + 1` and we want to fail only when that sentinel fires.
109
- */
110
- export function assertWithinModelResponseLimits(
111
- rowCount: number,
112
- serializedBytes: number,
113
- { maxRows, maxBytes }: ModelResponseLimitsConfig,
114
- source: QueryCapSource,
115
- ): void {
116
- if (maxRows > 0 && rowCount > maxRows) {
117
- // Tick the counter *before* throwing so it reflects the
118
- // event even if a downstream `catch` swallows the error
119
- // (notebook handlers and MCP tools both do this in places).
120
- recordQueryCapExceeded("rows", source);
121
- throw new PayloadTooLargeError(
122
- `Query returned more than ${maxRows} rows. Refine the query (add a LIMIT or more selective WHERE) or raise PUBLISHER_MAX_QUERY_ROWS.`,
123
- );
124
- }
125
- if (maxBytes > 0 && serializedBytes > maxBytes) {
126
- recordQueryCapExceeded("bytes", source);
127
- throw new PayloadTooLargeError(
128
- `Query response exceeded ${maxBytes} bytes (was ${serializedBytes}). Project fewer columns, add a LIMIT, or raise PUBLISHER_MAX_RESPONSE_BYTES.`,
129
- );
130
- }
131
- }
@@ -1,193 +0,0 @@
1
- // End-to-end routing contract for the `storage=` virtual-source serve path,
2
- // exercised through the real Model.getQueryResults against a real in-memory
3
- // DuckDB. The ORIGINAL source returns 0; the serve binding points at a table
4
- // returning 60 — so the value observed proves whether the query was routed
5
- // through the serve-shape transform (60) or served live from the original
6
- // model (0). This pins the mode gate, the binding gate, and the safe fallback.
7
- import { DuckDBConnection } from "@malloydata/db-duckdb";
8
- import {
9
- FixedConnectionMap,
10
- InMemoryURLReader,
11
- modelDefToModelInfo,
12
- Runtime,
13
- } from "@malloydata/malloy";
14
- import { afterEach, describe, expect, it } from "bun:test";
15
- import { Model } from "./model";
16
- import type { ServeBinding } from "./materialization_serve_transform";
17
-
18
- const ROOT = "file:///storage-serve-e2e/";
19
- const QUERY = "run: X -> { aggregate: t is total.sum() }";
20
- const BINDING: ServeBinding = {
21
- sourceName: "X",
22
- connectionName: "duckdb",
23
- virtualHandle: "h",
24
- tablePath: "mz_real",
25
- schema: [{ name: "total", type: "BIGINT" }],
26
- };
27
-
28
- /**
29
- * A Model whose original source `X` yields total=0, with a real DuckDB
30
- * connection carrying `mz_real` (total=60) that the serve binding rebinds to.
31
- */
32
- async function buildModel(opts?: {
33
- originalText?: string;
34
- storageSQL?: string;
35
- }): Promise<Model> {
36
- const duckdb = new DuckDBConnection("duckdb", ":memory:");
37
- // DuckDB shares a process-global :memory: db by connection name, so a prior
38
- // test's table can linger — replace it.
39
- await duckdb.runSQL(
40
- opts?.storageSQL ??
41
- "CREATE OR REPLACE TABLE mz_real AS SELECT 60 AS total",
42
- );
43
- const connMap = new Map<string, DuckDBConnection>([["duckdb", duckdb]]);
44
- const originalText =
45
- opts?.originalText ?? `source: X is duckdb.sql("SELECT 0 AS total")`;
46
- const urlReader = new InMemoryURLReader(
47
- new Map([[`${ROOT}m.malloy`, originalText]]),
48
- );
49
- const runtime = new Runtime({
50
- urlReader,
51
- connections: new FixedConnectionMap(connMap, "duckdb"),
52
- });
53
- const mm = runtime.loadModel(new URL(`${ROOT}m.malloy`), {
54
- importBaseURL: new URL(ROOT),
55
- });
56
- const compiled = await mm.getModel();
57
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
- const modelDef = (compiled as any)._modelDef;
59
- const modelInfo = modelDefToModelInfo(modelDef);
60
- const model = new Model(
61
- "pkg",
62
- "m.malloy",
63
- {},
64
- "model",
65
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
- mm as any,
67
- modelDef,
68
- undefined,
69
- undefined,
70
- undefined,
71
- undefined,
72
- undefined,
73
- undefined,
74
- undefined,
75
- modelInfo,
76
- );
77
- model.setServeMalloyConfig(connMap);
78
- return model;
79
- }
80
-
81
- async function runTotal(model: Model): Promise<number> {
82
- const res = await model.getQueryResults(
83
- undefined,
84
- undefined,
85
- QUERY,
86
- {},
87
- true,
88
- );
89
- // compactResult is the row value array: [{ t: <sum> }].
90
- const rows = res.compactResult as unknown as { t: number }[];
91
- return Number(rows[0].t);
92
- }
93
-
94
- describe("storage= serve routing (end-to-end)", () => {
95
- afterEach(() => {
96
- delete process.env.PERSIST_STORAGE_MODE;
97
- });
98
-
99
- it("mode=on + a binding routes the query to the materialized table", async () => {
100
- process.env.PERSIST_STORAGE_MODE = "on";
101
- const model = await buildModel();
102
- model.setServeBindings([BINDING]);
103
- expect(await runTotal(model)).toBe(60);
104
- });
105
-
106
- it("mode=off serves live even with a binding present (kill switch)", async () => {
107
- process.env.PERSIST_STORAGE_MODE = "off";
108
- const model = await buildModel();
109
- model.setServeBindings([BINDING]);
110
- expect(await runTotal(model)).toBe(0);
111
- });
112
-
113
- it("mode=write-only serves live (build-only rung)", async () => {
114
- process.env.PERSIST_STORAGE_MODE = "write-only";
115
- const model = await buildModel();
116
- model.setServeBindings([BINDING]);
117
- expect(await runTotal(model)).toBe(0);
118
- });
119
-
120
- it("mode=on with no bindings serves live", async () => {
121
- process.env.PERSIST_STORAGE_MODE = "on";
122
- const model = await buildModel();
123
- expect(await runTotal(model)).toBe(0);
124
- });
125
-
126
- it("falls back to live when the serve shape cannot compile (bad connection)", async () => {
127
- process.env.PERSIST_STORAGE_MODE = "on";
128
- const model = await buildModel();
129
- // A binding on a connection the config doesn't have — the serve-shape
130
- // compile throws, and the query must still succeed, served live.
131
- model.setServeBindings([{ ...BINDING, connectionName: "missing_conn" }]);
132
- expect(await runTotal(model)).toBe(0);
133
- });
134
-
135
- it("a binding for a source this model does not define does not break serving the model's own sources", async () => {
136
- // Bindings are pushed to EVERY model in the package, so a model can receive
137
- // a binding for a source it doesn't define (defined in a sibling model).
138
- // That source has no field list here, so its narrowed schema is empty — the
139
- // serve shape must simply omit it, not emit an empty `type: {}` that fails
140
- // the whole shape model and drops storage serving for X too.
141
- process.env.PERSIST_STORAGE_MODE = "on";
142
- const model = await buildModel();
143
- model.setServeBindings([
144
- BINDING,
145
- {
146
- sourceName: "OtherModelSource", // not defined in this model
147
- connectionName: "duckdb",
148
- virtualHandle: "h2",
149
- tablePath: "mz_real",
150
- schema: [{ name: "total", type: "BIGINT" }],
151
- },
152
- ]);
153
- expect(await runTotal(model)).toBe(60);
154
- });
155
-
156
- it("does not serve an except:-hidden column from storage (vector A)", async () => {
157
- process.env.PERSIST_STORAGE_MODE = "on";
158
- // X hides `secret` via except:, but the built table still carries it (the
159
- // build's getSQL projected it) and the binding schema captured it.
160
- const model = await buildModel({
161
- originalText:
162
- `source: X is duckdb.sql("SELECT 0 AS total, 'live' AS secret") ` +
163
- `extend { except: secret }`,
164
- storageSQL:
165
- "CREATE OR REPLACE TABLE mz_real AS " +
166
- "SELECT 60 AS total, 'from_storage' AS secret",
167
- });
168
- model.setServeBindings([
169
- {
170
- ...BINDING,
171
- schema: [
172
- { name: "total", type: "BIGINT" },
173
- { name: "secret", type: "VARCHAR" },
174
- ],
175
- },
176
- ]);
177
- // The public column still serves from storage (narrowing didn't break it).
178
- expect(await runTotal(model)).toBe(60);
179
- // A query on the hidden column must NOT be served from storage: the shape
180
- // no longer declares `secret`, so it falls back to live, where X's
181
- // `except:` makes `secret` undefined → the query is refused. (Before the
182
- // fix this returned 'from_storage' — the leak.)
183
- await expect(
184
- model.getQueryResults(
185
- undefined,
186
- undefined,
187
- "run: X -> { group_by: secret }",
188
- {},
189
- true,
190
- ),
191
- ).rejects.toThrow();
192
- });
193
- });