@malloy-publisher/server 0.0.235 → 0.0.236
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/api-doc.yaml +47 -17
- package/dist/package_load_worker.mjs +8 -1
- package/dist/server.mjs +471 -54
- package/package.json +5 -2
- package/.eslintrc.json +0 -44
- package/.prettierignore +0 -1
- package/.prettierrc +0 -4
- package/build.ts +0 -97
- package/eslint.config.mjs +0 -76
- package/k6-tests/README.md +0 -144
- package/k6-tests/bun.lock +0 -797
- package/k6-tests/load-test/load-test-crud-connections.ts +0 -263
- package/k6-tests/load-test/load-test-crud-packages.ts +0 -461
- package/k6-tests/load-test/load-test-crud-projects.ts +0 -160
- package/k6-tests/load-test/load-test-crud.ts +0 -142
- package/k6-tests/load-test/load-test.ts +0 -784
- package/k6-tests/package.json +0 -27
- package/k6-tests/scripts/run-with-otel.sh +0 -50
- package/k6-tests/smoke-test/smoke-test.ts +0 -158
- package/k6-tests/tsconfig.json +0 -29
- package/k6-tests/utils/client_factory.ts +0 -128
- package/k6-tests/utils/common.ts +0 -489
- package/k6-tests/utils/logger.ts +0 -62
- package/k6-tests/utils/types.d.ts +0 -35
- package/malloy_mcp.dxt +0 -0
- package/publisher.config.example.bigquery.json +0 -33
- package/publisher.config.example.duckdb.json +0 -23
- package/publisher.config.json +0 -23
- package/scripts/bake-duckdb-extensions.js +0 -110
- package/src/config.spec.ts +0 -1535
- package/src/config.theme.spec.ts +0 -178
- package/src/config.ts +0 -1142
- package/src/constants.ts +0 -105
- package/src/controller/compile.controller.ts +0 -40
- package/src/controller/connection.controller.spec.ts +0 -849
- package/src/controller/connection.controller.ts +0 -901
- package/src/controller/database.controller.ts +0 -24
- package/src/controller/materialization.controller.spec.ts +0 -293
- package/src/controller/materialization.controller.ts +0 -319
- package/src/controller/model.controller.ts +0 -164
- package/src/controller/package.controller.spec.ts +0 -273
- package/src/controller/package.controller.ts +0 -306
- package/src/controller/query.controller.ts +0 -152
- package/src/controller/theme.controller.ts +0 -83
- package/src/controller/watch-mode.controller.ts +0 -243
- package/src/data_styles.ts +0 -71
- package/src/default-publisher.config.json +0 -23
- package/src/ducklake_version.spec.ts +0 -163
- package/src/ducklake_version.ts +0 -153
- package/src/errors.spec.ts +0 -119
- package/src/errors.ts +0 -237
- package/src/filter_deprecation.spec.ts +0 -64
- package/src/filter_deprecation.ts +0 -42
- package/src/health.spec.ts +0 -123
- package/src/health.ts +0 -233
- package/src/heap_check.spec.ts +0 -144
- package/src/heap_check.ts +0 -144
- package/src/instrumentation.ts +0 -214
- package/src/json_utils.spec.ts +0 -51
- package/src/json_utils.ts +0 -33
- package/src/logger.spec.ts +0 -210
- package/src/logger.ts +0 -251
- package/src/malloy_pin_prereqs.spec.ts +0 -25
- package/src/materialization_metrics.spec.ts +0 -169
- package/src/materialization_metrics.ts +0 -346
- package/src/mcp/error_messages.spec.ts +0 -35
- package/src/mcp/error_messages.ts +0 -183
- package/src/mcp/handler_utils.spec.ts +0 -108
- package/src/mcp/handler_utils.ts +0 -251
- package/src/mcp/mcp_constants.ts +0 -6
- package/src/mcp/query_envelope.spec.ts +0 -229
- package/src/mcp/query_envelope.ts +0 -240
- package/src/mcp/server.protocol.spec.ts +0 -250
- package/src/mcp/server.ts +0 -87
- package/src/mcp/skills/build_skills_bundle.spec.ts +0 -51
- package/src/mcp/skills/build_skills_bundle.ts +0 -176
- package/src/mcp/skills/skills_bundle.json +0 -1
- package/src/mcp/skills/skills_bundle.spec.ts +0 -171
- package/src/mcp/tool_response.spec.ts +0 -108
- package/src/mcp/tool_response.ts +0 -138
- package/src/mcp/tools/compile_tool.spec.ts +0 -315
- package/src/mcp/tools/compile_tool.ts +0 -208
- package/src/mcp/tools/docs_search/build_docs_index.ts +0 -132
- package/src/mcp/tools/docs_search/malloy_docs_index.json +0 -1
- package/src/mcp/tools/docs_search_tool.spec.ts +0 -32
- package/src/mcp/tools/docs_search_tool.ts +0 -128
- package/src/mcp/tools/embedding_index.spec.ts +0 -1236
- package/src/mcp/tools/embedding_index.ts +0 -808
- package/src/mcp/tools/execute_query_tool.spec.ts +0 -294
- package/src/mcp/tools/execute_query_tool.ts +0 -316
- package/src/mcp/tools/get_context_eval.ts +0 -275
- package/src/mcp/tools/get_context_tool.spec.ts +0 -592
- package/src/mcp/tools/get_context_tool.ts +0 -585
- package/src/mcp/tools/reload_package_tool.spec.ts +0 -232
- package/src/mcp/tools/reload_package_tool.ts +0 -132
- package/src/mcp_config.spec.ts +0 -919
- package/src/mcp_config.ts +0 -425
- package/src/oom_guards.integration.spec.ts +0 -269
- package/src/package_load/package_load_pool.spec.ts +0 -252
- package/src/package_load/package_load_pool.ts +0 -931
- package/src/package_load/package_load_worker.ts +0 -1034
- package/src/package_load/protocol.ts +0 -364
- package/src/package_load/rpc_wait_accountant.spec.ts +0 -109
- package/src/package_load/rpc_wait_accountant.ts +0 -76
- package/src/package_load_metrics.spec.ts +0 -114
- package/src/package_load_metrics.ts +0 -127
- package/src/path_safety.spec.ts +0 -158
- package/src/path_safety.ts +0 -146
- package/src/pg_helpers.spec.ts +0 -223
- package/src/pg_helpers.ts +0 -52
- package/src/query_cap_metrics.spec.ts +0 -89
- package/src/query_cap_metrics.ts +0 -116
- package/src/query_concurrency.spec.ts +0 -247
- package/src/query_concurrency.ts +0 -237
- package/src/query_metadata_metrics.ts +0 -49
- package/src/query_param_utils.ts +0 -29
- package/src/query_timeout.spec.ts +0 -224
- package/src/query_timeout.ts +0 -179
- package/src/runtime/publisher.js +0 -328
- package/src/server-old.ts +0 -988
- package/src/server.ts +0 -2059
- package/src/service/annotations.spec.ts +0 -118
- package/src/service/annotations.ts +0 -91
- package/src/service/authorize.spec.ts +0 -154
- package/src/service/authorize.ts +0 -498
- package/src/service/authorize_integration.spec.ts +0 -2041
- package/src/service/build_plan.spec.ts +0 -685
- package/src/service/build_plan.ts +0 -797
- package/src/service/compile_authorize.spec.ts +0 -85
- package/src/service/compile_fragment_techniques.spec.ts +0 -156
- package/src/service/connection.spec.ts +0 -2360
- package/src/service/connection.ts +0 -2187
- package/src/service/connection_config.spec.ts +0 -734
- package/src/service/connection_config.ts +0 -899
- package/src/service/connection_federation.spec.ts +0 -184
- package/src/service/connection_fingerprint.spec.ts +0 -102
- package/src/service/connection_service.spec.ts +0 -711
- package/src/service/connection_service.ts +0 -291
- package/src/service/cron_evaluator.spec.ts +0 -79
- package/src/service/cron_evaluator.ts +0 -105
- package/src/service/db_utils.spec.ts +0 -1085
- package/src/service/db_utils.ts +0 -1502
- package/src/service/duckdb_instance_isolation.spec.ts +0 -137
- package/src/service/ducklake_lazy_attach.spec.ts +0 -110
- package/src/service/embedding_provider.spec.ts +0 -329
- package/src/service/embedding_provider.ts +0 -236
- package/src/service/environment.ts +0 -2191
- package/src/service/environment_admission.spec.ts +0 -344
- package/src/service/environment_compile.spec.ts +0 -197
- package/src/service/environment_store.spec.ts +0 -2274
- package/src/service/environment_store.ts +0 -2434
- package/src/service/environment_store_anchoring.spec.ts +0 -107
- package/src/service/environment_store_clone.spec.ts +0 -350
- package/src/service/explore_visibility.spec.ts +0 -434
- package/src/service/exports_probe.spec.ts +0 -107
- package/src/service/extension_fetch_policy.spec.ts +0 -256
- package/src/service/filter.spec.ts +0 -458
- package/src/service/filter.ts +0 -341
- package/src/service/filter_bypass.spec.ts +0 -418
- package/src/service/filter_integration.spec.ts +0 -1004
- package/src/service/freshness.spec.ts +0 -183
- package/src/service/freshness.ts +0 -112
- package/src/service/gcs_s3_utils.ts +0 -427
- package/src/service/given.ts +0 -105
- package/src/service/givens_integration.spec.ts +0 -219
- package/src/service/manifest_loader.spec.ts +0 -189
- package/src/service/manifest_loader.ts +0 -156
- package/src/service/materialization_build_session.spec.ts +0 -435
- package/src/service/materialization_build_session.ts +0 -681
- package/src/service/materialization_config_validation.spec.ts +0 -99
- package/src/service/materialization_config_validation.ts +0 -120
- package/src/service/materialization_eligibility.spec.ts +0 -158
- package/src/service/materialization_eligibility.ts +0 -305
- package/src/service/materialization_schedule_surface.spec.ts +0 -256
- package/src/service/materialization_scheduler.spec.ts +0 -289
- package/src/service/materialization_scheduler.ts +0 -304
- package/src/service/materialization_serve_transform.spec.ts +0 -1003
- package/src/service/materialization_serve_transform.ts +0 -779
- package/src/service/materialization_service.spec.ts +0 -2373
- package/src/service/materialization_service.ts +0 -2342
- package/src/service/materialization_test_fixtures.ts +0 -249
- package/src/service/model.spec.ts +0 -1157
- package/src/service/model.ts +0 -3370
- package/src/service/model_limits.spec.ts +0 -209
- package/src/service/model_limits.ts +0 -131
- package/src/service/model_storage_serve.spec.ts +0 -193
- package/src/service/model_storage_serve_joins.spec.ts +0 -193
- package/src/service/package.spec.ts +0 -612
- package/src/service/package.ts +0 -1799
- package/src/service/package_manifest.spec.ts +0 -230
- package/src/service/package_manifest.ts +0 -241
- package/src/service/package_memory_governor.spec.ts +0 -173
- package/src/service/package_memory_governor.ts +0 -233
- package/src/service/package_quote_bound_tables.spec.ts +0 -171
- package/src/service/package_race.spec.ts +0 -208
- package/src/service/package_reload_safety.spec.ts +0 -193
- package/src/service/package_rollback.spec.ts +0 -190
- package/src/service/package_worker_path.spec.ts +0 -645
- package/src/service/path_injection.spec.ts +0 -39
- package/src/service/persist_annotation_validation.spec.ts +0 -89
- package/src/service/persist_annotation_validation.ts +0 -59
- package/src/service/persistence_policy.spec.ts +0 -374
- package/src/service/proxy.spec.ts +0 -414
- package/src/service/proxy.ts +0 -248
- package/src/service/query_boundary.spec.ts +0 -470
- package/src/service/query_metadata.spec.ts +0 -408
- package/src/service/query_metadata.ts +0 -492
- package/src/service/query_metadata_identity.spec.ts +0 -149
- package/src/service/query_text.spec.ts +0 -79
- package/src/service/query_text.ts +0 -43
- package/src/service/quoting.spec.ts +0 -135
- package/src/service/quoting.ts +0 -78
- package/src/service/resolve_environment.ts +0 -15
- package/src/service/restricted_mode.spec.ts +0 -299
- package/src/service/source_extraction.ts +0 -225
- package/src/service/theme_store.ts +0 -199
- package/src/storage/DatabaseInterface.ts +0 -198
- package/src/storage/StorageManager.ts +0 -113
- package/src/storage/duckdb/ConnectionRepository.ts +0 -143
- package/src/storage/duckdb/DuckDBConnection.ts +0 -123
- package/src/storage/duckdb/DuckDBRepository.ts +0 -208
- package/src/storage/duckdb/EnvironmentRepository.ts +0 -148
- package/src/storage/duckdb/MaterializationRepository.spec.ts +0 -191
- package/src/storage/duckdb/MaterializationRepository.ts +0 -336
- package/src/storage/duckdb/PackageRepository.ts +0 -135
- package/src/storage/duckdb/schema.ts +0 -229
- package/src/stream_helpers.spec.ts +0 -280
- package/src/stream_helpers.ts +0 -162
- package/src/telemetry.ts +0 -20
- package/src/test_helpers/metrics_harness.ts +0 -166
- package/src/theme_key_parity.spec.ts +0 -57
- package/src/utils.ts +0 -29
- package/tests/fixtures/authorize-compile/model.malloy +0 -9
- package/tests/fixtures/authorize-compile/publisher.json +0 -4
- package/tests/fixtures/html-pages-nopublic/model.malloy +0 -1
- package/tests/fixtures/html-pages-nopublic/publisher.json +0 -5
- package/tests/fixtures/html-pages-test/data.csv +0 -3
- package/tests/fixtures/html-pages-test/public/assets/app.css +0 -3
- package/tests/fixtures/html-pages-test/public/barehtml.html +0 -4
- package/tests/fixtures/html-pages-test/public/bodymeta.html +0 -8
- package/tests/fixtures/html-pages-test/public/data.json +0 -1
- package/tests/fixtures/html-pages-test/public/fitcomment.html +0 -11
- package/tests/fixtures/html-pages-test/public/index.html +0 -9
- package/tests/fixtures/html-pages-test/public/nohead.html +0 -8
- package/tests/fixtures/html-pages-test/public/notfit.html +0 -13
- package/tests/fixtures/html-pages-test/public/slides.html +0 -12
- package/tests/fixtures/html-pages-test/public/sub/page2.html +0 -9
- package/tests/fixtures/html-pages-test/public/unterminated.html +0 -10
- package/tests/fixtures/html-pages-test/publisher.json +0 -5
- package/tests/fixtures/html-pages-test/report.malloy +0 -1
- package/tests/fixtures/persist-multi-level/data/orders.csv +0 -5
- package/tests/fixtures/persist-multi-level/multi_level.malloy +0 -18
- package/tests/fixtures/persist-multi-level/publisher.json +0 -5
- package/tests/fixtures/persist-schedule-test/data/orders.csv +0 -5
- package/tests/fixtures/persist-schedule-test/persist_schedule_test.malloy +0 -11
- package/tests/fixtures/persist-schedule-test/publisher.json +0 -7
- package/tests/fixtures/persist-schedule-warn-test/data/orders.csv +0 -5
- package/tests/fixtures/persist-schedule-warn-test/persist_warn_test.malloy +0 -11
- package/tests/fixtures/persist-schedule-warn-test/publisher.json +0 -6
- package/tests/fixtures/persist-test/data/orders.csv +0 -5
- package/tests/fixtures/persist-test/persist_test.malloy +0 -11
- package/tests/fixtures/persist-test/publisher.json +0 -5
- package/tests/fixtures/publisher.config.json +0 -15
- package/tests/fixtures/query-givens/data/orders.csv +0 -7
- package/tests/fixtures/query-givens/model.malloy +0 -34
- package/tests/fixtures/query-givens/publisher.json +0 -5
- package/tests/fixtures/xlsx/database.xlsx +0 -0
- package/tests/harness/mcp_test_setup.ts +0 -332
- package/tests/harness/rest_e2e.ts +0 -68
- package/tests/integration/authorize/compile_authorize_http.integration.spec.ts +0 -92
- package/tests/integration/concurrent_package/concurrent_package.integration.spec.ts +0 -280
- package/tests/integration/duckdb_storage/duckdb_storage.integration.spec.ts +0 -138
- package/tests/integration/first_boot/readiness_line.integration.spec.ts +0 -177
- package/tests/integration/html_pages/html_pages.integration.spec.ts +0 -440
- package/tests/integration/legacy_routes/legacy_routes.integration.spec.ts +0 -259
- package/tests/integration/materialization/environment_list.integration.spec.ts +0 -133
- package/tests/integration/materialization/freshness_gate.integration.spec.ts +0 -292
- package/tests/integration/materialization/manifest_binding.integration.spec.ts +0 -430
- package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +0 -441
- package/tests/integration/materialization/orchestrated_rebind.integration.spec.ts +0 -150
- package/tests/integration/materialization/reference_manifest.integration.spec.ts +0 -251
- package/tests/integration/materialization/schedule_edit.integration.spec.ts +0 -126
- package/tests/integration/materialization/scheduler.integration.spec.ts +0 -200
- package/tests/integration/materialization/scheduler_recovery.integration.spec.ts +0 -172
- package/tests/integration/materialization/scheduler_transitions.integration.spec.ts +0 -256
- package/tests/integration/materialization/scheduler_wiring.integration.spec.ts +0 -143
- package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +0 -438
- package/tests/integration/mcp/mcp_get_context_semantic.integration.spec.ts +0 -235
- package/tests/integration/mcp/mcp_transport.integration.spec.ts +0 -98
- package/tests/integration/query_givens/query_givens.integration.spec.ts +0 -146
- package/tests/integration/query_givens/query_givens_authorize.integration.spec.ts +0 -121
- package/tests/integration/sdk_givens/sdk_givens.integration.spec.ts +0 -110
- package/tests/integration/watch-mode/watch_mode.integration.spec.ts +0 -421
- package/tests/unit/controller/theme.controller.spec.ts +0 -141
- package/tests/unit/duckdb/attached_databases.test.ts +0 -1243
- package/tests/unit/duckdb/duckdb_connection.test.ts +0 -181
- package/tests/unit/duckdb/legacy_schema_migration.test.ts +0 -197
- package/tests/unit/duckdb/repositories.test.ts +0 -208
- package/tests/unit/ducklake/ducklake.test.ts +0 -952
- package/tests/unit/service/theme_store.spec.ts +0 -139
- 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
|
-
});
|