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 df71642af99 fix: EmptyCursorHolder#canVectorize must depend on the 
CursorBuildSpec. (#20822)
df71642af99 is described below

commit df71642af99a559caec628a969b3b610ab83625e
Author: Gian Merlino <[email protected]>
AuthorDate: Mon Nov 2 21:41:31 2026 -0800

    fix: EmptyCursorHolder#canVectorize must depend on the CursorBuildSpec. 
(#20822)
    
    Previously, EmptyCursorHolder#canVectorize always returned true. However,
    the expectation for CursorHolders is that they should check the
    virtual columns and aggregators from the spec. Not doing so led to
    the vector path being taken for virtual columns and aggregators that
    are not able to vectorize.
---
 .../apache/druid/segment/ConcatenatingCursor.java  |  5 +-
 .../apache/druid/segment/EmptyCursorHolder.java    | 64 +++++++---------
 .../PartialQueryableIndexCursorFactory.java        |  4 +-
 .../druid/segment/QueryableIndexCursorFactory.java |  2 +-
 .../incremental/IncrementalIndexCursorFactory.java | 10 +--
 .../segment/vector/ConcatenatingVectorCursor.java  |  5 +-
 .../druid/segment/EmptyCursorHolderTest.java       | 86 ++++++++++++++++++----
 7 files changed, 111 insertions(+), 65 deletions(-)

diff --git 
a/processing/src/main/java/org/apache/druid/segment/ConcatenatingCursor.java 
b/processing/src/main/java/org/apache/druid/segment/ConcatenatingCursor.java
index 010dd0ca1e7..7111087cf99 100644
--- a/processing/src/main/java/org/apache/druid/segment/ConcatenatingCursor.java
+++ b/processing/src/main/java/org/apache/druid/segment/ConcatenatingCursor.java
@@ -21,6 +21,7 @@ package org.apache.druid.segment;
 
 import com.google.common.base.Supplier;
 import org.apache.druid.error.DruidException;
+import org.apache.druid.segment.column.RowSignature;
 import org.apache.druid.segment.projections.ClusteringColumnSelectorFactory;
 
 import javax.annotation.Nullable;
@@ -109,7 +110,9 @@ public final class ConcatenatingCursor implements Cursor
     currentCursor = null;
     if (!delegateSet) {
       wrapperFactory.setDelegate(
-          
EmptyCursorHolder.forSpec(CursorBuildSpec.FULL_SCAN).asCursor().getColumnSelectorFactory(),
+          EmptyCursorHolder.forSpec(CursorBuildSpec.FULL_SCAN, 
RowSignature.empty())
+                           .asCursor()
+                           .getColumnSelectorFactory(),
           clusteringValuesByGroup.get(holderSuppliers.size() - 1)
       );
       delegateSet = true;
diff --git 
a/processing/src/main/java/org/apache/druid/segment/EmptyCursorHolder.java 
b/processing/src/main/java/org/apache/druid/segment/EmptyCursorHolder.java
index fb3baf28d4d..620063ed7ed 100644
--- a/processing/src/main/java/org/apache/druid/segment/EmptyCursorHolder.java
+++ b/processing/src/main/java/org/apache/druid/segment/EmptyCursorHolder.java
@@ -20,9 +20,7 @@
 package org.apache.druid.segment;
 
 import org.apache.druid.error.DruidException;
-import org.apache.druid.query.Order;
 import org.apache.druid.query.OrderBy;
-import org.apache.druid.query.context.QueryContextParameters;
 import org.apache.druid.query.dimension.DimensionSpec;
 import org.apache.druid.segment.column.ColumnCapabilities;
 import org.apache.druid.segment.join.filter.AllNullColumnSelectorFactory;
@@ -36,46 +34,31 @@ import org.apache.druid.segment.vector.VectorObjectSelector;
 import org.apache.druid.segment.vector.VectorValueSelector;
 
 import javax.annotation.Nullable;
-import java.util.Collections;
 import java.util.List;
 
 /**
- * A {@link CursorHolder} that yields no rows. Its cursors are always done, 
but its selector factories hand out harmless
- * nil selectors and {@link #canVectorize()} reports true.
- * <p>
- * Useful wherever a segment can be proven to match no rows without scanning 
it. Use {@link #forSpec(CursorBuildSpec)}
- * to produce an empty cursor matching the preferred ordering of the {@link 
CursorBuildSpec}.
+ * A {@link CursorHolder} that yields no rows. Useful wherever a segment can 
be proven to match no rows without
+ * needing to scan it.
  */
 public final class EmptyCursorHolder implements CursorHolder
 {
-  private static final EmptyCursorHolder NO_ORDER = new 
EmptyCursorHolder(Collections.emptyList());
-  private static final EmptyCursorHolder TIME_ASCENDING = new 
EmptyCursorHolder(Cursors.ascendingTimeOrder());
-  private static final EmptyCursorHolder TIME_DESCENDING = new 
EmptyCursorHolder(Cursors.descendingTimeOrder());
-
   private static final ColumnSelectorFactory NIL_SELECTOR_FACTORY = new 
AllNullColumnSelectorFactory();
 
   /**
-   * An empty holder that advertises the spec's preferred ordering. A cursor 
with no rows trivially satisfies any
-   * ordering, so it can honestly claim whatever the query asked for. The two 
{@code __time} directions reuse cached
-   * singletons (the common case); any other non-empty preferred ordering gets 
a fresh instance, and an empty preferred
-   * ordering yields the unordered shared instance.
+   * An empty holder that advertises the spec's preferred ordering.
    */
-  public static EmptyCursorHolder forSpec(CursorBuildSpec spec)
+  public static EmptyCursorHolder forSpec(CursorBuildSpec spec, 
ColumnInspector inspector)
   {
-    final List<OrderBy> preferredOrdering = spec.getPreferredOrdering();
-    final Order timeOrder = Cursors.getTimeOrdering(preferredOrdering);
-    return switch (timeOrder) {
-      case ASCENDING -> TIME_ASCENDING;
-      case DESCENDING -> TIME_DESCENDING;
-      default -> preferredOrdering.isEmpty() ? NO_ORDER : new 
EmptyCursorHolder(preferredOrdering);
-    };
+    return new EmptyCursorHolder(spec, inspector);
   }
 
-  private final List<OrderBy> ordering;
+  private final CursorBuildSpec spec;
+  private final ColumnInspector inspector;
 
-  private EmptyCursorHolder(List<OrderBy> ordering)
+  private EmptyCursorHolder(CursorBuildSpec spec, ColumnInspector inspector)
   {
-    this.ordering = ordering;
+    this.spec = spec;
+    this.inspector = inspector;
   }
 
   @Override
@@ -121,16 +104,18 @@ public final class EmptyCursorHolder implements 
CursorHolder
   @Override
   public boolean canVectorize()
   {
-    return true;
+    return QueryableIndexCursorHolder.canVectorizeVirtualColumnsAndAggregators(
+        spec.getVirtualColumns(),
+        spec.getAggregators(),
+        spec.getQueryContext(),
+        inspector
+    );
   }
 
   @Override
   public VectorCursor asVectorCursor()
   {
-    // The cursor produces no vectors (always done, current size 0), so the 
max vector size it reports is never used
-    // to read data — the inspector and the nil selectors built from it are 
internally consistent at any size. We
-    // report the default rather than the query's requested vector size so 
this holder can stay a shared singleton.
-    final ReadableVectorInspector inspector = new ReadableVectorInspector()
+    final ReadableVectorInspector vectorInspector = new 
ReadableVectorInspector()
     {
       @Override
       public int getId()
@@ -141,7 +126,7 @@ public final class EmptyCursorHolder implements CursorHolder
       @Override
       public int getMaxVectorSize()
       {
-        return QueryContextParameters.VECTOR_SIZE.getLiteralDefault();
+        return spec.getQueryContext().getVectorSize();
       }
 
       @Override
@@ -155,13 +140,13 @@ public final class EmptyCursorHolder implements 
CursorHolder
       @Override
       public ReadableVectorInspector getReadableVectorInspector()
       {
-        return inspector;
+        return vectorInspector;
       }
 
       @Override
       public SingleValueDimensionVectorSelector 
makeSingleValueDimensionSelector(DimensionSpec dimensionSpec)
       {
-        return NilVectorSelector.create(inspector);
+        return NilVectorSelector.create(vectorInspector);
       }
 
       @Override
@@ -178,13 +163,13 @@ public final class EmptyCursorHolder implements 
CursorHolder
       @Override
       public VectorValueSelector makeValueSelector(String column)
       {
-        return NilVectorSelector.create(inspector);
+        return NilVectorSelector.create(vectorInspector);
       }
 
       @Override
       public VectorObjectSelector makeObjectSelector(String column)
       {
-        return NilVectorSelector.create(inspector);
+        return NilVectorSelector.create(vectorInspector);
       }
 
       @Nullable
@@ -221,7 +206,7 @@ public final class EmptyCursorHolder implements CursorHolder
       @Override
       public int getMaxVectorSize()
       {
-        return inspector.getMaxVectorSize();
+        return vectorInspector.getMaxVectorSize();
       }
 
       @Override
@@ -235,6 +220,7 @@ public final class EmptyCursorHolder implements CursorHolder
   @Override
   public List<OrderBy> getOrdering()
   {
-    return ordering;
+    // A cursor with no rows trivially satisfies any ordering, so it can claim 
whatever the query asked for.
+    return spec.getPreferredOrdering();
   }
 }
diff --git 
a/processing/src/main/java/org/apache/druid/segment/PartialQueryableIndexCursorFactory.java
 
b/processing/src/main/java/org/apache/druid/segment/PartialQueryableIndexCursorFactory.java
index 5e9e573d0e0..faab37ce673 100644
--- 
a/processing/src/main/java/org/apache/druid/segment/PartialQueryableIndexCursorFactory.java
+++ 
b/processing/src/main/java/org/apache/druid/segment/PartialQueryableIndexCursorFactory.java
@@ -125,7 +125,7 @@ public class PartialQueryableIndexCursorFactory implements 
CursorFactory
     // restricted: dispatch from the restricted plan rather than letting the 
delegate match over the whole segment
     final PartialQueryableIndex.CursorPrefetchPlan plan = 
index.planCursorPrefetch(spec, servableBundles);
     if (plan.bundles().isEmpty()) {
-      return EmptyCursorHolder.forSpec(spec);
+      return EmptyCursorHolder.forSpec(spec, delegate);
     }
     if (plan.clusterGroupPlan() != null) {
       return delegate.makeClusteredCursorHolder(spec, plan.clusterGroupPlan());
@@ -142,7 +142,7 @@ public class PartialQueryableIndexCursorFactory implements 
CursorFactory
     if (plan.bundles().isEmpty()) {
       // Filter rules out every cluster group, or nothing servable can answer 
the spec: no bundle to acquire, nothing
       // to download.
-      return AsyncCursorHolder.completed(EmptyCursorHolder.forSpec(spec));
+      return AsyncCursorHolder.completed(EmptyCursorHolder.forSpec(spec, 
delegate));
     }
     if (plan.clusterGroupPlan() != null) {
       // reuse the plan's cluster resolution
diff --git 
a/processing/src/main/java/org/apache/druid/segment/QueryableIndexCursorFactory.java
 
b/processing/src/main/java/org/apache/druid/segment/QueryableIndexCursorFactory.java
index 6632e2f1bb7..cfc27801d6f 100644
--- 
a/processing/src/main/java/org/apache/druid/segment/QueryableIndexCursorFactory.java
+++ 
b/processing/src/main/java/org/apache/druid/segment/QueryableIndexCursorFactory.java
@@ -127,7 +127,7 @@ public class QueryableIndexCursorFactory implements 
ResidentCursorFactory
   public CursorHolder makeClusteredCursorHolder(CursorBuildSpec spec, 
ClusterGroupQueryPlan plan)
   {
     if (plan.survivingGroups().isEmpty()) {
-      return EmptyCursorHolder.forSpec(spec);
+      return EmptyCursorHolder.forSpec(spec, this);
     }
 
     if (plan.survivingGroups().size() == 1) {
diff --git 
a/processing/src/main/java/org/apache/druid/segment/incremental/IncrementalIndexCursorFactory.java
 
b/processing/src/main/java/org/apache/druid/segment/incremental/IncrementalIndexCursorFactory.java
index e364fe546ed..40969abcb92 100644
--- 
a/processing/src/main/java/org/apache/druid/segment/incremental/IncrementalIndexCursorFactory.java
+++ 
b/processing/src/main/java/org/apache/druid/segment/incremental/IncrementalIndexCursorFactory.java
@@ -154,16 +154,16 @@ public class IncrementalIndexCursorFactory implements 
ResidentCursorFactory
     final ClusteredValueGroupsBaseTableSchema summary = 
clusteredBaseTable.toMetadataSchema();
     final ClusterGroupQueryPlan plan = 
Projections.planClusterGroupQuery(summary.getClusterGroups(), spec);
     final List<TableClusterGroupSpec> surviving = plan.survivingGroups();
+    // Capabilities come from the groups rather than the base index, since the 
base index's dimensions never see values
+    // computed by the cluster spec's virtual columns. Snapshot them like the 
per-group selector factories do.
+    final ColumnInspector allGroupsInspector =
+        column -> 
ColumnCapabilitiesImpl.snapshot(clusteredBaseTable.getColumnCapabilities(column),
 COERCE_LOGIC);
 
     if (surviving.isEmpty()) {
-      return EmptyCursorHolder.forSpec(spec);
+      return EmptyCursorHolder.forSpec(spec, allGroupsInspector);
     }
 
     final RowSignature clusteringColumns = summary.getClusteringColumns();
-    // Capabilities come from the groups rather than the base index, since the 
base index's dimensions never see values
-    // computed by the cluster spec's virtual columns. Snapshot them like the 
per-group selector factories do.
-    final ColumnInspector allGroupsInspector =
-        column -> 
ColumnCapabilitiesImpl.snapshot(clusteredBaseTable.getColumnCapabilities(column),
 COERCE_LOGIC);
     final List<Object[]> clusteringValuesByGroup = new 
ArrayList<>(surviving.size());
     final List<Supplier<CursorHolder>> holderSuppliers = new 
ArrayList<>(surviving.size());
     final Closer closer = Closer.create();
diff --git 
a/processing/src/main/java/org/apache/druid/segment/vector/ConcatenatingVectorCursor.java
 
b/processing/src/main/java/org/apache/druid/segment/vector/ConcatenatingVectorCursor.java
index cf71fb2be0d..17377fbd778 100644
--- 
a/processing/src/main/java/org/apache/druid/segment/vector/ConcatenatingVectorCursor.java
+++ 
b/processing/src/main/java/org/apache/druid/segment/vector/ConcatenatingVectorCursor.java
@@ -24,6 +24,7 @@ import org.apache.druid.error.DruidException;
 import org.apache.druid.segment.CursorBuildSpec;
 import org.apache.druid.segment.CursorHolder;
 import org.apache.druid.segment.EmptyCursorHolder;
+import org.apache.druid.segment.column.RowSignature;
 import 
org.apache.druid.segment.projections.ClusteringVectorColumnSelectorFactory;
 
 import java.util.List;
@@ -116,7 +117,9 @@ public final class ConcatenatingVectorCursor implements 
VectorCursor
     currentCursor = null;
     if (!delegateSet) {
       wrapperFactory.setDelegate(
-          
EmptyCursorHolder.forSpec(CursorBuildSpec.FULL_SCAN).asVectorCursor().getColumnSelectorFactory(),
+          EmptyCursorHolder.forSpec(CursorBuildSpec.FULL_SCAN, 
RowSignature.empty())
+                           .asVectorCursor()
+                           .getColumnSelectorFactory(),
           clusteringValuesByGroup.get(holderSuppliers.size() - 1)
       );
       delegateSet = true;
diff --git 
a/processing/src/test/java/org/apache/druid/segment/EmptyCursorHolderTest.java 
b/processing/src/test/java/org/apache/druid/segment/EmptyCursorHolderTest.java
index 7932cadcca8..e53d9f7eeb4 100644
--- 
a/processing/src/test/java/org/apache/druid/segment/EmptyCursorHolderTest.java
+++ 
b/processing/src/test/java/org/apache/druid/segment/EmptyCursorHolderTest.java
@@ -20,13 +20,27 @@
 package org.apache.druid.segment;
 
 import org.apache.druid.query.OrderBy;
+import org.apache.druid.query.QueryContext;
+import org.apache.druid.query.aggregation.CountAggregatorFactory;
+import org.apache.druid.query.aggregation.HistogramAggregatorFactory;
+import org.apache.druid.query.aggregation.LongSumAggregatorFactory;
+import org.apache.druid.query.context.QueryContextParameters;
+import org.apache.druid.query.expression.TestExprMacroTable;
+import org.apache.druid.segment.column.ColumnType;
+import org.apache.druid.segment.column.RowSignature;
+import org.apache.druid.segment.virtual.ExpressionVirtualColumn;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
 import java.util.List;
+import java.util.Map;
 
 class EmptyCursorHolderTest
 {
+  private static final RowSignature INSPECTOR = 
RowSignature.builder().add("x", ColumnType.LONG).build();
+  private static final VirtualColumns VECTORIZABLE_VIRTUAL_COLUMNS = 
VirtualColumns.create(
+      new ExpressionVirtualColumn("v", "x + 1", ColumnType.LONG, 
TestExprMacroTable.INSTANCE)
+  );
   private static final CursorBuildSpec ASC = CursorBuildSpec.builder()
                                                             
.setPreferredOrdering(Cursors.ascendingTimeOrder())
                                                             .build();
@@ -37,55 +51,95 @@ class EmptyCursorHolderTest
   @Test
   void testForTimeOrderAscendingAdvertisesAscendingTime()
   {
-    CursorHolder holder = EmptyCursorHolder.forSpec(ASC);
+    CursorHolder holder = EmptyCursorHolder.forSpec(ASC, INSPECTOR);
     Assertions.assertEquals(
         Cursors.ascendingTimeOrder(),
         holder.getOrdering()
     );
-    Assertions.assertSame(EmptyCursorHolder.forSpec(ASC), holder);
   }
 
   @Test
   void testForTimeOrderDescendingAdvertisesDescendingTime()
   {
-    CursorHolder holder = EmptyCursorHolder.forSpec(DESC);
+    CursorHolder holder = EmptyCursorHolder.forSpec(DESC, INSPECTOR);
     Assertions.assertEquals(
         Cursors.descendingTimeOrder(),
         holder.getOrdering()
     );
-    Assertions.assertSame(EmptyCursorHolder.forSpec(DESC), holder);
   }
 
   @Test
-  void testForTimeOrderNoneReturnsUnorderedInstance()
+  void testForTimeOrderNoneAdvertisesNoOrdering()
   {
-    CursorHolder holder = EmptyCursorHolder.forSpec(CursorBuildSpec.FULL_SCAN);
+    CursorHolder holder = EmptyCursorHolder.forSpec(CursorBuildSpec.FULL_SCAN, 
INSPECTOR);
     Assertions.assertEquals(
         List.of(),
         holder.getOrdering()
     );
-    
Assertions.assertSame(EmptyCursorHolder.forSpec(CursorBuildSpec.FULL_SCAN), 
holder);
   }
 
   @Test
   void testForSpecNonTimeOrderingAdvertisedVerbatim()
   {
-    // preferredOrdering decouples from assumed __time ordering; an empty 
cursor trivially satisfies any ordering, so
-    // forSpec advertises a non-__time preferred ordering verbatim via a 
fresh, uncached instance.
+    // An empty cursor trivially satisfies any ordering, so a non-__time 
preferred ordering is advertised verbatim.
     final List<OrderBy> ordering = List.of(OrderBy.ascending("dim"));
     final CursorBuildSpec spec = 
CursorBuildSpec.builder().setPreferredOrdering(ordering).build();
-    final CursorHolder holder = EmptyCursorHolder.forSpec(spec);
+    final CursorHolder holder = EmptyCursorHolder.forSpec(spec, INSPECTOR);
     Assertions.assertEquals(ordering, holder.getOrdering());
-    // Not the unordered singleton, and arbitrary orderings are not cached (a 
fresh instance each call).
-    Assertions.assertNotSame(holder, 
EmptyCursorHolder.forSpec(CursorBuildSpec.FULL_SCAN));
-    Assertions.assertNotSame(holder, EmptyCursorHolder.forSpec(spec));
   }
 
   @Test
   void testCursorIsAlwaysDone()
   {
-    Assertions.assertTrue(EmptyCursorHolder.forSpec(ASC).asCursor().isDone());
-    Assertions.assertTrue(EmptyCursorHolder.forSpec(DESC).asCursor().isDone());
-    
Assertions.assertTrue(EmptyCursorHolder.forSpec(CursorBuildSpec.FULL_SCAN).asCursor().isDone());
+    Assertions.assertTrue(EmptyCursorHolder.forSpec(ASC, 
INSPECTOR).asCursor().isDone());
+    Assertions.assertTrue(EmptyCursorHolder.forSpec(DESC, 
INSPECTOR).asCursor().isDone());
+    Assertions.assertTrue(EmptyCursorHolder.forSpec(CursorBuildSpec.FULL_SCAN, 
INSPECTOR).asCursor().isDone());
+  }
+
+  @Test
+  void testCanVectorizeWithoutVirtualColumnsOrAggregators()
+  {
+    Assertions.assertTrue(EmptyCursorHolder.forSpec(CursorBuildSpec.FULL_SCAN, 
INSPECTOR).canVectorize());
+    Assertions.assertTrue(EmptyCursorHolder.forSpec(ASC, 
INSPECTOR).canVectorize());
+    Assertions.assertTrue(EmptyCursorHolder.forSpec(DESC, 
INSPECTOR).canVectorize());
+  }
+
+  @Test
+  void testCanVectorizeWithVectorizableVirtualColumnsAndAggregators()
+  {
+    final CursorBuildSpec spec =
+        CursorBuildSpec.builder(ASC)
+                       .setVirtualColumns(VECTORIZABLE_VIRTUAL_COLUMNS)
+                       .setAggregators(List.of(new 
CountAggregatorFactory("c"), new LongSumAggregatorFactory("s", "v")))
+                       .build();
+    final CursorHolder holder = EmptyCursorHolder.forSpec(spec, INSPECTOR);
+    Assertions.assertTrue(holder.canVectorize());
+    Assertions.assertEquals(Cursors.ascendingTimeOrder(), 
holder.getOrdering());
+  }
+
+  @Test
+  void testCannotVectorizeWithNonVectorizableAggregator()
+  {
+    final CursorBuildSpec spec =
+        CursorBuildSpec.builder(DESC)
+                       .setAggregators(List.of(new 
HistogramAggregatorFactory("h", "x", List.of(1f))))
+                       .build();
+    final CursorHolder holder = EmptyCursorHolder.forSpec(spec, INSPECTOR);
+    Assertions.assertFalse(holder.canVectorize());
+    Assertions.assertEquals(Cursors.descendingTimeOrder(), 
holder.getOrdering());
+    Assertions.assertTrue(holder.asCursor().isDone());
+  }
+
+  @Test
+  void testCannotVectorizeWhenVirtualColumnVectorizationDisabled()
+  {
+    final CursorBuildSpec spec =
+        CursorBuildSpec.builder()
+                       .setVirtualColumns(VECTORIZABLE_VIRTUAL_COLUMNS)
+                       .setQueryContext(
+                           
QueryContext.of(Map.of(QueryContextParameters.VECTORIZE_VIRTUAL_COLUMNS.getName(),
 "false"))
+                       )
+                       .build();
+    Assertions.assertFalse(EmptyCursorHolder.forSpec(spec, 
INSPECTOR).canVectorize());
   }
 }


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

Reply via email to