Skip to content

fix(client): page explicit LIMITs above index.max_result_window through scroll (#224) - #225

Merged
fupelaqu merged 2 commits into
mainfrom
fix/224-limit-above-max-result-window
Aug 11, 2026
Merged

fix(client): page explicit LIMITs above index.max_result_window through scroll (#224)#225
fupelaqu merged 2 commits into
mainfrom
fix/224-limit-above-max-result-window

Conversation

@fupelaqu

Copy link
Copy Markdown
Contributor

Closes #224

Problem

A SELECT with an explicit LIMIT n above the index's index.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 — and the rejection reached the caller as an opaque generic error naming neither LIMIT nor max_result_window.

Fix

Defect 1 — routing

SearchApi.search / searchAsync extend #209's scroll routing: a row-shaped query whose LIMIT window (offset + limit) exceeds SearchApi.DefaultMaxResultWindow (10,000) now pages through scroll bounded by maxDocuments = offset + limit, dropping the first offset rows client-side. The statement's LIMIT is stripped before translation (scroll contexts reject from; per-page size is 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:

  • Aggregation-shaped queries are never routed (returnsRows false) — their result is the aggregation itself (SELECT without LIMIT returns only 10 rows on the non-scroll search path #209 rule).
  • The licensed cap is unaffected: CoreDqlExtension rule (1) still 402-rejects an explicit LIMIT above 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)

  • core: a max_result_window rejection is translated into an actionable message naming LIMIT/OFFSET and 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").
  • core: singleSearchAsync / multiSearchAsync now recover a failed future into an ElasticFailure — previously the raw Throwable propagated to consumers (this is exactly how the Flight sidecar ended up showing There was an error servicing your request).
  • es8/es9: error extraction now includes the rootCause / causedBy reasons from the typed ErrorCause tree (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:

Test Asserts
LIMIT 11000 (above window) exactly 11,000 distinct rows
same query, no LIMIT all 12,000 rows (the #224 asymmetry pair)
LIMIT 11000 via searchAsync 11,000 rows on the async path
ORDER BY id LIMIT 10500 OFFSET 1000 exact slice id_01001..id_11500, sorted
LIMIT 42 / LIMIT 10000 one-shot bound kept, boundary not routed
index with max_result_window: 100, LIMIT 250 failure message names max_result_window + LIMIT

Green on real ES 6.8 (rest + jest) / 7.17 / 8.18 / 9.0. Sibling completeness guards (#197 ScrollCompletenessSpec, #207 WindowPartitionCompletenessSpec, #209 SelectCompletenessSpec) and the 737 core unit tests stay green; cross-compiled 2.12 + 2.13; scalafmtCheck + headerCheck pass.

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

fupelaqu and others added 2 commits August 11, 2026 17:05
…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>
@fupelaqu
fupelaqu merged commit de4700a into main Aug 11, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SELECT with an explicit LIMIT above index.max_result_window fails, while the same query with NO LIMIT succeeds

1 participant