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 cfe5f6f4d683..364879d03616 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.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 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 List getClusterGroupSchemas() * 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 QueryableIndex getClusterGroupQueryableIndex(TableClusterGroupSpec groupS if (groupIndex < 0) { throw DruidException.defensive("Cluster group spec is not part of this segment"); } - final Map> groupColumns = clusterGroupColumnsByIndex.computeIfAbsent( + final Map> baseColumns = clusterGroupColumnsByIndex.computeIfAbsent( groupIndex, i -> buildColumnSuppliers( clusteredBaseSummary.getTimeColumnName(), @@ -476,6 +478,20 @@ public QueryableIndex getClusterGroupQueryableIndex(TableClusterGroupSpec groupS Map.of() ) ); + + final Map> 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 3704c055ff23..04d4f02869b5 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 @@ private AsyncCursorHolder makeClusteredCursorHolderAsync(CursorBuildSpec spec) final List 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 6f604fbb18cc..a7c2c04d7714 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 @@ default List getClusterGroupSchemas() } /** - * 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. + *

+ * 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 1803554dc56e..a29b2b570722 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.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 @@ private CursorHolder makeSingleGroupClusteredCursorHolder( 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 @@ private CursorHolder makeSingleGroupClusteredCursorHolder( ); } + // 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 @@ private CursorHolder makeMultiGroupClusteredCursorHolder( 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 b33ed50de778..38bf75992224 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 IndexableAdapter getProjectionAdapter(String projection) @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 6c7c37bf7a10..065a64932326 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.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 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 ColumnCapabilities getColumnCapabilities(String column) 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 List getClusterGroupSchemas() * 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 QueryableIndex getClusterGroupQueryableIndex(TableClusterGroupSpec groupS if (index < 0) { throw DruidException.defensive("Cluster group spec is not part of this segment"); } - final Map> groupColumns = clusterGroupColumns.get(index); + // add clustering columns are constants for query paths + final Map> 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 000000000000..e2e495e70afb --- /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> 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 000000000000..8b4177c8f078 --- /dev/null +++ b/processing/src/main/java/org/apache/druid/segment/column/ConstantNumericColumn.java @@ -0,0 +1,97 @@ +/* + * 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.math.expr.ExpressionType; +import org.apache.druid.query.monomorphicprocessing.RuntimeShapeInspector; +import org.apache.druid.segment.ColumnValueSelector; +import org.apache.druid.segment.ConstantExprEvalSelector; +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. Selectors are backed by the + * shared constant-selector helpers ({@link ConstantExprEvalSelector} / {@link ConstantVectorSelectors}). 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 ConstantExprEvalSelector(ExprEval.ofType(ExpressionType.fromColumnTypeStrict(type), 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 + } +} 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 000000000000..3e36fb5796ac --- /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 000000000000..e2ec1951e482 --- /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 +{ + @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 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 aada6b78816d..000000000000 --- 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 - { - 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 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 8fd8dbd41ed7..000000000000 --- 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 3782b61a60f2..d19d174d8129 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 @@ void testPersistAndLoadClusteredSegment() 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 8f58fac90443..e89123ac9f9c 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.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 @@ void testFilterOnClusteringColumnPrunesNonMatchingGroups() } } + @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> 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> 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 @@ void testGroupByOnNonClusteringColumnRollsAcrossGroups() 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/data/ConstantUtf8IndexedTest.java b/processing/src/test/java/org/apache/druid/segment/data/ConstantUtf8IndexedTest.java new file mode 100644 index 000000000000..1c154fa3b73d --- /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 f7fc6ba1f54a..000000000000 --- 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; - } - } -}