This is an automated email from the ASF dual-hosted git repository.
FrankChen021 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/master by this push:
new b555785b40e fix(sql): honor Statement.setMaxRows in the Avatica JDBC
server (#19941)
b555785b40e is described below
commit b555785b40e1d7278958f6e5c1d5792934875c9a
Author: kdelay <[email protected]>
AuthorDate: Wed Sep 16 11:31:10 2026 +0900
fix(sql): honor Statement.setMaxRows in the Avatica JDBC server (#19941)
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.
---
.../druid/sql/avatica/DruidJdbcResultSet.java | 7 +-
.../druid/sql/avatica/DruidJdbcStatement.java | 7 +-
.../druid/sql/avatica/DruidStatementTest.java | 80 ++++++++++++++++++++++
3 files changed, 91 insertions(+), 3 deletions(-)
diff --git
a/sql/src/main/java/org/apache/druid/sql/avatica/DruidJdbcResultSet.java
b/sql/src/main/java/org/apache/druid/sql/avatica/DruidJdbcResultSet.java
index 1f75a135177..e1190aa8eab 100644
--- a/sql/src/main/java/org/apache/druid/sql/avatica/DruidJdbcResultSet.java
+++ b/sql/src/main/java/org/apache/druid/sql/avatica/DruidJdbcResultSet.java
@@ -113,7 +113,7 @@ public class DruidJdbcResultSet implements Closeable
}
/**
- * Fetch the next batch up to the batch size or EOF. Return
+ * Fetch the next batch up to the batch size, the row limit, or EOF. Return
* the resulting frame. Exceptions are handled by the executor
* framework.
*/
@@ -130,7 +130,10 @@ public class DruidJdbcResultSet implements Closeable
rowCount++;
}
- final Meta.Frame result = new Meta.Frame(offset, yielder.isDone(), rows);
+ // The frame is complete once the row limit is reached, even if the query
+ // itself has more rows. Reporting only yielder.isDone() here would make
+ // every later fetch return an empty, not-done frame forever.
+ final Meta.Frame result = new Meta.Frame(offset, yielder.isDone() ||
offset + rowCount >= limit, rows);
offset += rowCount;
return result;
}
diff --git
a/sql/src/main/java/org/apache/druid/sql/avatica/DruidJdbcStatement.java
b/sql/src/main/java/org/apache/druid/sql/avatica/DruidJdbcStatement.java
index 6e4570e4a58..ec22a660c91 100644
--- a/sql/src/main/java/org/apache/druid/sql/avatica/DruidJdbcStatement.java
+++ b/sql/src/main/java/org/apache/druid/sql/avatica/DruidJdbcStatement.java
@@ -63,13 +63,18 @@ public class DruidJdbcStatement extends
AbstractDruidJdbcStatement
* (execute) request rather than captured when the statement was created, so
that
* a statement reused after a reconnect or from a different remote is
attributed
* to the actual caller of this execution.
+ * <p>
+ * {@code maxRowCount} is the total row limit requested by the client through
+ * {@code Statement.setMaxRows()}. Avatica maps "no limit" to a negative
value
+ * before it reaches this method, which {@link DruidJdbcResultSet} reads as
+ * unlimited.
*/
public synchronized void execute(SqlQueryPlus queryPlus, long maxRowCount,
@Nullable String remoteAddress)
{
closeResultSet();
this.sqlQuery = queryPlus.withContext(defaultContext,
queryContext).freshCopy();
DirectStatement stmt = lifecycleFactory.directStatement(this.sqlQuery,
remoteAddress);
- resultSet = new DruidJdbcResultSet(this, stmt, Long.MAX_VALUE,
fetcherFactory);
+ resultSet = new DruidJdbcResultSet(this, stmt, maxRowCount,
fetcherFactory);
try {
resultSet.execute();
}
diff --git
a/sql/src/test/java/org/apache/druid/sql/avatica/DruidStatementTest.java
b/sql/src/test/java/org/apache/druid/sql/avatica/DruidStatementTest.java
index cd669d75105..5154f980aaa 100644
--- a/sql/src/test/java/org/apache/druid/sql/avatica/DruidStatementTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/avatica/DruidStatementTest.java
@@ -359,6 +359,86 @@ public class DruidStatementTest extends CalciteTestBase
}
}
+ /**
+ * The row limit from {@code Statement.setMaxRows()} arrives as the
+ * {@code maxRowCount} argument of
+ * {@link DruidJdbcStatement#execute(SqlQueryPlus, long, String)} and must
cap
+ * the result set even when the client asks for more rows than the limit.
+ */
+ @Test
+ public void testMaxRowCountDirect()
+ {
+ SqlQueryPlus queryPlus =
+ SqlQueryPlus.builder()
+ .sql(SELECT_FROM_FOO)
+ .auth(AllowAllAuthenticator.ALLOW_ALL_RESULT)
+ .buildJdbc();
+ try (final DruidJdbcStatement statement = jdbcStatement()) {
+ // The query returns six rows; the client only wants two.
+ statement.execute(queryPlus, 2, null);
+ Meta.Frame frame =
statement.nextFrame(AbstractDruidJdbcStatement.START_OFFSET, 10);
+ Assertions.assertEquals(
+ Meta.Frame.create(
+ 0,
+ true,
+ Lists.newArrayList(
+ new Object[]{DateTimes.of("2000-01-01").getMillis(), 1L, "",
"a", 1.0f},
+ new Object[]{
+ DateTimes.of("2000-01-02").getMillis(),
+ 1L,
+ "10.1",
+ null,
+ 2.0f
+ }
+ )
+ ),
+ frame
+ );
+ Assertions.assertTrue(statement.isDone());
+ }
+ }
+
+ /**
+ * A row limit reached across two frames must still mark the last frame
+ * complete. Otherwise the client keeps fetching empty, not-done frames
+ * because the underlying query has rows the limit hides.
+ */
+ @Test
+ public void testMaxRowCountSplitOverTwoFramesDirect()
+ {
+ SqlQueryPlus queryPlus =
+ SqlQueryPlus.builder()
+ .sql(SELECT_FROM_FOO)
+ .auth(AllowAllAuthenticator.ALLOW_ALL_RESULT)
+ .buildJdbc();
+ try (final DruidJdbcStatement statement = jdbcStatement()) {
+ // The query returns six rows; the client only wants three.
+ statement.execute(queryPlus, 3, null);
+
+ // First frame, ask for 2 rows: below the limit, so more rows follow.
+ Meta.Frame frame =
statement.nextFrame(AbstractDruidJdbcStatement.START_OFFSET, 2);
+ Assertions.assertEquals(
+ firstFrameResults(),
+ frame
+ );
+ Assertions.assertFalse(statement.isDone());
+
+ // Second frame, ask for more rows than remain under the limit.
+ frame = statement.nextFrame(2, 10);
+ Assertions.assertEquals(
+ Meta.Frame.create(
+ 2,
+ true,
+ Collections.singletonList(
+ new Object[]{DateTimes.of("2000-01-03").getMillis(), 1L,
"2", "", 3.0f}
+ )
+ ),
+ frame
+ );
+ Assertions.assertTrue(statement.isDone());
+ }
+ }
+
/**
* Verify that JDBC automatically closes the first result set when we
* open a second for the same statement.
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]