Skip to content

fix(sql): honor Statement.setMaxRows in the Avatica JDBC server - #19941

Open
kdelay wants to merge 1 commit into
apache:masterfrom
kdelay:fix/issue-19918-jdbc-max-rows
Open

fix(sql): honor Statement.setMaxRows in the Avatica JDBC server#19941
kdelay wants to merge 1 commit into
apache:masterfrom
kdelay:fix/issue-19918-jdbc-max-rows

Conversation

@kdelay

@kdelay kdelay commented Aug 9, 2026

Copy link
Copy Markdown

Fixes #19918.

Description

A JDBC client that calls Statement.setMaxRows(n) still gets every row the query produces. Avatica sends the value to DruidMeta.prepareAndExecute as maxRowCount, but DruidJdbcStatement.execute() discarded it and constructed the result set with Long.MAX_VALUE.

Passed the received row limit through

DruidJdbcStatement.execute() now hands maxRowCount to DruidJdbcResultSet instead of Long.MAX_VALUE. DruidJdbcPreparedStatement already did this, so this makes the two statement types consistent.

Unlimited statements are unaffected. AvaticaStatement.executeInternal converts the JDBC "no limit" value to -1 before the request leaves the client (maxRowCount <= 0 ? -1 : maxRowCount), and DruidJdbcResultSet.execute() already reads a negative limit as Integer.MAX_VALUE. So the only values that reach the fetcher as a real cap are the ones the user asked for.

Marked a frame complete when the row limit is reached

DruidJdbcResultSet.ResultFetcher.call() set Meta.Frame.done from yielder.isDone() alone. When the limit is lower than the number of rows the query produces, the yielder is not exhausted at the limit, so the last in-limit frame reported done = false. The next fetch then computes batchLimit = min(limit - offset, batchSize) = 0, returns an empty frame that is again not done, and the client keeps fetching forever. The frame is now also complete once offset + rowCount reaches the limit.

Not covered: prepared statements

The reporter's example uses a PreparedStatement, and that path still does not honor the limit. Avatica calls Meta.prepare(..., -1) when the statement is created and passes only the fetch size at execution time, so PreparedStatement.getMaxRows() never reaches the server. That is CALCITE-719, still open upstream. Adding an explicit LIMIT to the SQL remains the workaround there. This PR fixes what Druid can fix on its own.

Release note

The Avatica JDBC server now honors Statement.setMaxRows(). Previously the setting was ignored and the full result was returned. PreparedStatement.setMaxRows() is still not honored because the value is not transmitted to the server (CALCITE-719).


Key changed/added classes in this PR
  • DruidJdbcStatement
  • DruidJdbcResultSet

This PR has:

  • been self-reviewed.
  • added comments explaining the "why" and the intent of the code wherever would not be obvious for an unfamiliar reader.
  • added unit tests or modified existing tests to cover new code paths, ensuring the threshold for code coverage is met.

Two tests were added to DruidStatementTest: testMaxRowCountDirect (limit reached inside the first frame) and testMaxRowCountSplitOverTwoFramesDirect (limit reached in a later frame, which is the case that used to hang).

Local run on this branch, JDK 26 on macOS:

mvn test -pl sql -am -Dtest="org.apache.druid.sql.avatica.**" -Pskip-static-checks -Dweb.console.skip=true -T1C

DruidStatementTest 16/16 and DruidAvaticaHandlerTest 45/45 pass. AvaticaModuleTest and one case each in DruidAvaticaJsonHandlerTest and DruidAvaticaProtobufHandlerTest error with Failed to mock class org.apache.druid.server.DruidNode; those reproduce identically on an unmodified master checkout here, so they are a local JDK/EasyMock issue rather than a result of this change. mvn checkstyle:check -pl sql reports 0 violations.

DruidMeta.prepareAndExecute receives the row limit a JDBC client sets
through Statement.setMaxRows(), but DruidJdbcStatement.execute() dropped
it and built the result set with Long.MAX_VALUE, so the limit never took
effect. Pass the received value through instead. Avatica maps "no limit"
to a negative value before this point, and DruidJdbcResultSet already
reads a negative limit as unlimited, so unlimited statements are
unaffected. The prepared-statement path already passed its limit through.

ResultFetcher marked a frame complete only when the yielder was
exhausted. With a limit lower than the number of rows the query produces,
the last in-limit frame reported done = false and every later fetch
returned an empty, not-done frame, so the client never reached EOF. Mark
the frame complete when the limit is reached as well.

Prepared statements are still not covered: Avatica does not transmit
PreparedStatement.getMaxRows() to the server (CALCITE-719), so the value
never arrives at Druid.

@FrankChen021 FrankChen021 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have reviewed the code for correctness, edge cases, concurrency, and integration risks; no issues found.

Reviewed 3 of 3 changed files.

Validation: git diff --check passed; tests and builds were not run.


This is an automated review by Codex GPT-5.6-Luna(max)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Druid Avatica JDBC driver does not honor Statement.setMaxRows()

2 participants