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]