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

clintropolis 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 f7543426da6 fix: constant columns for clustered segment (#19675)
f7543426da6 is described below

commit f7543426da6cb8c1d2c575e1d2229ccbb635a560
Author: Clint Wylie <[email protected]>
AuthorDate: Mon Jul 13 16:56:38 2026 -0700

    fix: constant columns for clustered segment (#19675)
    
    changes:
    * `QueryableIndex#getClusterGroupQueryableIndex` now accepts a boolean flag 
to control whether or not constant columns are created for the clustering 
columns (true for queries, false for persist/merge)
    * these constants resolve some issues around filtering clustered segments
---
 .../druid/segment/PartialQueryableIndex.java       |  20 ++-
 .../PartialQueryableIndexCursorFactory.java        |   2 +-
 .../org/apache/druid/segment/QueryableIndex.java   |   9 +-
 .../druid/segment/QueryableIndexCursorFactory.java |  36 +----
 .../segment/QueryableIndexIndexableAdapter.java    |   3 +-
 .../apache/druid/segment/SimpleQueryableIndex.java |  21 ++-
 .../druid/segment/column/ConstantColumns.java      | 110 +++++++++++++
 .../segment/column/ConstantNumericColumn.java      | 151 ++++++++++++++++++
 .../druid/segment/data/ConstantColumnarInts.java   |  70 +++++++++
 .../druid/segment/data/ConstantUtf8Indexed.java    |  96 ++++++++++++
 ...SingleGroupClusteringColumnSelectorFactory.java | 172 ---------------------
 ...GroupClusteringVectorColumnSelectorFactory.java | 143 -----------------
 .../druid/segment/IndexMergerV10ClusteredTest.java |   8 +-
 .../QueryableIndexCursorFactoryClusteredTest.java  |  60 ++++++-
 .../segment/column/ConstantNumericColumnTest.java  |  74 +++++++++
 .../segment/data/ConstantUtf8IndexedTest.java      |  73 +++++++++
 ...leGroupClusteringColumnSelectorFactoryTest.java | 143 -----------------
 17 files changed, 687 insertions(+), 504 deletions(-)

diff --git 
a/processing/src/main/java/org/apache/druid/segment/PartialQueryableIndex.java 
b/processing/src/main/java/org/apache/druid/segment/PartialQueryableIndex.java
index cfe5f6f4d68..364879d0361 100644
--- 
a/processing/src/main/java/org/apache/druid/segment/PartialQueryableIndex.java
+++ 
b/processing/src/main/java/org/apache/druid/segment/PartialQueryableIndex.java
@@ -35,6 +35,7 @@ import org.apache.druid.segment.column.ColumnConfig;
 import org.apache.druid.segment.column.ColumnDescriptor;
 import org.apache.druid.segment.column.ColumnHolder;
 import org.apache.druid.segment.column.ColumnType;
+import org.apache.druid.segment.column.ConstantColumns;
 import org.apache.druid.segment.column.ValueType;
 import org.apache.druid.segment.data.Indexed;
 import org.apache.druid.segment.data.ListIndexed;
@@ -55,6 +56,7 @@ import javax.annotation.Nullable;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
@@ -456,7 +458,7 @@ public class PartialQueryableIndex implements QueryableIndex
    * cursor-factory level via {@code ClusteringColumnSelectorFactory}.
    */
   @Override
-  public QueryableIndex getClusterGroupQueryableIndex(TableClusterGroupSpec 
groupSpec)
+  public QueryableIndex getClusterGroupQueryableIndex(TableClusterGroupSpec 
groupSpec, boolean withClusteringColumns)
   {
     if (clusteredBaseSummary == null) {
       throw DruidException.defensive("getClusterGroupQueryableIndex called on 
a non-clustered segment");
@@ -466,7 +468,7 @@ public class PartialQueryableIndex implements QueryableIndex
     if (groupIndex < 0) {
       throw DruidException.defensive("Cluster group spec is not part of this 
segment");
     }
-    final Map<String, Supplier<BaseColumnHolder>> groupColumns = 
clusterGroupColumnsByIndex.computeIfAbsent(
+    final Map<String, Supplier<BaseColumnHolder>> baseColumns = 
clusterGroupColumnsByIndex.computeIfAbsent(
         groupIndex,
         i -> buildColumnSuppliers(
             clusteredBaseSummary.getTimeColumnName(),
@@ -476,6 +478,20 @@ public class PartialQueryableIndex implements 
QueryableIndex
             Map.of()
         )
     );
+
+    final Map<String, Supplier<BaseColumnHolder>> groupColumns;
+    if (withClusteringColumns) {
+      groupColumns = new HashMap<>(baseColumns);
+      ConstantColumns.addConstantClusteringColumns(
+          groupColumns,
+          clusteredBaseSummary.getClusteringColumns(),
+          groupSpec.lookupClusteringValues(),
+          groupSpec.getNumRows(),
+          bitmapFactory
+      );
+    } else {
+      groupColumns = baseColumns;
+    }
     final Metadata groupMetadata = new Metadata(
         null,
         null,
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 3704c055ff2..04d4f02869b 100644
--- 
a/processing/src/main/java/org/apache/druid/segment/PartialQueryableIndexCursorFactory.java
+++ 
b/processing/src/main/java/org/apache/druid/segment/PartialQueryableIndexCursorFactory.java
@@ -137,7 +137,7 @@ public class PartialQueryableIndexCursorFactory implements 
CursorFactory
 
     final List<DownloadBundle> bundles = new ArrayList<>(surviving.size());
     for (TableClusterGroupSpec group : surviving) {
-      final QueryableIndex groupIndex = 
index.getClusterGroupQueryableIndex(group);
+      final QueryableIndex groupIndex = 
index.getClusterGroupQueryableIndex(group, true);
       final CursorBuildSpec groupSpec = plan.rebuildCursorBuildSpec(spec, 
group);
       bundles.add(
           new DownloadBundle(
diff --git 
a/processing/src/main/java/org/apache/druid/segment/QueryableIndex.java 
b/processing/src/main/java/org/apache/druid/segment/QueryableIndex.java
index 6f604fbb18c..a7c2c04d771 100644
--- a/processing/src/main/java/org/apache/druid/segment/QueryableIndex.java
+++ b/processing/src/main/java/org/apache/druid/segment/QueryableIndex.java
@@ -135,12 +135,15 @@ public interface QueryableIndex extends Closeable, 
ColumnInspector
   }
 
   /**
-   * Returns a {@link QueryableIndex} sub-view scoped to a single cluster 
group's column data. Mirrors
-   * {@link #getProjectionQueryableIndex(String)} but for cluster groups, 
addressed by reference rather than name.
+   * Returns a {@link QueryableIndex} sub-view scoped to a single cluster 
group's column data.
    * Default returns {@code null}; only clustered segments override.
+   * <p>
+   * Clustering columns are constant within a group. When {@code 
withClusteringColumns} is true (query paths) they are
+   * additionally exposed as constant columns so filters and selectors can 
resolve them; when false
+   * (the merge/persist path) they are omitted (since they are stored as 
metadata instead of actual columns).
    */
   @Nullable
-  default QueryableIndex getClusterGroupQueryableIndex(TableClusterGroupSpec 
groupSpec)
+  default QueryableIndex getClusterGroupQueryableIndex(TableClusterGroupSpec 
groupSpec, boolean withClusteringColumns)
   {
     return null;
   }
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 1803554dc56..a29b2b57072 100644
--- 
a/processing/src/main/java/org/apache/druid/segment/QueryableIndexCursorFactory.java
+++ 
b/processing/src/main/java/org/apache/druid/segment/QueryableIndexCursorFactory.java
@@ -38,8 +38,6 @@ import 
org.apache.druid.segment.projections.ClusteringColumnSelectorFactory;
 import 
org.apache.druid.segment.projections.ClusteringVectorColumnSelectorFactory;
 import org.apache.druid.segment.projections.Projections;
 import org.apache.druid.segment.projections.QueryableProjection;
-import 
org.apache.druid.segment.projections.SingleGroupClusteringColumnSelectorFactory;
-import 
org.apache.druid.segment.projections.SingleGroupClusteringVectorColumnSelectorFactory;
 import org.apache.druid.segment.projections.TableClusterGroupSpec;
 import org.apache.druid.segment.vector.ConcatenatingVectorCursor;
 import org.apache.druid.segment.vector.MultiValueDimensionVectorSelector;
@@ -194,7 +192,7 @@ public class QueryableIndexCursorFactory implements 
ResidentCursorFactory
       TableClusterGroupSpec valueGroup
   )
   {
-    final QueryableIndex groupIndex = 
index.getClusterGroupQueryableIndex(valueGroup);
+    final QueryableIndex groupIndex = 
index.getClusterGroupQueryableIndex(valueGroup, true);
     if (groupIndex == null) {
       throw DruidException.defensive(
           "No cluster-group sub-index resolvable for clustering values "
@@ -202,39 +200,13 @@ public class QueryableIndexCursorFactory implements 
ResidentCursorFactory
       );
     }
 
+    // groupIndex exposes the group's clustering columns as constant columns, 
no selector wrapper is needed
     return new QueryableIndexCursorHolder(
         groupIndex,
         plan.rebuildCursorBuildSpec(spec, valueGroup),
         QueryableIndexTimeBoundaryInspector.create(groupIndex),
         valueGroup.getSummary().getOrdering()
-    )
-    {
-      @Override
-      protected ColumnSelectorFactory makeColumnSelectorFactoryForOffset(
-          ColumnCache columnCache,
-          Offset baseOffset
-      )
-      {
-        return new SingleGroupClusteringColumnSelectorFactory(
-            super.makeColumnSelectorFactoryForOffset(columnCache, baseOffset),
-            valueGroup.getSummary().getClusteringColumns(),
-            valueGroup.lookupClusteringValues()
-        );
-      }
-
-      @Override
-      protected VectorColumnSelectorFactory 
makeVectorColumnSelectorFactoryForOffset(
-          ColumnCache columnCache,
-          VectorOffset baseOffset
-      )
-      {
-        return new SingleGroupClusteringVectorColumnSelectorFactory(
-            super.makeVectorColumnSelectorFactoryForOffset(columnCache, 
baseOffset),
-            valueGroup.getSummary().getClusteringColumns(),
-            valueGroup.lookupClusteringValues()
-        );
-      }
-    };
+    );
   }
 
   /**
@@ -258,7 +230,7 @@ public class QueryableIndexCursorFactory implements 
ResidentCursorFactory
     final Closer closer = Closer.create();
     for (TableClusterGroupSpec valueGroup : matching) {
       clusteringValuesByGroup.add(valueGroup.lookupClusteringValues());
-      final QueryableIndex groupIndex = 
index.getClusterGroupQueryableIndex(valueGroup);
+      final QueryableIndex groupIndex = 
index.getClusterGroupQueryableIndex(valueGroup, true);
       if (groupIndex == null) {
         throw DruidException.defensive(
             "No cluster-group sub-index resolvable for clustering values "
diff --git 
a/processing/src/main/java/org/apache/druid/segment/QueryableIndexIndexableAdapter.java
 
b/processing/src/main/java/org/apache/druid/segment/QueryableIndexIndexableAdapter.java
index b33ed50de77..38bf7599222 100644
--- 
a/processing/src/main/java/org/apache/druid/segment/QueryableIndexIndexableAdapter.java
+++ 
b/processing/src/main/java/org/apache/druid/segment/QueryableIndexIndexableAdapter.java
@@ -287,7 +287,8 @@ public class QueryableIndexIndexableAdapter implements 
IndexableAdapter
   @Override
   public IndexableAdapter getClusterGroupAdapter(TableClusterGroupSpec spec)
   {
-    final QueryableIndex groupIndex = 
input.getClusterGroupQueryableIndex(spec);
+    // Merge/persist must not write clustering columns into the per-group 
files, so omit them here.
+    final QueryableIndex groupIndex = 
input.getClusterGroupQueryableIndex(spec, false);
     DruidException.conditionalDefensive(groupIndex != null, "Cluster group 
spec [%s] was not found", spec);
     return new QueryableIndexIndexableAdapter(groupIndex);
   }
diff --git 
a/processing/src/main/java/org/apache/druid/segment/SimpleQueryableIndex.java 
b/processing/src/main/java/org/apache/druid/segment/SimpleQueryableIndex.java
index 6c7c37bf7a1..065a6493232 100644
--- 
a/processing/src/main/java/org/apache/druid/segment/SimpleQueryableIndex.java
+++ 
b/processing/src/main/java/org/apache/druid/segment/SimpleQueryableIndex.java
@@ -36,6 +36,7 @@ import org.apache.druid.segment.column.ColumnCapabilities;
 import org.apache.druid.segment.column.ColumnCapabilitiesImpl;
 import org.apache.druid.segment.column.ColumnHolder;
 import org.apache.druid.segment.column.ColumnType;
+import org.apache.druid.segment.column.ConstantColumns;
 import org.apache.druid.segment.column.ValueType;
 import org.apache.druid.segment.data.Indexed;
 import org.apache.druid.segment.data.ListIndexed;
@@ -48,6 +49,7 @@ import org.joda.time.Interval;
 
 import javax.annotation.Nullable;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
@@ -234,7 +236,7 @@ public abstract class SimpleQueryableIndex implements 
QueryableIndex
     if (groups.isEmpty()) {
       return null;
     }
-    final QueryableIndex firstGroupIndex = 
getClusterGroupQueryableIndex(groups.get(0));
+    final QueryableIndex firstGroupIndex = 
getClusterGroupQueryableIndex(groups.get(0), false);
     return firstGroupIndex == null ? null : 
firstGroupIndex.getColumnCapabilities(column);
   }
 
@@ -273,7 +275,7 @@ public abstract class SimpleQueryableIndex implements 
QueryableIndex
    * the returned index, they're injected at the cursor-factory level via 
{@code ClusteringColumnSelectorFactory}.
    */
   @Override
-  public QueryableIndex getClusterGroupQueryableIndex(TableClusterGroupSpec 
groupSpec)
+  public QueryableIndex getClusterGroupQueryableIndex(TableClusterGroupSpec 
groupSpec, boolean withClusteringColumns)
   {
     if (clusteredBaseSummary == null) {
       throw DruidException.defensive("getClusterGroupQueryableIndex called on 
a non-clustered segment");
@@ -283,7 +285,20 @@ public abstract class SimpleQueryableIndex implements 
QueryableIndex
     if (index < 0) {
       throw DruidException.defensive("Cluster group spec is not part of this 
segment");
     }
-    final Map<String, Supplier<BaseColumnHolder>> groupColumns = 
clusterGroupColumns.get(index);
+    // add clustering columns are constants for query paths
+    final Map<String, Supplier<BaseColumnHolder>> groupColumns;
+    if (withClusteringColumns) {
+      groupColumns = new HashMap<>(clusterGroupColumns.get(index));
+      ConstantColumns.addConstantClusteringColumns(
+          groupColumns,
+          clusteredBaseSummary.getClusteringColumns(),
+          groupSpec.lookupClusteringValues(),
+          groupSpec.getNumRows(),
+          bitmapFactory
+      );
+    } else {
+      groupColumns = clusterGroupColumns.get(index);
+    }
     final Metadata groupMetadata = new Metadata(
         null,
         null,
diff --git 
a/processing/src/main/java/org/apache/druid/segment/column/ConstantColumns.java 
b/processing/src/main/java/org/apache/druid/segment/column/ConstantColumns.java
new file mode 100644
index 00000000000..e2e495e70af
--- /dev/null
+++ 
b/processing/src/main/java/org/apache/druid/segment/column/ConstantColumns.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.segment.column;
+
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
+import org.apache.druid.collections.bitmap.BitmapFactory;
+import org.apache.druid.error.DruidException;
+import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.segment.data.ConstantColumnarInts;
+import org.apache.druid.segment.data.ConstantUtf8Indexed;
+
+import javax.annotation.Nullable;
+import java.nio.ByteBuffer;
+import java.util.Map;
+
+/**
+ * Fabricates in-memory {@link BaseColumnHolder}s for columns whose value is 
constant across every row, without any
+ * on-disk data. This is the runtime analogue of {@link 
org.apache.druid.segment.serde.NullColumnPartSerde} for
+ * arbitrary constants: a STRING constant is presented as a single-value 
{@link StringUtf8DictionaryEncodedColumn}
+ * (backed by a one-entry dictionary), and numeric constants as a {@link 
ConstantNumericColumn}.
+ */
+public final class ConstantColumns
+{
+  private ConstantColumns()
+  {
+    // no instantiation
+  }
+
+  /**
+   * Add a constant column (via {@link #makeConstantColumnHolder}) to {@code 
target} for each column in
+   * {@code clusteringColumns}, using the group-constant value at the matching 
position of {@code clusteringValues}.
+   */
+  public static void addConstantClusteringColumns(
+      Map<String, Supplier<BaseColumnHolder>> target,
+      RowSignature clusteringColumns,
+      Object[] clusteringValues,
+      int numRows,
+      BitmapFactory bitmapFactory
+  )
+  {
+    DruidException.conditionalDefensive(
+        clusteringValues.length == clusteringColumns.size(),
+        "clusteringValues length [%s] must match clusteringColumns size [%s]",
+        clusteringValues.length,
+        clusteringColumns.size()
+    );
+    for (int i = 0; i < clusteringColumns.size(); i++) {
+      final String columnName = clusteringColumns.getColumnName(i);
+      final ColumnType columnType = clusteringColumns.getColumnType(i)
+                                                     .orElseThrow(() -> 
DruidException.defensive(
+                                                         "clustering column 
[%s] is missing a type",
+                                                         columnName
+                                                     ));
+      final BaseColumnHolder holder = makeConstantColumnHolder(columnType, 
clusteringValues[i], numRows, bitmapFactory);
+      target.put(columnName, Suppliers.ofInstance(holder));
+    }
+  }
+
+  public static BaseColumnHolder makeConstantColumnHolder(
+      ColumnType type,
+      @Nullable Object value,
+      int numRows,
+      BitmapFactory bitmapFactory
+  )
+  {
+    final ColumnBuilder builder = new ColumnBuilder()
+        .setType(type)
+        .setHasMultipleValues(false)
+        .setHasNulls(value == null);
+
+    if (type.is(ValueType.STRING)) {
+      final ByteBuffer utf8 = value == null ? null : 
StringUtils.toUtf8ByteBuffer((String) value);
+      builder.setDictionaryEncodedColumnSupplier(
+          Suppliers.ofInstance(
+              new StringUtf8DictionaryEncodedColumn(
+                  new ConstantColumnarInts(numRows, 0),
+                  null,
+                  new ConstantUtf8Indexed(utf8),
+                  bitmapFactory
+              )
+          )
+      );
+    } else if (type.isNumeric()) {
+      builder.setNumericColumnSupplier(
+          Suppliers.ofInstance(new ConstantNumericColumn(type, (Number) value, 
numRows))
+      );
+    } else {
+      throw DruidException.defensive("Cannot build a constant column for 
type[%s]", type);
+    }
+    return builder.build();
+  }
+}
diff --git 
a/processing/src/main/java/org/apache/druid/segment/column/ConstantNumericColumn.java
 
b/processing/src/main/java/org/apache/druid/segment/column/ConstantNumericColumn.java
new file mode 100644
index 00000000000..e17633c3e41
--- /dev/null
+++ 
b/processing/src/main/java/org/apache/druid/segment/column/ConstantNumericColumn.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.segment.column;
+
+import org.apache.druid.query.monomorphicprocessing.RuntimeShapeInspector;
+import org.apache.druid.segment.ColumnValueSelector;
+import org.apache.druid.segment.data.ReadableOffset;
+import org.apache.druid.segment.vector.ConstantVectorSelectors;
+import org.apache.druid.segment.vector.ReadableVectorOffset;
+import org.apache.druid.segment.vector.VectorObjectSelector;
+import org.apache.druid.segment.vector.VectorValueSelector;
+
+import javax.annotation.Nullable;
+
+/**
+ * A numeric {@link NumericColumn} of a fixed length whose value is the same 
for every row. Vector selectors are backed
+ * by the shared {@link ConstantVectorSelectors} helpers; the scalar selector 
returns the boxed value directly. Used to
+ * fabricate a per-group clustering column for a clustered base table 
(constant within the group), but is not otherwise
+ * specific to clustered segments.
+ */
+public final class ConstantNumericColumn implements NumericColumn
+{
+  private final ColumnType type;
+  @Nullable
+  private final Number value;
+  private final int numRows;
+
+  public ConstantNumericColumn(ColumnType type, @Nullable Number value, int 
numRows)
+  {
+    this.type = type;
+    this.value = value;
+    this.numRows = numRows;
+  }
+
+  @Override
+  public int length()
+  {
+    return numRows;
+  }
+
+  @Override
+  public long getLongSingleValueRow(int rowNum)
+  {
+    return value == null ? 0L : value.longValue();
+  }
+
+  @Override
+  public ColumnValueSelector<?> makeColumnValueSelector(ReadableOffset offset)
+  {
+    return new ConstantNumericValueSelector(value);
+  }
+
+  @Override
+  public VectorValueSelector makeVectorValueSelector(ReadableVectorOffset 
offset)
+  {
+    return ConstantVectorSelectors.vectorValueSelector(offset, value);
+  }
+
+  @Override
+  public VectorObjectSelector makeVectorObjectSelector(ReadableVectorOffset 
offset)
+  {
+    return ConstantVectorSelectors.vectorObjectSelector(offset, value);
+  }
+
+  @Override
+  public void inspectRuntimeShape(RuntimeShapeInspector inspector)
+  {
+    inspector.visit("type", type.asTypeString());
+    inspector.visit("value", String.valueOf(value));
+  }
+
+  @Override
+  public void close()
+  {
+    // nothing to close
+  }
+
+  /**
+   * Constant numeric {@link ColumnValueSelector}. {@link #getObject()} 
returns the boxed value (or null)
+   */
+  private static final class ConstantNumericValueSelector implements 
ColumnValueSelector<Object>
+  {
+    @Nullable
+    private final Number value;
+
+    private ConstantNumericValueSelector(@Nullable Number value)
+    {
+      this.value = value;
+    }
+
+    @Override
+    public double getDouble()
+    {
+      return value == null ? 0d : value.doubleValue();
+    }
+
+    @Override
+    public float getFloat()
+    {
+      return value == null ? 0f : value.floatValue();
+    }
+
+    @Override
+    public long getLong()
+    {
+      return value == null ? 0L : value.longValue();
+    }
+
+    @Override
+    public boolean isNull()
+    {
+      return value == null;
+    }
+
+    @Nullable
+    @Override
+    public Object getObject()
+    {
+      return value;
+    }
+
+    @Override
+    public Class<Object> classOfObject()
+    {
+      return Object.class;
+    }
+
+    @Override
+    public void inspectRuntimeShape(RuntimeShapeInspector inspector)
+    {
+      inspector.visit("value", String.valueOf(value));
+    }
+  }
+}
diff --git 
a/processing/src/main/java/org/apache/druid/segment/data/ConstantColumnarInts.java
 
b/processing/src/main/java/org/apache/druid/segment/data/ConstantColumnarInts.java
new file mode 100644
index 00000000000..3e36fb5796a
--- /dev/null
+++ 
b/processing/src/main/java/org/apache/druid/segment/data/ConstantColumnarInts.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.segment.data;
+
+import org.apache.druid.query.monomorphicprocessing.RuntimeShapeInspector;
+
+import java.util.Arrays;
+
+/**
+ * A {@link ColumnarInts} of a fixed length that returns the same dictionary 
id for every row. Used to back a
+ * single-value dictionary-encoded column whose value is constant across all 
rows.
+ */
+public final class ConstantColumnarInts implements ColumnarInts
+{
+  private final int numRows;
+  private final int value;
+
+  public ConstantColumnarInts(int numRows, int value)
+  {
+    this.numRows = numRows;
+    this.value = value;
+  }
+
+  @Override
+  public int size()
+  {
+    return numRows;
+  }
+
+  @Override
+  public int get(int index)
+  {
+    return value;
+  }
+
+  @Override
+  public void get(int[] out, int offset, int start, int length)
+  {
+    Arrays.fill(out, offset, offset + length, value);
+  }
+
+  @Override
+  public void inspectRuntimeShape(RuntimeShapeInspector inspector)
+  {
+    inspector.visit("value", value);
+  }
+
+  @Override
+  public void close()
+  {
+    // nothing to close
+  }
+}
diff --git 
a/processing/src/main/java/org/apache/druid/segment/data/ConstantUtf8Indexed.java
 
b/processing/src/main/java/org/apache/druid/segment/data/ConstantUtf8Indexed.java
new file mode 100644
index 00000000000..e2ec1951e48
--- /dev/null
+++ 
b/processing/src/main/java/org/apache/druid/segment/data/ConstantUtf8Indexed.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.segment.data;
+
+import org.apache.druid.error.DruidException;
+import org.apache.druid.java.util.common.ByteBufferUtils;
+import org.apache.druid.query.monomorphicprocessing.RuntimeShapeInspector;
+
+import javax.annotation.Nullable;
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.Iterator;
+
+/**
+ * A single-entry {@link Indexed} of {@link ByteBuffer}, far lighter than a 
{@link GenericIndexed} for the degenerate
+ * one-value case (e.g. a constant column's dictionary). {@link #get} returns 
a fresh {@link ByteBuffer#duplicate()}
+ * each call so callers that advance the buffer's position while decoding 
(e.g. {@link
+ * org.apache.druid.java.util.common.StringUtils#fromUtf8(ByteBuffer)}) never 
corrupt the shared value or race with
+ * concurrent readers. The single value is treated as immutable: its 
position/limit are never mutated by this class.
+ */
+public final class ConstantUtf8Indexed implements Indexed<ByteBuffer>
+{
+  @Nullable
+  private final ByteBuffer value;
+
+  public ConstantUtf8Indexed(@Nullable ByteBuffer value)
+  {
+    this.value = value;
+  }
+
+  @Override
+  public int size()
+  {
+    return 1;
+  }
+
+  @Nullable
+  @Override
+  public ByteBuffer get(int index)
+  {
+    if (index != 0) {
+      throw DruidException.defensive("index[%s] out of bounds for a 
single-value dictionary", index);
+    }
+    return value == null ? null : value.duplicate();
+  }
+
+  @Override
+  public int indexOf(@Nullable ByteBuffer other)
+  {
+    // utf8Comparator handles nulls (nulls first, matching the dictionary sort 
order) and reads via absolute indexing
+    // without advancing positions, so the shared value can be passed directly.
+    final int comparison = ByteBufferUtils.utf8Comparator().compare(value, 
other);
+    if (comparison == 0) {
+      return 0;
+    }
+    // Single-entry sorted lookup: mirror GenericIndexed#indexOf's not-found 
encoding of -(insertionPoint) - 1.
+    // value < other -> other would insert after it (point 1 -> -2); value > 
other -> before it (point 0 -> -1).
+    return comparison < 0 ? -2 : -1;
+  }
+
+  @Override
+  public Iterator<ByteBuffer> iterator()
+  {
+    return Collections.singletonList(value == null ? null : 
value.duplicate()).iterator();
+  }
+
+  @Override
+  public boolean isSorted()
+  {
+    // A single-entry dictionary is trivially sorted (and unique).
+    return true;
+  }
+
+  @Override
+  public void inspectRuntimeShape(RuntimeShapeInspector inspector)
+  {
+    inspector.visit("value", value);
+  }
+}
diff --git 
a/processing/src/main/java/org/apache/druid/segment/projections/SingleGroupClusteringColumnSelectorFactory.java
 
b/processing/src/main/java/org/apache/druid/segment/projections/SingleGroupClusteringColumnSelectorFactory.java
deleted file mode 100644
index aada6b78816..00000000000
--- 
a/processing/src/main/java/org/apache/druid/segment/projections/SingleGroupClusteringColumnSelectorFactory.java
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.druid.segment.projections;
-
-import org.apache.druid.error.DruidException;
-import org.apache.druid.math.expr.Evals;
-import org.apache.druid.math.expr.ExprEval;
-import org.apache.druid.math.expr.ExpressionType;
-import org.apache.druid.query.dimension.DimensionSpec;
-import org.apache.druid.query.monomorphicprocessing.RuntimeShapeInspector;
-import org.apache.druid.segment.ColumnSelectorFactory;
-import org.apache.druid.segment.ColumnValueSelector;
-import org.apache.druid.segment.ConstantExprEvalSelector;
-import org.apache.druid.segment.DimensionSelector;
-import org.apache.druid.segment.RowIdSupplier;
-import org.apache.druid.segment.column.ColumnCapabilities;
-import org.apache.druid.segment.column.ColumnCapabilitiesImpl;
-import org.apache.druid.segment.column.ColumnType;
-import org.apache.druid.segment.column.RowSignature;
-import org.apache.druid.segment.column.ValueType;
-
-import javax.annotation.Nullable;
-
-/**
- * Single-cluster-group counterpart of {@link 
ClusteringColumnSelectorFactory}, used when a query prunes a clustered
- * {@link org.apache.druid.segment.QueryableIndexCursorFactory}). 
Non-clustering columns are delegated directly to
- * the per-group factory; clustering columns, which are not physically stored 
in the per-group data, are surfaced as
- * constants from the group's clustering tuple.
- */
-public class SingleGroupClusteringColumnSelectorFactory implements 
ColumnSelectorFactory
-{
-  private final ColumnSelectorFactory delegate;
-  private final RowSignature clusteringColumns;
-  private final Object[] clusteringValues;
-
-  public SingleGroupClusteringColumnSelectorFactory(
-      ColumnSelectorFactory delegate,
-      RowSignature clusteringColumns,
-      Object[] clusteringValues
-  )
-  {
-    if (clusteringValues == null || clusteringValues.length != 
clusteringColumns.size()) {
-      throw DruidException.defensive(
-          "clusteringValues length [%s] must match clusteringColumns size 
[%s]",
-          clusteringValues == null ? "null" : clusteringValues.length,
-          clusteringColumns.size()
-      );
-    }
-    this.delegate = delegate;
-    this.clusteringColumns = clusteringColumns;
-    this.clusteringValues = clusteringValues;
-  }
-
-  @Override
-  public DimensionSelector makeDimensionSelector(DimensionSpec dimensionSpec)
-  {
-    final int idx = clusteringColumns.indexOf(dimensionSpec.getDimension());
-    if (idx < 0) {
-      return delegate.makeDimensionSelector(dimensionSpec);
-    }
-    final Object raw = clusteringValues[idx];
-    return DimensionSelector.constant(Evals.asString(raw), 
dimensionSpec.getExtractionFn());
-  }
-
-  @Override
-  public ColumnValueSelector makeColumnValueSelector(String columnName)
-  {
-    final int idx = clusteringColumns.indexOf(columnName);
-    if (idx < 0) {
-      return delegate.makeColumnValueSelector(columnName);
-    }
-    return new 
ConstantClusteringValueSelector(clusteringColumns.getColumnType(idx).orElseThrow(),
 clusteringValues[idx]);
-  }
-
-  @Nullable
-  @Override
-  public ColumnCapabilities getColumnCapabilities(String column)
-  {
-    final int idx = clusteringColumns.indexOf(column);
-    if (idx < 0) {
-      return delegate.getColumnCapabilities(column);
-    }
-    final ColumnType type = clusteringColumns.getColumnType(idx).orElseThrow();
-    if (type.is(ValueType.STRING)) {
-      return 
ColumnCapabilitiesImpl.createSimpleSingleValueStringColumnCapabilities()
-                                   .setDictionaryEncoded(true)
-                                   .setDictionaryValuesSorted(true)
-                                   .setDictionaryValuesUnique(true);
-    }
-    return ColumnCapabilitiesImpl.createSimpleNumericColumnCapabilities(type);
-  }
-
-  @Nullable
-  @Override
-  public RowIdSupplier getRowIdSupplier()
-  {
-    return delegate.getRowIdSupplier();
-  }
-
-  /**
-   * Constant value selector for a clustering column's group-constant typed 
value.
-   */
-  private static final class ConstantClusteringValueSelector implements 
ColumnValueSelector<Object>
-  {
-    private final ConstantExprEvalSelector inner;
-
-    private ConstantClusteringValueSelector(ColumnType columnType, Object 
value)
-    {
-      this.inner = new 
ConstantExprEvalSelector(ExprEval.ofType(ExpressionType.fromColumnTypeStrict(columnType),
 value));
-    }
-
-    @Override
-    public double getDouble()
-    {
-      return inner.getDouble();
-    }
-
-    @Override
-    public float getFloat()
-    {
-      return inner.getFloat();
-    }
-
-    @Override
-    public long getLong()
-    {
-      return inner.getLong();
-    }
-
-    @Override
-    public boolean isNull()
-    {
-      return inner.isNull();
-    }
-
-    @Nullable
-    @Override
-    public Object getObject()
-    {
-      return inner.getObject().value();
-    }
-
-    @Override
-    public Class<Object> classOfObject()
-    {
-      return Object.class;
-    }
-
-    @Override
-    public void inspectRuntimeShape(RuntimeShapeInspector inspector)
-    {
-      inner.inspectRuntimeShape(inspector);
-    }
-  }
-}
diff --git 
a/processing/src/main/java/org/apache/druid/segment/projections/SingleGroupClusteringVectorColumnSelectorFactory.java
 
b/processing/src/main/java/org/apache/druid/segment/projections/SingleGroupClusteringVectorColumnSelectorFactory.java
deleted file mode 100644
index 8fd8dbd41ed..00000000000
--- 
a/processing/src/main/java/org/apache/druid/segment/projections/SingleGroupClusteringVectorColumnSelectorFactory.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.druid.segment.projections;
-
-import org.apache.druid.error.DruidException;
-import org.apache.druid.math.expr.Evals;
-import org.apache.druid.query.dimension.DimensionSpec;
-import org.apache.druid.segment.column.ColumnCapabilities;
-import org.apache.druid.segment.column.ColumnCapabilitiesImpl;
-import org.apache.druid.segment.column.ColumnType;
-import org.apache.druid.segment.column.RowSignature;
-import org.apache.druid.segment.column.ValueType;
-import org.apache.druid.segment.vector.ConstantVectorSelectors;
-import org.apache.druid.segment.vector.MultiValueDimensionVectorSelector;
-import org.apache.druid.segment.vector.ReadableVectorInspector;
-import org.apache.druid.segment.vector.SingleValueDimensionVectorSelector;
-import org.apache.druid.segment.vector.VectorColumnSelectorFactory;
-import org.apache.druid.segment.vector.VectorObjectSelector;
-import org.apache.druid.segment.vector.VectorValueSelector;
-
-import javax.annotation.Nullable;
-
-/**
- * Vectorized counterpart of {@link 
SingleGroupClusteringColumnSelectorFactory}: the single-cluster-group fast path 
for
- * a clustered base-table segment. Non-clustering columns delegate directly to 
the per-group
- * {@link VectorColumnSelectorFactory}; clustering columns are surfaced as 
constant vector selectors.
- */
-public class SingleGroupClusteringVectorColumnSelectorFactory implements 
VectorColumnSelectorFactory
-{
-  private final VectorColumnSelectorFactory delegate;
-  private final RowSignature clusteringColumns;
-  private final Object[] clusteringValues;
-
-  public SingleGroupClusteringVectorColumnSelectorFactory(
-      VectorColumnSelectorFactory delegate,
-      RowSignature clusteringColumns,
-      Object[] clusteringValues
-  )
-  {
-    if (clusteringValues == null || clusteringValues.length != 
clusteringColumns.size()) {
-      throw DruidException.defensive(
-          "clusteringValues length [%s] must match clusteringColumns size 
[%s]",
-          clusteringValues == null ? "null" : clusteringValues.length,
-          clusteringColumns.size()
-      );
-    }
-    this.delegate = delegate;
-    this.clusteringColumns = clusteringColumns;
-    this.clusteringValues = clusteringValues;
-  }
-
-  @Override
-  public ReadableVectorInspector getReadableVectorInspector()
-  {
-    return delegate.getReadableVectorInspector();
-  }
-
-  @Override
-  public int getMaxVectorSize()
-  {
-    return delegate.getMaxVectorSize();
-  }
-
-  @Override
-  public SingleValueDimensionVectorSelector 
makeSingleValueDimensionSelector(DimensionSpec dimensionSpec)
-  {
-    final int idx = clusteringColumns.indexOf(dimensionSpec.getDimension());
-    if (idx < 0) {
-      return delegate.makeSingleValueDimensionSelector(dimensionSpec);
-    }
-    final Object raw = clusteringValues[idx];
-    return 
ConstantVectorSelectors.singleValueDimensionVectorSelector(delegate.getReadableVectorInspector(),
 Evals.asString(raw));
-  }
-
-  @Override
-  public MultiValueDimensionVectorSelector 
makeMultiValueDimensionSelector(DimensionSpec dimensionSpec)
-  {
-    if (clusteringColumns.indexOf(dimensionSpec.getDimension()) < 0) {
-      return delegate.makeMultiValueDimensionSelector(dimensionSpec);
-    }
-    throw DruidException.defensive(
-        "clustering column [%s] is not dictionary-encoded; no multi-value 
dimension vector selector",
-        dimensionSpec.getDimension()
-    );
-  }
-
-  @Override
-  public VectorValueSelector makeValueSelector(String column)
-  {
-    final int idx = clusteringColumns.indexOf(column);
-    if (idx < 0) {
-      return delegate.makeValueSelector(column);
-    }
-    final Object raw = clusteringValues[idx];
-    final Number number = (raw instanceof Number) ? (Number) raw : null;
-    return 
ConstantVectorSelectors.vectorValueSelector(delegate.getReadableVectorInspector(),
 number);
-  }
-
-  @Override
-  public VectorObjectSelector makeObjectSelector(String column)
-  {
-    final int idx = clusteringColumns.indexOf(column);
-    if (idx < 0) {
-      return delegate.makeObjectSelector(column);
-    }
-    return 
ConstantVectorSelectors.vectorObjectSelector(delegate.getReadableVectorInspector(),
 clusteringValues[idx]);
-  }
-
-  @Nullable
-  @Override
-  public ColumnCapabilities getColumnCapabilities(String column)
-  {
-    final int idx = clusteringColumns.indexOf(column);
-    if (idx < 0) {
-      return delegate.getColumnCapabilities(column);
-    }
-    final ColumnType type = clusteringColumns.getColumnType(idx).orElseThrow();
-    if (type.is(ValueType.STRING)) {
-      return 
ColumnCapabilitiesImpl.createSimpleSingleValueStringColumnCapabilities()
-                                   .setDictionaryEncoded(true)
-                                   .setDictionaryValuesSorted(true)
-                                   .setDictionaryValuesUnique(true);
-    }
-    return ColumnCapabilitiesImpl.createSimpleNumericColumnCapabilities(type);
-  }
-}
diff --git 
a/processing/src/test/java/org/apache/druid/segment/IndexMergerV10ClusteredTest.java
 
b/processing/src/test/java/org/apache/druid/segment/IndexMergerV10ClusteredTest.java
index 3782b61a60f..d19d174d812 100644
--- 
a/processing/src/test/java/org/apache/druid/segment/IndexMergerV10ClusteredTest.java
+++ 
b/processing/src/test/java/org/apache/druid/segment/IndexMergerV10ClusteredTest.java
@@ -419,12 +419,16 @@ class IndexMergerV10ClusteredTest extends 
InitializedNullHandlingTest
     Assertions.assertEquals(1, groups.get(1).getNumRows());
     Assertions.assertEquals(3, index.getNumRows());
 
-    // Per-group QueryableIndex: region physically present, clustering column 
not.
-    final QueryableIndex acmeGroup = 
index.getClusterGroupQueryableIndex(groups.get(0));
+    // Per-group QueryableIndex (merge/persist view, 
withClusteringColumns=false): region physically present,
+    // clustering column not.
+    final QueryableIndex acmeGroup = 
index.getClusterGroupQueryableIndex(groups.get(0), false);
     Assertions.assertNotNull(acmeGroup);
     Assertions.assertEquals(2, acmeGroup.getNumRows());
     Assertions.assertNotNull(acmeGroup.getColumnHolder("region"));
     Assertions.assertNull(acmeGroup.getColumnHolder("tenant"));
+    // Query view (withClusteringColumns=true): the clustering column is 
fabricated as a constant column.
+    final QueryableIndex acmeGroupForQuery = 
index.getClusterGroupQueryableIndex(groups.get(0), true);
+    Assertions.assertNotNull(acmeGroupForQuery.getColumnHolder("tenant"));
 
     // Full scan through the real clustered cursor path: groups in clustering 
order, constants injected.
     Assertions.assertEquals(
diff --git 
a/processing/src/test/java/org/apache/druid/segment/QueryableIndexCursorFactoryClusteredTest.java
 
b/processing/src/test/java/org/apache/druid/segment/QueryableIndexCursorFactoryClusteredTest.java
index 8f58fac9044..e89123ac9f9 100644
--- 
a/processing/src/test/java/org/apache/druid/segment/QueryableIndexCursorFactoryClusteredTest.java
+++ 
b/processing/src/test/java/org/apache/druid/segment/QueryableIndexCursorFactoryClusteredTest.java
@@ -42,6 +42,7 @@ import org.apache.druid.query.dimension.DefaultDimensionSpec;
 import org.apache.druid.query.filter.ColumnIndexSelector;
 import org.apache.druid.query.filter.EqualityFilter;
 import org.apache.druid.query.filter.Filter;
+import org.apache.druid.query.filter.RangeFilter;
 import org.apache.druid.query.filter.TypedInFilter;
 import org.apache.druid.query.filter.ValueMatcher;
 import org.apache.druid.query.groupby.GroupByQuery;
@@ -310,6 +311,61 @@ class QueryableIndexCursorFactoryClusteredTest
     }
   }
 
+  @Test
+  void testRangeFilterOnClusteringColumnMatchesViaFabricatedColumn()
+  {
+    segmentIndex = standardTwoGroup();
+    final QueryableIndexCursorFactory factory = new 
QueryableIndexCursorFactory(
+        segmentIndex,
+        QueryableIndexTimeBoundaryInspector.create(segmentIndex)
+    );
+
+    // A RangeFilter on the clustering column is NOT folded by the pruner 
(only Equality/In/Null fold), so it survives
+    // to the per-group cursor. Clustering columns are constant-per-group and 
not stored on disk, so before the
+    // per-group index fabricated them as constant columns this returned 
nothing; now the range resolves against the
+    // fabricated column's value matcher and both groups (tenant in [a, z]) 
match all rows.
+    final Filter filter = new RangeFilter("tenant", ColumnType.STRING, "a", 
"z", false, false, null);
+    try (CursorHolder holder = factory.makeCursorHolder(specWith(filter))) {
+      final List<List<String>> rows = 
collectTenantRegionRows(holder.asCursor());
+      Assertions.assertEquals(
+          List.of(
+              List.of("acme", "us-east-1"),
+              List.of("acme", "us-west-2"),
+              List.of("globex", "eu-west-1")
+          ),
+          rows
+      );
+    }
+  }
+
+  @Test
+  void testResidualRangeFilterOnClusteringColumnSingleGroup()
+  {
+    segmentIndex = standardTwoGroup();
+    final QueryableIndexCursorFactory factory = new 
QueryableIndexCursorFactory(
+        segmentIndex,
+        QueryableIndexTimeBoundaryInspector.create(segmentIndex)
+    );
+
+    // tenant = 'acme' folds and prunes to the single acme group; the 
RangeFilter on the clustering column survives as
+    // a residual leaf on the single-group cursor. It must match against the 
fabricated constant column ('acme' is in
+    // [a, m]) rather than an absent physical column.
+    final Filter filter = new AndFilter(List.of(
+        new EqualityFilter("tenant", ColumnType.STRING, "acme", null),
+        new RangeFilter("tenant", ColumnType.STRING, "a", "m", false, false, 
null)
+    ));
+    try (CursorHolder holder = factory.makeCursorHolder(specWith(filter))) {
+      final List<List<String>> rows = 
collectTenantRegionRows(holder.asCursor());
+      Assertions.assertEquals(
+          List.of(
+              List.of("acme", "us-east-1"),
+              List.of("acme", "us-west-2")
+          ),
+          rows
+      );
+    }
+  }
+
   @Test
   void testReportedOrderingIsTheFullSegmentOrderingRegardlessOfGroupPruning()
   {
@@ -653,8 +709,8 @@ class QueryableIndexCursorFactoryClusteredTest
   void testGroupByOnNonClusteringColumnWithinSingleGroup()
   {
     // Filter to a single cluster group (tenant=acme), then GROUP BY a 
non-clustering column. Exercises the
-    // single-group cursor-holder path + 
SingleGroupClusteringColumnSelectorFactory end-to-end: `region` groups
-    // correctly via the group's own (stable) dictionary, and the pruned-out 
globex group contributes nothing.
+    // single-group cursor-holder path end-to-end: `region` groups correctly 
via the group's own dictionary, and the
+    // pruned-out globex group contributes nothing.
     segmentIndex = buildSegment(List.of(
         row("acme", "2025-01-01T00:00:00", "us-east-1"),
         row("acme", "2025-01-01T00:10:00", "us-east-1"),
diff --git 
a/processing/src/test/java/org/apache/druid/segment/column/ConstantNumericColumnTest.java
 
b/processing/src/test/java/org/apache/druid/segment/column/ConstantNumericColumnTest.java
new file mode 100644
index 00000000000..79e46c9ec7c
--- /dev/null
+++ 
b/processing/src/test/java/org/apache/druid/segment/column/ConstantNumericColumnTest.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.segment.column;
+
+import org.apache.druid.math.expr.ExprEval;
+import org.apache.druid.segment.ColumnValueSelector;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class ConstantNumericColumnTest
+{
+  @Test
+  void testLongSelectorReturnsBoxedValueNotExprEval()
+  {
+    final ColumnValueSelector<?> selector =
+        new ConstantNumericColumn(ColumnType.LONG, 42L, 
3).makeColumnValueSelector(null);
+    // The single-group path reads this selector directly (no unwrapping 
wrapper), so getObject() must return the boxed
+    // value like a physical numeric column -- not the ExprEval that 
ConstantExprEvalSelector would expose.
+    Assertions.assertFalse(selector.getObject() instanceof ExprEval);
+    Assertions.assertEquals(42L, selector.getObject());
+    Assertions.assertEquals(Object.class, selector.classOfObject());
+    Assertions.assertEquals(42L, selector.getLong());
+    Assertions.assertEquals(42.0, selector.getDouble(), 0.0);
+    Assertions.assertFalse(selector.isNull());
+  }
+
+  @Test
+  void testDoubleSelector()
+  {
+    final ColumnValueSelector<?> selector =
+        new ConstantNumericColumn(ColumnType.DOUBLE, 1.5, 
1).makeColumnValueSelector(null);
+    Assertions.assertEquals(1.5, selector.getObject());
+    Assertions.assertEquals(1.5, selector.getDouble(), 0.0);
+    Assertions.assertEquals(1L, selector.getLong());
+    Assertions.assertFalse(selector.isNull());
+  }
+
+  @Test
+  void testFloatSelector()
+  {
+    final ColumnValueSelector<?> selector =
+        new ConstantNumericColumn(ColumnType.FLOAT, 2.5f, 
1).makeColumnValueSelector(null);
+    Assertions.assertEquals(2.5f, selector.getObject());
+    Assertions.assertEquals(2.5f, selector.getFloat(), 0.0f);
+    Assertions.assertFalse(selector.isNull());
+  }
+
+  @Test
+  void testNullValue()
+  {
+    final ColumnValueSelector<?> selector =
+        new ConstantNumericColumn(ColumnType.LONG, null, 
1).makeColumnValueSelector(null);
+    Assertions.assertTrue(selector.isNull());
+    Assertions.assertNull(selector.getObject());
+    Assertions.assertEquals(0L, selector.getLong());
+  }
+}
diff --git 
a/processing/src/test/java/org/apache/druid/segment/data/ConstantUtf8IndexedTest.java
 
b/processing/src/test/java/org/apache/druid/segment/data/ConstantUtf8IndexedTest.java
new file mode 100644
index 00000000000..1c154fa3b73
--- /dev/null
+++ 
b/processing/src/test/java/org/apache/druid/segment/data/ConstantUtf8IndexedTest.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.segment.data;
+
+import org.apache.druid.error.DruidException;
+import org.apache.druid.java.util.common.StringUtils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class ConstantUtf8IndexedTest
+{
+  @Test
+  void testGetReturnsAFreshBufferEachCallSoDecodingDoesNotConsumeSharedState()
+  {
+    final ConstantUtf8Indexed indexed =
+        new ConstantUtf8Indexed(StringUtils.toUtf8ByteBuffer("acme"));
+    // StringUtils.fromUtf8 advances the buffer's position; a shared buffer 
would be drained after the first read.
+    Assertions.assertEquals("acme", StringUtils.fromUtf8(indexed.get(0)));
+    Assertions.assertEquals("acme", StringUtils.fromUtf8(indexed.get(0)));
+    Assertions.assertEquals(1, indexed.size());
+    Assertions.assertTrue(indexed.isSorted());
+  }
+
+  @Test
+  void testIndexOfFollowsSortedBinarySearchContract()
+  {
+    // value "m": found -> 0; a value that sorts before -> insertion point 0 
-> -1; one that sorts after -> point 1
+    // -> -2 (the -(insertionPoint) - 1 contract that GenericIndexed uses, 
required because isSorted() is true).
+    final ConstantUtf8Indexed indexed =
+        new ConstantUtf8Indexed(StringUtils.toUtf8ByteBuffer("m"));
+    Assertions.assertEquals(0, 
indexed.indexOf(StringUtils.toUtf8ByteBuffer("m")));
+    Assertions.assertEquals(-1, 
indexed.indexOf(StringUtils.toUtf8ByteBuffer("a")));
+    Assertions.assertEquals(-2, 
indexed.indexOf(StringUtils.toUtf8ByteBuffer("z")));
+    // null sorts before any non-null value -> insertion point 0 -> -1.
+    Assertions.assertEquals(-1, indexed.indexOf(null));
+  }
+
+  @Test
+  void testNullValue()
+  {
+    final ConstantUtf8Indexed indexed = new ConstantUtf8Indexed(null);
+    Assertions.assertEquals(1, indexed.size());
+    Assertions.assertNull(indexed.get(0));
+    Assertions.assertEquals(0, indexed.indexOf(null));
+    // a non-null value sorts after the null entry -> insertion point 1 -> -2.
+    Assertions.assertEquals(-2, 
indexed.indexOf(StringUtils.toUtf8ByteBuffer("acme")));
+  }
+
+  @Test
+  void testGetOutOfBoundsThrows()
+  {
+    final ConstantUtf8Indexed indexed =
+        new ConstantUtf8Indexed(StringUtils.toUtf8ByteBuffer("acme"));
+    Assertions.assertThrows(DruidException.class, () -> indexed.get(1));
+  }
+}
diff --git 
a/processing/src/test/java/org/apache/druid/segment/projections/SingleGroupClusteringColumnSelectorFactoryTest.java
 
b/processing/src/test/java/org/apache/druid/segment/projections/SingleGroupClusteringColumnSelectorFactoryTest.java
deleted file mode 100644
index f7fc6ba1f54..00000000000
--- 
a/processing/src/test/java/org/apache/druid/segment/projections/SingleGroupClusteringColumnSelectorFactoryTest.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.druid.segment.projections;
-
-import org.apache.druid.query.dimension.DefaultDimensionSpec;
-import org.apache.druid.query.dimension.DimensionSpec;
-import org.apache.druid.segment.ColumnSelectorFactory;
-import org.apache.druid.segment.ColumnValueSelector;
-import org.apache.druid.segment.DimensionSelector;
-import org.apache.druid.segment.NilColumnValueSelector;
-import org.apache.druid.segment.RowIdSupplier;
-import org.apache.druid.segment.column.ColumnCapabilities;
-import org.apache.druid.segment.column.ColumnCapabilitiesImpl;
-import org.apache.druid.segment.column.ColumnType;
-import org.apache.druid.segment.column.RowSignature;
-import org.apache.druid.segment.column.ValueType;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Test;
-
-import javax.annotation.Nullable;
-
-class SingleGroupClusteringColumnSelectorFactoryTest
-{
-  private static final RowSignature CLUSTERING = 
RowSignature.builder().add("tenant", ColumnType.STRING).build();
-
-  @Test
-  void testNonClusteringColumnPreservesDictionaryEncodedCapabilities()
-  {
-    final ColumnCapabilities dictEncoded =
-        
ColumnCapabilitiesImpl.createSimpleSingleValueStringColumnCapabilities()
-                              .setDictionaryEncoded(true)
-                              .setDictionaryValuesUnique(true)
-                              .setDictionaryValuesSorted(true);
-    final RecordingDelegate delegate = new RecordingDelegate(dictEncoded);
-    final SingleGroupClusteringColumnSelectorFactory f =
-        new SingleGroupClusteringColumnSelectorFactory(delegate, CLUSTERING, 
new Object[]{"acme"});
-
-    final ColumnCapabilities caps = f.getColumnCapabilities("region");
-    Assertions.assertNotNull(caps);
-    Assertions.assertTrue(caps.isDictionaryEncoded().isTrue());
-    Assertions.assertEquals("region", delegate.lastCapabilitiesColumn);
-  }
-
-  @Test
-  void testNonClusteringColumnSelectorsDelegatedDirectly()
-  {
-    final RecordingDelegate delegate = new RecordingDelegate(null);
-    final SingleGroupClusteringColumnSelectorFactory f =
-        new SingleGroupClusteringColumnSelectorFactory(delegate, CLUSTERING, 
new Object[]{"acme"});
-
-    // No wrapper / generation indirection: the delegate is consulted 
immediately on selector creation.
-    final DimensionSelector dim = 
f.makeDimensionSelector(DefaultDimensionSpec.of("region"));
-    Assertions.assertEquals("region", delegate.lastDimSelectorName);
-    Assertions.assertEquals("delegated:region", dim.lookupName(0));
-
-    f.makeColumnValueSelector("metric");
-    Assertions.assertEquals("metric", delegate.lastValueSelectorName);
-  }
-
-  @Test
-  void testClusteringColumnReturnsConstantWithoutHittingDelegate()
-  {
-    final RecordingDelegate delegate = new RecordingDelegate(null);
-    final SingleGroupClusteringColumnSelectorFactory f =
-        new SingleGroupClusteringColumnSelectorFactory(delegate, CLUSTERING, 
new Object[]{"acme"});
-
-    final DimensionSelector dim = 
f.makeDimensionSelector(DefaultDimensionSpec.of("tenant"));
-    Assertions.assertEquals("acme", dim.lookupName(dim.getRow().get(0)));
-    Assertions.assertNull(delegate.lastDimSelectorName, "delegate must not be 
hit for clustering columns");
-
-    final ColumnValueSelector val = f.makeColumnValueSelector("tenant");
-    Assertions.assertEquals("acme", val.getObject());
-
-    // A single group's clustering value is a constant 1-entry dictionary, so 
it is reported as dictionary-encoded /
-    // sorted / unique to route grouping through the dictionary-id path.
-    final ColumnCapabilities caps = f.getColumnCapabilities("tenant");
-    Assertions.assertNotNull(caps);
-    Assertions.assertTrue(caps.is(ValueType.STRING));
-    Assertions.assertTrue(caps.isDictionaryEncoded().isTrue());
-    Assertions.assertTrue(caps.areDictionaryValuesSorted().isTrue());
-    Assertions.assertTrue(caps.areDictionaryValuesUnique().isTrue());
-  }
-
-  private static class RecordingDelegate implements ColumnSelectorFactory
-  {
-    @Nullable
-    private final ColumnCapabilities capabilities;
-    String lastDimSelectorName;
-    String lastValueSelectorName;
-    String lastCapabilitiesColumn;
-
-    RecordingDelegate(@Nullable ColumnCapabilities capabilities)
-    {
-      this.capabilities = capabilities;
-    }
-
-    @Override
-    public DimensionSelector makeDimensionSelector(DimensionSpec dimensionSpec)
-    {
-      lastDimSelectorName = dimensionSpec.getDimension();
-      return DimensionSelector.constant("delegated:" + 
dimensionSpec.getDimension());
-    }
-
-    @Override
-    public ColumnValueSelector makeColumnValueSelector(String columnName)
-    {
-      lastValueSelectorName = columnName;
-      return NilColumnValueSelector.instance();
-    }
-
-    @Nullable
-    @Override
-    public ColumnCapabilities getColumnCapabilities(String column)
-    {
-      lastCapabilitiesColumn = column;
-      return capabilities;
-    }
-
-    @Nullable
-    @Override
-    public RowIdSupplier getRowIdSupplier()
-    {
-      return null;
-    }
-  }
-}


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

Reply via email to