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]

Reply via email to