This is an automated email from the ASF dual-hosted git repository.

gianm 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 e78e3b56452 fix: ScanQueryFrameProcessor should not send offset to 
data servers. (#20588)
e78e3b56452 is described below

commit e78e3b56452d2f3c9283eb807831288beba975c0
Author: Gian Merlino <[email protected]>
AuthorDate: Wed Oct 14 11:23:05 2026 -0700

    fix: ScanQueryFrameProcessor should not send offset to data servers. 
(#20588)
    
    ScanQueryFrameProcessor currently includes the original offset and
    limit when sending queries to data servers. It should instead send
    an offset of zero and add the offset to the limit.
---
 .../msq/querykit/scan/ScanQueryFrameProcessor.java |  18 +--
 .../druid/msq/exec/MSQLoadedSegmentTests.java      | 145 +++++++++++++++++++++
 2 files changed, 155 insertions(+), 8 deletions(-)

diff --git 
a/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/scan/ScanQueryFrameProcessor.java
 
b/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/scan/ScanQueryFrameProcessor.java
index b2dac19e4b1..5ce7b01580a 100644
--- 
a/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/scan/ScanQueryFrameProcessor.java
+++ 
b/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/scan/ScanQueryFrameProcessor.java
@@ -188,7 +188,7 @@ public class ScanQueryFrameProcessor extends 
BaseLeafFrameProcessor
   public ReturnOrAwait<Object> runIncrementally(final IntSet readableInputs) 
throws IOException
   {
     if (sharedRunningCountForLimit != null
-        && sharedRunningCountForLimit.get() >= getQueryOffsetPlusLimit()) {
+        && sharedRunningCountForLimit.get() >= getOffsetPlusLimit(query)) {
       return ReturnOrAwait.returnObject(Unit.instance());
     }
 
@@ -223,14 +223,16 @@ public class ScanQueryFrameProcessor extends 
BaseLeafFrameProcessor
    */
   private static ScanQuery prepareScanQueryForDataServer(@NotNull ScanQuery 
scanQuery)
   {
+    // Add the offset to the limit, and remove the offset, when pushing down a 
query.
+    final Druids.ScanQueryBuilder builder = 
Druids.ScanQueryBuilder.copy(scanQuery).offset(0);
+
     if (Order.NONE.equals(scanQuery.getTimeOrder()) && 
!scanQuery.getOrderBys().isEmpty()) {
-      return Druids.ScanQueryBuilder.copy(scanQuery)
-                                    .orderBy(ImmutableList.of())
-                                    .limit(0)
-                                    .build();
+      builder.orderBy(ImmutableList.of()).limit(0);
     } else {
-      return scanQuery;
+      builder.limit(getOffsetPlusLimit(scanQuery));
     }
+
+    return builder.build();
   }
 
   @Override
@@ -557,7 +559,7 @@ public class ScanQueryFrameProcessor extends 
BaseLeafFrameProcessor
     if (query.isLimited()
         && ordering.size() >= query.getOrderBys().size()
         && query.getOrderBys().equals(ordering.subList(0, 
query.getOrderBys().size()))) {
-      cursorPushDownLimit = getQueryOffsetPlusLimit();
+      cursorPushDownLimit = getOffsetPlusLimit(query);
     } else {
       cursorPushDownLimit = NO_LIMIT;
     }
@@ -569,7 +571,7 @@ public class ScanQueryFrameProcessor extends 
BaseLeafFrameProcessor
    * Returns the {@link ScanQuery#getScanRowsOffset()} plus {@link 
ScanQuery#getScanRowsLimit()}, or
    * {@link Long#MAX_VALUE} if that addition would overflow.
    */
-  private long getQueryOffsetPlusLimit()
+  private static long getOffsetPlusLimit(final ScanQuery query)
   {
     final long sum = query.getScanRowsOffset() + query.getScanRowsLimit();
     if (sum < 0) {
diff --git 
a/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQLoadedSegmentTests.java
 
b/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQLoadedSegmentTests.java
index fd629551e0a..0b13ab04676 100644
--- 
a/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQLoadedSegmentTests.java
+++ 
b/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQLoadedSegmentTests.java
@@ -25,9 +25,11 @@ import com.google.common.collect.ImmutableSet;
 import com.google.common.util.concurrent.Futures;
 import org.apache.druid.client.ImmutableSegmentLoadInfo;
 import org.apache.druid.error.ThrowableMatcher;
+import org.apache.druid.java.util.common.DateTimes;
 import org.apache.druid.java.util.common.ISE;
 import org.apache.druid.java.util.common.Intervals;
 import org.apache.druid.java.util.common.granularity.Granularities;
+import org.apache.druid.java.util.common.guava.Sequence;
 import org.apache.druid.java.util.common.guava.Sequences;
 import org.apache.druid.java.util.common.guava.Yielders;
 import org.apache.druid.msq.indexing.LegacyMSQSpec;
@@ -35,12 +37,18 @@ import org.apache.druid.msq.indexing.MSQTuningConfig;
 import org.apache.druid.msq.indexing.destination.TaskReportMSQDestination;
 import org.apache.druid.msq.test.MSQTestBase;
 import org.apache.druid.msq.util.MultiStageQueryContext;
+import org.apache.druid.query.DefaultGenericQueryMetricsFactory;
 import org.apache.druid.query.OrderBy;
+import org.apache.druid.query.QueryPlus;
+import org.apache.druid.query.QueryRunner;
 import org.apache.druid.query.aggregation.CountAggregatorFactory;
+import org.apache.druid.query.context.ResponseContext;
 import org.apache.druid.query.dimension.DefaultDimensionSpec;
 import org.apache.druid.query.groupby.GroupByQuery;
 import org.apache.druid.query.groupby.ResultRow;
 import org.apache.druid.query.scan.ScanQuery;
+import org.apache.druid.query.scan.ScanQueryQueryToolChest;
+import org.apache.druid.query.scan.ScanResultValue;
 import org.apache.druid.query.spec.MultipleIntervalSegmentSpec;
 import org.apache.druid.segment.column.ColumnType;
 import org.apache.druid.segment.column.RowSignature;
@@ -56,7 +64,10 @@ import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
+import java.util.List;
 import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doAnswer;
@@ -222,6 +233,102 @@ public class MSQLoadedSegmentTests extends MSQTestBase
         .verifyResults();
   }
 
+  @Test
+  public void testSelectWithLoadedSegmentsOnFooWithOrderByLimitOffset()
+  {
+    RowSignature resultSignature = RowSignature.builder()
+                                               .add("cnt", ColumnType.LONG)
+                                               .add("dim1", ColumnType.STRING)
+                                               .build();
+
+    stubDataServerScanResults(
+        ImmutableList.of("cnt", "dim1"),
+        ImmutableList.of(
+            ImmutableList.of(1L, "qwe"),
+            ImmutableList.of(1L, "tyu")
+        )
+    );
+
+    testSelectQuery()
+        .setSql("select cnt, dim1 from foo order by dim1 limit 2 offset 6")
+        .setExpectedMSQSpec(
+            LegacyMSQSpec.builder()
+                   .query(
+                       newScanQueryBuilder()
+                           .dataSource(CalciteTests.DATASOURCE1)
+                           .intervals(querySegmentSpec(Filtration.eternity()))
+                           .columns("cnt", "dim1")
+                           .columnTypes(resultSignature.getColumnTypes())
+                           
.orderBy(ImmutableList.of(OrderBy.ascending("dim1")))
+                           .offset(6)
+                           .limit(2)
+                           .context(REALTIME_QUERY_CTX)
+                           .build()
+                   )
+                   .columnMappings(ColumnMappings.identity(resultSignature))
+                   .tuningConfig(MSQTuningConfig.defaultConfig())
+                   .destination(TaskReportMSQDestination.INSTANCE)
+                   .build()
+        )
+        .setQueryContext(REALTIME_QUERY_CTX)
+        .setExpectedRowSignature(resultSignature)
+        .setExpectedResultRows(ImmutableList.of(
+            new Object[]{1L, "qwe"},
+            new Object[]{1L, "tyu"}
+        ))
+        .verifyResults();
+  }
+
+  @Test
+  public void testSelectWithLoadedSegmentsOnFooWithTimeOrderLimitOffset()
+  {
+    RowSignature resultSignature = RowSignature.builder()
+                                               .add("__time", ColumnType.LONG)
+                                               .add("cnt", ColumnType.LONG)
+                                               .add("dim1", ColumnType.STRING)
+                                               .build();
+
+    final long time1 = DateTimes.of("2003-01-01").getMillis();
+    final long time2 = DateTimes.of("2003-01-02").getMillis();
+
+    stubDataServerScanResults(
+        ImmutableList.of("__time", "cnt", "dim1"),
+        ImmutableList.of(
+            ImmutableList.of(time1, 1L, "qwe"),
+            ImmutableList.of(time2, 1L, "tyu")
+        )
+    );
+
+    testSelectQuery()
+        .setSql("select __time, cnt, dim1 from foo order by __time limit 2 
offset 6")
+        .setExpectedMSQSpec(
+            LegacyMSQSpec.builder()
+                   .query(
+                       newScanQueryBuilder()
+                           .dataSource(CalciteTests.DATASOURCE1)
+                           .intervals(querySegmentSpec(Filtration.eternity()))
+                           .columns("__time", "cnt", "dim1")
+                           .columnTypes(resultSignature.getColumnTypes())
+                           
.orderBy(ImmutableList.of(OrderBy.ascending("__time")))
+                           .offset(6)
+                           .limit(2)
+                           .context(REALTIME_QUERY_CTX)
+                           .build()
+                   )
+                   .columnMappings(ColumnMappings.identity(resultSignature))
+                   .tuningConfig(MSQTuningConfig.defaultConfig())
+                   .destination(TaskReportMSQDestination.INSTANCE)
+                   .build()
+        )
+        .setQueryContext(REALTIME_QUERY_CTX)
+        .setExpectedRowSignature(resultSignature)
+        .setExpectedResultRows(ImmutableList.of(
+            new Object[]{time1, 1L, "qwe"},
+            new Object[]{time2, 1L, "tyu"}
+        ))
+        .verifyResults();
+  }
+
   @Test
   public void testGroupByWithLoadedSegmentsOnFoo()
   {
@@ -394,4 +501,42 @@ public class MSQLoadedSegmentTests extends MSQTestBase
         .setExpectedExecutionErrorMatcher(ThrowableMatcher.of(ISE.class))
         .verifyExecutionError();
   }
+
+  /**
+   * Stubs {@link #dataServerQueryHandler} to serve scan queries from the 
given rows, passing them through
+   * {@link ScanQueryQueryToolChest#mergeResults} so that offset and limit are 
applied the way a realtime server
+   * applies them.
+   */
+  private void stubDataServerScanResults(final List<String> columns, final 
List<List<Object>> rows)
+  {
+    doAnswer(
+        invocationOnMock -> {
+          final ScanQuery query = invocationOnMock.getArgument(0);
+          final Function<Sequence<ScanResultValue>, Sequence<Object[]>> 
mappingFunction =
+              invocationOnMock.getArgument(2);
+
+          // One row per batch, since that is what the time-ordered merge 
expects.
+          final QueryRunner<ScanResultValue> baseRunner = (queryPlus, 
responseContext) -> Sequences.simple(
+              rows.stream()
+                  .map(row -> new ScanResultValue(null, columns, 
ImmutableList.of(row)))
+                  .collect(Collectors.toList())
+          );
+
+          final Sequence<ScanResultValue> results =
+              new 
ScanQueryQueryToolChest(DefaultGenericQueryMetricsFactory.instance())
+                  .mergeResults(baseRunner)
+                  .run(QueryPlus.wrap(query), ResponseContext.createEmpty());
+
+          return Futures.immediateFuture(
+              new DataServerQueryResult<>(
+                  
ImmutableList.of(Yielders.each(mappingFunction.apply(results))),
+                  ImmutableList.of(),
+                  "foo"
+              )
+          );
+        }
+    )
+        .when(dataServerQueryHandler)
+        .fetchRowsFromDataServer(any(), any(), any(), any());
+  }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to