fix(client): page explicit LIMITs above index.max_result_window through scroll (#224) - #225
Merged
Merged
Conversation
…gh scroll (#224) A SELECT with an explicit LIMIT above the index's max_result_window (ES default 10,000) failed outright, while the same query with NO LIMIT succeeded and returned every row: the one-shot search path issued a single search with size = LIMIT, which Elasticsearch rejects whenever from + size exceeds the window. The failure reached the caller as an opaque error naming neither LIMIT nor max_result_window. Routing (defect 1): extend #209's scroll routing in SearchApi.search / searchAsync — a row-shaped query whose LIMIT window (offset + limit) exceeds the ES default window (SearchApi.DefaultMaxResultWindow, 10,000) now pages through scroll bounded by maxDocuments = offset + limit, with the first offset rows dropped client-side and the statement's LIMIT stripped before translation (scroll contexts reject "from"). The per-index setting is deliberately not probed: an index tuned higher just pages (still correct, and fast post-#197); aggregation-shaped queries are never routed (their result is the aggregation itself). The licensed cap is unaffected — CoreDqlExtension rule (1) still 402-rejects an explicit LIMIT above quota before execution, so the bounded scroll can never exceed a vetted limit. Error opacity (defect 2), for the residual one-shot rejections (index tuned BELOW the threshold; UNION legs): - core: translate a max_result_window rejection into an actionable message naming LIMIT/OFFSET and the remedies, scanning message, cause chain and suppressed exceptions (ES 6/7 RHLC nests the per-shard root cause as suppressed on "all shards failed"); - core: singleSearchAsync / multiSearchAsync now recover a FAILED future into an ElasticFailure — previously the raw Throwable propagated to consumers, which flattened it into a generic error; - es8/es9: extract rootCause / causedBy reasons from the typed ErrorCause tree (invisible outside the module), and recover the async search paths through the same extraction. New LimitCompletenessSpec (testkit + 5 client subclasses): 12,000-doc 3-shard index asserting the asymmetry directly — LIMIT 11000 and no-LIMIT both complete, ORDER BY + OFFSET stays exact through the routing, LIMIT 10000 stays one-shot (boundary), and a window-lowered index yields an error naming max_result_window. Green on real ES 6.8 (rest + jest) / 7.17 / 8.18 / 9.0; sibling completeness guards (#197/#207/#209) and the 737 core unit tests stay green. Closes #224 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #224
Problem
A
SELECTwith an explicitLIMIT nabove the index'sindex.max_result_window(ES default 10,000) failed outright, while the same query with noLIMITsucceeded and returned every row. The one-shot search path issued a single search withsize = LIMIT, which Elasticsearch rejects wheneverfrom + sizeexceeds the window — and the rejection reached the caller as an opaque generic error naming neitherLIMITnormax_result_window.Fix
Defect 1 — routing
SearchApi.search/searchAsyncextend #209's scroll routing: a row-shaped query whose LIMIT window (offset + limit) exceedsSearchApi.DefaultMaxResultWindow(10,000) now pages through scroll bounded bymaxDocuments = offset + limit, dropping the firstoffsetrows client-side. The statement'sLIMITis stripped before translation (scroll contexts rejectfrom; per-pagesizeis the scroll batch size).Design choice (the issue's option 2): the per-index window is deliberately not probed — a multi-index query has no single window anyway. An index tuned higher just pages (still correct, and fast post-#197); an index tuned lower keeps a one-shot rejection below the threshold, now translated (defect 2).
Guard rails preserved:
returnsRowsfalse) — their result is the aggregation itself (SELECT without LIMIT returns only 10 rows on the non-scroll search path #209 rule).CoreDqlExtensionrule (1) still 402-rejects an explicitLIMITabove a finite quota before execution, so the bounded scroll can never exceed a vetted limit.LIMIT 10000(exactly at the window) stays one-shot — the guard is strict>.Defect 2 — error opacity (residual one-shot rejections: index tuned below the threshold, UNION legs)
max_result_windowrejection is translated into an actionable message namingLIMIT/OFFSETand the remedies. The scan walks message, cause chain and suppressed exceptions (ES 6/7 RHLC nests the per-shard root cause as suppressed on "all shards failed").singleSearchAsync/multiSearchAsyncnowrecovera failed future into anElasticFailure— previously the raw Throwable propagated to consumers (this is exactly how the Flight sidecar ended up showingThere was an error servicing your request).rootCause/causedByreasons from the typedErrorCausetree (invisible outside those modules — it is not part of any Throwable message), and the async search paths recover through the same extraction.Tests
New
LimitCompletenessSpec(testkit trait + 5 client subclasses), asserting the issue's asymmetry directly on a 12,000-doc / 3-shard index:LIMIT 11000(above window)LIMIT 11000viasearchAsyncORDER BY id LIMIT 10500 OFFSET 1000id_01001..id_11500, sortedLIMIT 42/LIMIT 10000max_result_window: 100,LIMIT 250max_result_window+LIMITGreen on real ES 6.8 (rest + jest) / 7.17 / 8.18 / 9.0. Sibling completeness guards (#197
ScrollCompletenessSpec, #207WindowPartitionCompletenessSpec, #209SelectCompletenessSpec) and the 737 core unit tests stay green; cross-compiled 2.12 + 2.13;scalafmtCheck+headerCheckpass.Consumers
The routing lives in core, so every consumer (JDBC, ADBC, REPL, Flight SQL sidecar) inherits the fix on the next core bump — including the sidecar where arrow#157 was first observed.
🤖 Generated with Claude Code