@malloy-publisher/server 0.0.234 → 0.0.236

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