diff --git a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/groupby/GroupByPostShuffleFrameProcessor.java b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/groupby/GroupByPostShuffleFrameProcessor.java index 7861a45f61d0..5b0a3ddefd2e 100644 --- a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/groupby/GroupByPostShuffleFrameProcessor.java +++ b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/groupby/GroupByPostShuffleFrameProcessor.java @@ -109,7 +109,7 @@ public GroupByPostShuffleFrameProcessor( RowBasedGrouperHelper.createResultRowBasedColumnSelectorFactory( query, () -> outputRow, - RowSignature.Finalization.YES + GroupByQueryKit.isFinalize(query) ? RowSignature.Finalization.YES : RowSignature.Finalization.NO ) ); } diff --git a/integration-tests-ex/cases/src/test/resources/catalog/incompatibleTypeAssignmentWithValidationDisabled_select.sql b/integration-tests-ex/cases/src/test/resources/catalog/incompatibleTypeAssignmentWithValidationDisabled_select.sql index 4b6fafdae916..ef3e474736cd 100644 --- a/integration-tests-ex/cases/src/test/resources/catalog/incompatibleTypeAssignmentWithValidationDisabled_select.sql +++ b/integration-tests-ex/cases/src/test/resources/catalog/incompatibleTypeAssignmentWithValidationDisabled_select.sql @@ -4,7 +4,7 @@ "expectedResults": [ { "__time": 1672058096000, - "double_col": 0.0 + "double_col": null } ] } diff --git a/processing/src/main/java/org/apache/druid/frame/field/FieldWriters.java b/processing/src/main/java/org/apache/druid/frame/field/FieldWriters.java index 028c9fd39c56..dde61a4d2ea9 100644 --- a/processing/src/main/java/org/apache/druid/frame/field/FieldWriters.java +++ b/processing/src/main/java/org/apache/druid/frame/field/FieldWriters.java @@ -22,6 +22,7 @@ import org.apache.druid.frame.key.RowKey; import org.apache.druid.frame.write.RowBasedFrameWriterFactory; import org.apache.druid.frame.write.UnsupportedColumnTypeException; +import org.apache.druid.frame.write.cast.TypeCastSelectors; import org.apache.druid.java.util.common.ISE; import org.apache.druid.query.dimension.DefaultDimensionSpec; import org.apache.druid.segment.ColumnSelectorFactory; @@ -101,7 +102,8 @@ private static FieldWriter makeLongWriter( final String columnName ) { - final ColumnValueSelector selector = selectorFactory.makeColumnValueSelector(columnName); + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(selectorFactory, columnName, ColumnType.LONG); return LongFieldWriter.forPrimitive(selector); } @@ -110,7 +112,8 @@ private static FieldWriter makeFloatWriter( final String columnName ) { - final ColumnValueSelector selector = selectorFactory.makeColumnValueSelector(columnName); + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(selectorFactory, columnName, ColumnType.FLOAT); return FloatFieldWriter.forPrimitive(selector); } @@ -119,7 +122,8 @@ private static FieldWriter makeDoubleWriter( final String columnName ) { - final ColumnValueSelector selector = selectorFactory.makeColumnValueSelector(columnName); + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(selectorFactory, columnName, ColumnType.DOUBLE); return DoubleFieldWriter.forPrimitive(selector); } @@ -139,7 +143,8 @@ private static FieldWriter makeStringArrayWriter( final boolean removeNullBytes ) { - final ColumnValueSelector selector = selectorFactory.makeColumnValueSelector(columnName); + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(selectorFactory, columnName, ColumnType.STRING_ARRAY); return new StringArrayFieldWriter(selector, removeNullBytes); } @@ -148,7 +153,8 @@ private static FieldWriter makeLongArrayWriter( final String columnName ) { - final ColumnValueSelector selector = selectorFactory.makeColumnValueSelector(columnName); + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(selectorFactory, columnName, ColumnType.LONG_ARRAY); return NumericArrayFieldWriter.getLongArrayFieldWriter(selector); } @@ -157,7 +163,8 @@ private static FieldWriter makeFloatArrayWriter( final String columnName ) { - final ColumnValueSelector selector = selectorFactory.makeColumnValueSelector(columnName); + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(selectorFactory, columnName, ColumnType.FLOAT_ARRAY); return NumericArrayFieldWriter.getFloatArrayFieldWriter(selector); } @@ -166,7 +173,8 @@ private static FieldWriter makeDoubleArrayWriter( final String columnName ) { - final ColumnValueSelector selector = selectorFactory.makeColumnValueSelector(columnName); + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(selectorFactory, columnName, ColumnType.DOUBLE_ARRAY); return NumericArrayFieldWriter.getDoubleArrayFieldWriter(selector); } @@ -185,7 +193,8 @@ private static FieldWriter makeComplexWriter( throw new ISE("No serde for complexTypeName[%s], cannot write column [%s]", columnTypeName, columnName); } - final ColumnValueSelector selector = selectorFactory.makeColumnValueSelector(columnName); + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(selectorFactory, columnName, ColumnType.ofComplex(columnTypeName)); return new ComplexFieldWriter(serde, selector); } } diff --git a/processing/src/main/java/org/apache/druid/frame/field/NumericArrayFieldSelector.java b/processing/src/main/java/org/apache/druid/frame/field/NumericArrayFieldSelector.java index f15361d47ea1..f1bcb2e5c705 100644 --- a/processing/src/main/java/org/apache/druid/frame/field/NumericArrayFieldSelector.java +++ b/processing/src/main/java/org/apache/druid/frame/field/NumericArrayFieldSelector.java @@ -39,7 +39,7 @@ * * @param Type of the individual array elements */ -public abstract class NumericArrayFieldSelector implements ColumnValueSelector +public abstract class NumericArrayFieldSelector implements ColumnValueSelector { /** * Memory containing the serialized values of the array @@ -81,15 +81,15 @@ public void inspectRuntimeShape(RuntimeShapeInspector inspector) @Nullable @Override - public Object getObject() + public Object[] getObject() { return computeCurrentArray(); } @Override - public Class classOfObject() + public Class classOfObject() { - return Object.class; + return Object[].class; } @Override @@ -131,7 +131,7 @@ public boolean isNull() public abstract int getIndividualFieldSize(); @Nullable - private Number[] computeCurrentArray() + private Object[] computeCurrentArray() { final long fieldPosition = fieldPointer.position(); final long fieldLength = fieldPointer.length(); diff --git a/processing/src/main/java/org/apache/druid/frame/field/NumericArrayFieldWriter.java b/processing/src/main/java/org/apache/druid/frame/field/NumericArrayFieldWriter.java index e220d6bdc519..8096e323dc32 100644 --- a/processing/src/main/java/org/apache/druid/frame/field/NumericArrayFieldWriter.java +++ b/processing/src/main/java/org/apache/druid/frame/field/NumericArrayFieldWriter.java @@ -21,12 +21,10 @@ import org.apache.datasketches.memory.WritableMemory; import org.apache.druid.common.config.NullHandling; -import org.apache.druid.frame.write.FrameWriterUtils; import org.apache.druid.query.monomorphicprocessing.RuntimeShapeInspector; import org.apache.druid.segment.ColumnValueSelector; import javax.annotation.Nullable; -import java.util.List; import java.util.concurrent.atomic.AtomicInteger; /** @@ -128,7 +126,7 @@ public NumericArrayFieldWriter(final ColumnValueSelector selector, NumericFieldW @Override public long writeTo(WritableMemory memory, long position, long maxSize) { - Object row = selector.getObject(); + final Object[] row = (Object[]) selector.getObject(); if (row == null) { int requiredSize = Byte.BYTES; if (requiredSize > maxSize) { @@ -137,18 +135,6 @@ public long writeTo(WritableMemory memory, long position, long maxSize) memory.putByte(position, NULL_ROW); return requiredSize; } else { - - List list = FrameWriterUtils.getNumericArrayFromObject(row); - - if (list == null) { - int requiredSize = Byte.BYTES; - if (requiredSize > maxSize) { - return -1; - } - memory.putByte(position, NULL_ROW); - return requiredSize; - } - // Create a columnValueSelector to write the individual elements re-using the NumericFieldWriter AtomicInteger index = new AtomicInteger(0); ColumnValueSelector columnValueSelector = new ColumnValueSelector() @@ -199,7 +185,7 @@ public boolean isNull() @Override public Number getObject() { - return list.get(index.get()); + return (Number) row[index.get()]; } @Override @@ -215,7 +201,7 @@ public Class classOfObject() // Next [(1 + Numeric Size) x Number of elements of array] bytes are reserved for the elements of the array and // their null markers // Last byte is reserved for array termination - int requiredSize = Byte.BYTES + (writer.getNumericSizeBytes() + Byte.BYTES) * list.size() + Byte.BYTES; + int requiredSize = Byte.BYTES + (writer.getNumericSizeBytes() + Byte.BYTES) * row.length + Byte.BYTES; if (requiredSize > maxSize) { return -1; @@ -225,7 +211,7 @@ public Class classOfObject() memory.putByte(position + offset, NON_NULL_ROW); offset += Byte.BYTES; - for (; index.get() < list.size(); index.incrementAndGet()) { + for (; index.get() < row.length; index.incrementAndGet()) { writer.writeTo( memory, position + offset, diff --git a/processing/src/main/java/org/apache/druid/frame/write/FrameWriterUtils.java b/processing/src/main/java/org/apache/druid/frame/write/FrameWriterUtils.java index d857cf33e03c..9ba2b29fe51b 100644 --- a/processing/src/main/java/org/apache/druid/frame/write/FrameWriterUtils.java +++ b/processing/src/main/java/org/apache/druid/frame/write/FrameWriterUtils.java @@ -144,60 +144,16 @@ public static List getUtf8ByteBuffersFromStringArraySelector( @SuppressWarnings("rawtypes") final BaseObjectColumnValueSelector selector ) { - Object row = selector.getObject(); + final Object[] row = (Object[]) selector.getObject(); if (row == null) { return null; - } else if (row instanceof String) { - return Collections.singletonList(getUtf8ByteBufferFromString((String) row)); - } - - final List retVal = new ArrayList<>(); - if (row instanceof List) { - for (int i = 0; i < ((List) row).size(); i++) { - retVal.add(getUtf8ByteBufferFromString(((List) row).get(i))); - } - } else if (row instanceof Object[]) { - for (Object value : (Object[]) row) { - retVal.add(getUtf8ByteBufferFromString((String) value)); - } } else { - throw new ISE("Unexpected type %s found", row.getClass().getName()); - } - return retVal; - } - - /** - * Retrieves a numeric list from a Java object, given that the object is an instance of something that can be returned - * from {@link ColumnValueSelector#getObject()} of valid numeric array selectors representations - * - * While {@link BaseObjectColumnValueSelector} specifies that only instances of {@code Object[]} can be returned from - * the numeric array selectors, this method also handles a few more cases which can be encountered if the selector is - * directly implemented on top of the group by stuff - */ - @Nullable - public static List getNumericArrayFromObject(Object row) - { - if (row == null) { - return null; - } else if (row instanceof Number) { - return Collections.singletonList((Number) row); - } - - final List retVal = new ArrayList<>(); - - if (row instanceof List) { - for (int i = 0; i < ((List) row).size(); i++) { - retVal.add((Number) ((List) row).get(i)); - } - } else if (row instanceof Object[]) { - for (Object value : (Object[]) row) { - retVal.add((Number) value); + final List retVal = new ArrayList<>(); + for (Object value : row) { + retVal.add(getUtf8ByteBufferFromString((String) value)); } - } else { - throw new ISE("Unexpected type %s found", row.getClass().getName()); + return retVal; } - - return retVal; } /** @@ -275,6 +231,7 @@ public static void copyByteBufferToMemoryDisallowingNullBytes( * Whenever "allowNullBytes" is true, "removeNullBytes" must be false. Use the methods {@link #copyByteBufferToMemoryAllowingNullBytes} * and {@link #copyByteBufferToMemoryDisallowingNullBytes} to copy between the memory *

+ * * @throws InvalidNullByteException if "allowNullBytes" and "removeNullBytes" is false and a null byte is encountered */ private static void copyByteBufferToMemory( diff --git a/processing/src/main/java/org/apache/druid/frame/write/cast/ObjectToArrayColumnValueSelector.java b/processing/src/main/java/org/apache/druid/frame/write/cast/ObjectToArrayColumnValueSelector.java new file mode 100644 index 000000000000..30537a7ecd9b --- /dev/null +++ b/processing/src/main/java/org/apache/druid/frame/write/cast/ObjectToArrayColumnValueSelector.java @@ -0,0 +1,101 @@ +/* + * 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.frame.write.cast; + +import org.apache.druid.error.DruidException; +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.RowIdSupplier; + +import javax.annotation.Nullable; + +/** + * Wraps a {@link ColumnValueSelector}, calls {@link ColumnValueSelector#getObject()}, interprets that value using + * {@link ExprEval#ofType}, and casts it using {@link ExprEval#castTo}. + */ +public class ObjectToArrayColumnValueSelector implements ColumnValueSelector +{ + private final ColumnValueSelector selector; + @Nullable + private final ExpressionType desiredType; + @Nullable + private final RowIdSupplier rowIdSupplier; + + public ObjectToArrayColumnValueSelector( + final ColumnValueSelector selector, + final ExpressionType desiredType, + @Nullable final RowIdSupplier rowIdSupplier + ) + { + this.selector = selector; + this.desiredType = desiredType; + this.rowIdSupplier = rowIdSupplier; + + if (!desiredType.isArray() || desiredType.getElementType() == null) { + throw DruidException.defensive("Expected array with nonnull element type, got[%s]", desiredType); + } + } + + @Override + public double getDouble() + { + throw DruidException.defensive("Unexpected call to getDouble on array selector"); + } + + @Override + public float getFloat() + { + throw DruidException.defensive("Unexpected call to getFloat on array selector"); + } + + @Override + public long getLong() + { + throw DruidException.defensive("Unexpected call to getLong on array selector"); + } + + @Override + public boolean isNull() + { + throw DruidException.defensive("Unexpected call to isNull on array selector"); + } + + @Nullable + @Override + public Object[] getObject() + { + return (Object[]) TypeCastSelectors.bestEffortCoerce(selector.getObject(), desiredType); + } + + @Override + public Class classOfObject() + { + return Object[].class; + } + + @Override + public void inspectRuntimeShape(RuntimeShapeInspector inspector) + { + inspector.visit("selector", selector); + inspector.visit("rowIdSupplier", rowIdSupplier); + } +} diff --git a/processing/src/main/java/org/apache/druid/frame/write/cast/ObjectToNumberColumnValueSelector.java b/processing/src/main/java/org/apache/druid/frame/write/cast/ObjectToNumberColumnValueSelector.java new file mode 100644 index 000000000000..ae09cfd0f20c --- /dev/null +++ b/processing/src/main/java/org/apache/druid/frame/write/cast/ObjectToNumberColumnValueSelector.java @@ -0,0 +1,135 @@ +/* + * 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.frame.write.cast; + +import org.apache.druid.common.config.NullHandling; +import org.apache.druid.error.DruidException; +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.RowIdSupplier; + +import javax.annotation.Nullable; + +/** + * Wraps a {@link ColumnValueSelector}, calls {@link ColumnValueSelector#getObject()} and provides primitive numeric + * accessors based on that object value. + */ +public class ObjectToNumberColumnValueSelector implements ColumnValueSelector +{ + private final ColumnValueSelector selector; + private final ExpressionType desiredType; + + @Nullable + private final RowIdSupplier rowIdSupplier; + + @Nullable + private Number currentValue; + private long currentRowId = RowIdSupplier.INIT; + + /** + * Package-private; create using {@link TypeCastSelectors#makeColumnValueSelector} or + * {@link TypeCastSelectors#wrapColumnValueSelectorIfNeeded}. + */ + ObjectToNumberColumnValueSelector( + final ColumnValueSelector selector, + final ExpressionType desiredType, + @Nullable final RowIdSupplier rowIdSupplier + ) + { + this.selector = selector; + this.desiredType = desiredType; + this.rowIdSupplier = rowIdSupplier; + + if (!desiredType.isNumeric()) { + throw DruidException.defensive("Expected numeric type, got[%s]", desiredType); + } + } + + @Override + public double getDouble() + { + final Number n = computeIfNeeded(); + return n == null ? NullHandling.ZERO_DOUBLE : n.doubleValue(); + } + + @Override + public float getFloat() + { + final Number n = computeIfNeeded(); + return n == null ? NullHandling.ZERO_FLOAT : n.floatValue(); + } + + @Override + public long getLong() + { + final Number n = computeIfNeeded(); + return n == null ? NullHandling.ZERO_LONG : n.longValue(); + } + + @Override + public boolean isNull() + { + return computeIfNeeded() == null; + } + + @Nullable + @Override + public Number getObject() + { + return computeIfNeeded(); + } + + @Override + public Class classOfObject() + { + return Number.class; + } + + @Override + public void inspectRuntimeShape(RuntimeShapeInspector inspector) + { + inspector.visit("selector", selector); + inspector.visit("rowIdSupplier", rowIdSupplier); + } + + @Nullable + private Number computeIfNeeded() + { + if (rowIdSupplier == null) { + return eval(); + } else { + final long rowId = rowIdSupplier.getRowId(); + + if (currentRowId != rowId) { + currentValue = eval(); + currentRowId = rowId; + } + + return currentValue; + } + } + + @Nullable + private Number eval() + { + return (Number) TypeCastSelectors.bestEffortCoerce(selector.getObject(), desiredType); + } +} diff --git a/processing/src/main/java/org/apache/druid/frame/write/cast/TypeCastSelectors.java b/processing/src/main/java/org/apache/druid/frame/write/cast/TypeCastSelectors.java new file mode 100644 index 000000000000..ec6f16aad759 --- /dev/null +++ b/processing/src/main/java/org/apache/druid/frame/write/cast/TypeCastSelectors.java @@ -0,0 +1,121 @@ +/* + * 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.frame.write.cast; + +import org.apache.druid.error.DruidException; +import org.apache.druid.math.expr.ExprEval; +import org.apache.druid.math.expr.ExpressionType; +import org.apache.druid.segment.BaseObjectColumnValueSelector; +import org.apache.druid.segment.ColumnSelectorFactory; +import org.apache.druid.segment.ColumnValueSelector; +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.ColumnType; +import org.apache.druid.segment.column.ValueType; + +import javax.annotation.Nullable; + +public class TypeCastSelectors +{ + /** + * Create a {@link ColumnValueSelector} that does its own typecasting if necessary. If typecasting is not necessary, + * returns a selector directly from the underlying {@link ColumnSelectorFactory}. + * + * @param columnSelectorFactory underlying factory + * @param column column name + * @param desiredType desired type of selector. Can be anything except {@link ColumnType#STRING}. + * For strings, use {@link DimensionSelector} rather than {@link ColumnValueSelector}. + */ + public static ColumnValueSelector makeColumnValueSelector( + final ColumnSelectorFactory columnSelectorFactory, + final String column, + final ColumnType desiredType + ) + { + final ColumnValueSelector selector = columnSelectorFactory.makeColumnValueSelector(column); + final ColumnCapabilities selectorCapabilities = columnSelectorFactory.getColumnCapabilities(column); + + return wrapColumnValueSelectorIfNeeded( + selector, + selectorCapabilities, + columnSelectorFactory.getRowIdSupplier(), + desiredType + ); + } + + /** + * Wraps a {@link ColumnValueSelector} with a type casting selector if necessary. If typecasting is not necessary, + * returns the original selector. + * + * @param selector selector + * @param selectorCapabilities capabilities for the selector, from {@link ColumnSelectorFactory#getColumnCapabilities} + * @param rowIdSupplier row id supplier, from {@link ColumnSelectorFactory#getRowIdSupplier()} + * @param desiredType desired type for the returned selector + */ + public static ColumnValueSelector wrapColumnValueSelectorIfNeeded( + final ColumnValueSelector selector, + @Nullable final ColumnCapabilities selectorCapabilities, + @Nullable final RowIdSupplier rowIdSupplier, + final ColumnType desiredType + ) + { + final ExpressionType desiredExpressionType = ExpressionType.fromColumnType(desiredType); + + if (desiredType.is(ValueType.STRING)) { + throw DruidException.defensive("Type[%s] should be read using a DimensionSelector", desiredType); + } else if (desiredType.isNumeric() + && (selectorCapabilities == null || !selectorCapabilities.isNumeric())) { + // When capabilities are unknown, or known to be non-numeric, fall back to getObject() and explicit typecasting. + // This avoids using primitive numeric accessors (getLong / getDouble / getFloat / isNull) on a selector that + // may not support them. + return new ObjectToNumberColumnValueSelector(selector, desiredExpressionType, rowIdSupplier); + } else if (desiredType.isArray()) { + // Always wrap if desiredType is an array. Even if the underlying selector claims to offer the same type as + // desiredType, it may fail to respect the BaseObjectColumnValueSelector contract. For example, it may return + // List rather than Object[]. (RowBasedColumnSelectorFactory can do this if used incorrectly, i.e., if the + // ColumnInspector declares type ARRAY for a column, but the RowAdapter does not provide Object[].) + return new ObjectToArrayColumnValueSelector(selector, desiredExpressionType, rowIdSupplier); + } else { + // OK to return the original selector. + return selector; + } + } + + /** + * Coerce an object to an object compatible with what {@link BaseObjectColumnValueSelector#getObject()} for a column + * of the provided desiredType. + * + * @param obj object + * @param desiredType desired type + */ + @Nullable + public static Object bestEffortCoerce( + @Nullable final Object obj, + @Nullable final ExpressionType desiredType + ) + { + if (obj == null || desiredType == null) { + return obj; + } + + return ExprEval.ofType(desiredType, obj).value(); + } +} diff --git a/processing/src/main/java/org/apache/druid/frame/write/columnar/FrameColumnWriters.java b/processing/src/main/java/org/apache/druid/frame/write/columnar/FrameColumnWriters.java index 93f0c12bae6f..6efe1870816a 100644 --- a/processing/src/main/java/org/apache/druid/frame/write/columnar/FrameColumnWriters.java +++ b/processing/src/main/java/org/apache/druid/frame/write/columnar/FrameColumnWriters.java @@ -21,6 +21,7 @@ import org.apache.druid.frame.allocation.MemoryAllocator; import org.apache.druid.frame.write.UnsupportedColumnTypeException; +import org.apache.druid.frame.write.cast.TypeCastSelectors; import org.apache.druid.java.util.common.ISE; import org.apache.druid.query.dimension.DefaultDimensionSpec; import org.apache.druid.segment.ColumnSelectorFactory; @@ -102,7 +103,8 @@ private static LongFrameColumnWriter makeLongWriter( ) { final ColumnCapabilities capabilities = selectorFactory.getColumnCapabilities(columnName); - final ColumnValueSelector selector = selectorFactory.makeColumnValueSelector(columnName); + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(selectorFactory, columnName, ColumnType.LONG); return new LongFrameColumnWriter(selector, allocator, hasNullsForNumericWriter(capabilities)); } @@ -113,7 +115,8 @@ private static FloatFrameColumnWriter makeFloatWriter( ) { final ColumnCapabilities capabilities = selectorFactory.getColumnCapabilities(columnName); - final ColumnValueSelector selector = selectorFactory.makeColumnValueSelector(columnName); + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(selectorFactory, columnName, ColumnType.FLOAT); return new FloatFrameColumnWriter(selector, allocator, hasNullsForNumericWriter(capabilities)); } @@ -124,7 +127,8 @@ private static DoubleFrameColumnWriter makeDoubleWriter( ) { final ColumnCapabilities capabilities = selectorFactory.getColumnCapabilities(columnName); - final ColumnValueSelector selector = selectorFactory.makeColumnValueSelector(columnName); + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(selectorFactory, columnName, ColumnType.DOUBLE); return new DoubleFrameColumnWriter(selector, allocator, hasNullsForNumericWriter(capabilities)); } @@ -149,7 +153,8 @@ private static StringFrameColumnWriter makeStringArrayWriter( final String columnName ) { - final ColumnValueSelector selector = selectorFactory.makeColumnValueSelector(columnName); + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(selectorFactory, columnName, ColumnType.STRING_ARRAY); return new StringArrayFrameColumnWriterImpl(selector, allocator); } @@ -159,7 +164,8 @@ private static NumericArrayFrameColumnWriter makeLongArrayWriter( final String columnName ) { - final ColumnValueSelector selector = selectorFactory.makeColumnValueSelector(columnName); + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(selectorFactory, columnName, ColumnType.LONG_ARRAY); return new LongArrayFrameColumnWriter(selector, allocator); } @@ -169,7 +175,8 @@ private static NumericArrayFrameColumnWriter makeFloatArrayWriter( final String columnName ) { - final ColumnValueSelector selector = selectorFactory.makeColumnValueSelector(columnName); + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(selectorFactory, columnName, ColumnType.FLOAT_ARRAY); return new FloatArrayFrameColumnWriter(selector, allocator); } @@ -179,7 +186,8 @@ private static NumericArrayFrameColumnWriter makeDoubleArrayWriter( final String columnName ) { - final ColumnValueSelector selector = selectorFactory.makeColumnValueSelector(columnName); + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(selectorFactory, columnName, ColumnType.DOUBLE_ARRAY); return new DoubleArrayFrameColumnWriter(selector, allocator); } @@ -199,7 +207,8 @@ private static ComplexFrameColumnWriter makeComplexWriter( throw new ISE("No serde for complexTypeName[%s], cannot write column [%s]", columnTypeName, columnName); } - final ColumnValueSelector selector = selectorFactory.makeColumnValueSelector(columnName); + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(selectorFactory, columnName, ColumnType.ofComplex(columnTypeName)); return new ComplexFrameColumnWriter(selector, allocator, serde); } diff --git a/processing/src/main/java/org/apache/druid/frame/write/columnar/NumericArrayFrameColumnWriter.java b/processing/src/main/java/org/apache/druid/frame/write/columnar/NumericArrayFrameColumnWriter.java index 619bf53b8d3d..432ce8b97aa3 100644 --- a/processing/src/main/java/org/apache/druid/frame/write/columnar/NumericArrayFrameColumnWriter.java +++ b/processing/src/main/java/org/apache/druid/frame/write/columnar/NumericArrayFrameColumnWriter.java @@ -24,11 +24,8 @@ import org.apache.druid.frame.allocation.AppendableMemory; import org.apache.druid.frame.allocation.MemoryAllocator; import org.apache.druid.frame.allocation.MemoryRange; -import org.apache.druid.frame.write.FrameWriterUtils; import org.apache.druid.segment.ColumnValueSelector; -import java.util.List; - /** * Parent class for the family of writers writing numeric arrays in columnar frames. Since the numeric primitives are * fixed width, we don't need to store the width of each element. The memory layout of a column written by this writer @@ -119,8 +116,8 @@ public NumericArrayFrameColumnWriter( @Override public boolean addSelection() { - List numericArray = FrameWriterUtils.getNumericArrayFromObject(selector.getObject()); - int rowLength = numericArray == null ? 0 : numericArray.size(); + final Object[] row = (Object[]) selector.getObject(); + int rowLength = row == null ? 0 : row.length; // Begin memory allocations before writing if ((long) lastCumulativeRowLength + rowLength > Integer.MAX_VALUE) { @@ -142,7 +139,7 @@ public boolean addSelection() final MemoryRange rowLengthsCursor = cumulativeRowLengths.cursor(); - if (numericArray == null) { + if (row == null) { rowLengthsCursor.memory().putInt(rowLengthsCursor.start(), -(lastCumulativeRowLength + rowLength) - 1); } else { rowLengthsCursor.memory().putInt(rowLengthsCursor.start(), lastCumulativeRowLength + rowLength); @@ -155,7 +152,7 @@ public boolean addSelection() final MemoryRange rowDataCursor = rowLength > 0 ? rowData.cursor() : null; for (int i = 0; i < rowLength; ++i) { - final Number element = numericArray.get(i); + final Number element = (Number) row[i]; final long memoryOffset = rowDataCursor.start() + ((long) elementSizeBytes() * i); if (element == null) { rowNullityDataCursor.memory() diff --git a/processing/src/main/java/org/apache/druid/math/expr/ExprEval.java b/processing/src/main/java/org/apache/druid/math/expr/ExprEval.java index 4bde15192217..a16b563c77ca 100644 --- a/processing/src/main/java/org/apache/druid/math/expr/ExprEval.java +++ b/processing/src/main/java/org/apache/druid/math/expr/ExprEval.java @@ -406,7 +406,7 @@ public static ExprEval bestEffortArray(@Nullable List theList) public static ExprEval bestEffortOf(@Nullable Object val) { if (val == null) { - return new StringExprEval(null); + return StringExprEval.OF_NULL; } if (val instanceof ExprEval) { return (ExprEval) val; @@ -519,6 +519,12 @@ public static ExprEval bestEffortOf(@Nullable Object val) return ofComplex(ExpressionType.UNKNOWN_COMPLEX, val); } + /** + * Create an eval of the provided type. Coerces the provided object to the desired type. + * + * @param type type, or null to be equivalent to {@link #bestEffortOf(Object)} + * @param value object to be coerced to the type + */ public static ExprEval ofType(@Nullable ExpressionType type, @Nullable Object value) { if (type == null) { @@ -1145,30 +1151,31 @@ public final ExprEval castTo(ExpressionType castTo) switch (castTo.getType()) { case DOUBLE: return ExprEval.ofDouble(computeNumber()); + case LONG: return ExprEval.ofLong(computeNumber()); + case STRING: return this; + case ARRAY: if (value == null) { return new ArrayExprEval(castTo, null); } - final Number number = computeNumber(); - switch (castTo.getElementType().getType()) { - case DOUBLE: - return ExprEval.ofDoubleArray( - new Object[]{number == null ? null : number.doubleValue()} - ); - case LONG: - return ExprEval.ofLongArray( - new Object[]{number == null ? null : number.longValue()} - ); - case STRING: - return ExprEval.ofStringArray(new Object[]{value}); - default: - ExpressionType elementType = (ExpressionType) castTo.getElementType(); - return new ArrayExprEval(castTo, new Object[]{castTo(elementType).value()}); + ExprType type = castTo.getElementType().getType(); + if (type == ExprType.DOUBLE) { + final Number number = computeNumber(); + return ExprEval.ofDoubleArray(new Object[]{number == null ? null : number.doubleValue()}); + } else if (type == ExprType.LONG) { + final Number number = computeNumber(); + return ExprEval.ofLongArray(new Object[]{number == null ? null : number.longValue()}); + } else if (type == ExprType.STRING) { + return ExprEval.ofStringArray(new Object[]{value}); } + + ExpressionType elementType = (ExpressionType) castTo.getElementType(); + return new ArrayExprEval(castTo, new Object[]{castTo(elementType).value()}); + case COMPLEX: if (ExpressionType.NESTED_DATA.equals(castTo)) { return new NestedDataExprEval(value); diff --git a/processing/src/test/java/org/apache/druid/frame/field/StringArrayFieldWriterTest.java b/processing/src/test/java/org/apache/druid/frame/field/StringArrayFieldWriterTest.java index 6aba25ddf22c..44d8ed5a26d4 100644 --- a/processing/src/test/java/org/apache/druid/frame/field/StringArrayFieldWriterTest.java +++ b/processing/src/test/java/org/apache/druid/frame/field/StringArrayFieldWriterTest.java @@ -50,7 +50,7 @@ public class StringArrayFieldWriterTest extends InitializedNullHandlingTest public MockitoRule mockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS); @Mock - public BaseObjectColumnValueSelector> selector; + public BaseObjectColumnValueSelector selector; private WritableMemory memory; private FieldWriter fieldWriter; @@ -115,7 +115,8 @@ private void doTest(@Nullable final List values) private void mockSelector(@Nullable final List values) { - Mockito.when(selector.getObject()).thenReturn(values); + final Object[] arr = values == null ? null : values.toArray(); + Mockito.when(selector.getObject()).thenReturn(arr); } private long writeToMemory(final FieldWriter writer) diff --git a/processing/src/test/java/org/apache/druid/frame/field/StringFieldReaderTest.java b/processing/src/test/java/org/apache/druid/frame/field/StringFieldReaderTest.java index b0f589ed4804..200c469269ff 100644 --- a/processing/src/test/java/org/apache/druid/frame/field/StringFieldReaderTest.java +++ b/processing/src/test/java/org/apache/druid/frame/field/StringFieldReaderTest.java @@ -60,7 +60,7 @@ public class StringFieldReaderTest extends InitializedNullHandlingTest public MockitoRule mockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS); @Mock - public BaseObjectColumnValueSelector> writeSelector; + public BaseObjectColumnValueSelector writeSelector; private WritableMemory memory; private FieldWriter fieldWriter; @@ -277,7 +277,8 @@ public void test_makeDimensionSelector_multiString_withExtractionFn() private void writeToMemory(@Nullable final List values) { - Mockito.when(writeSelector.getObject()).thenReturn(values); + final Object[] arr = values == null ? null : values.toArray(); + Mockito.when(writeSelector.getObject()).thenReturn(arr); if (fieldWriter.writeTo(memory, MEMORY_POSITION, memory.getCapacity() - MEMORY_POSITION) < 0) { throw new ISE("Could not write"); diff --git a/processing/src/test/java/org/apache/druid/frame/write/cast/TypeCastSelectorsTest.java b/processing/src/test/java/org/apache/druid/frame/write/cast/TypeCastSelectorsTest.java new file mode 100644 index 000000000000..dc2a4a5b46cf --- /dev/null +++ b/processing/src/test/java/org/apache/druid/frame/write/cast/TypeCastSelectorsTest.java @@ -0,0 +1,394 @@ +/* + * 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.frame.write.cast; + +import com.google.common.collect.ImmutableMap; +import org.apache.druid.error.DruidException; +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.DimensionSelector; +import org.apache.druid.segment.column.ColumnCapabilities; +import org.apache.druid.segment.column.ColumnType; +import org.apache.druid.segment.column.RowSignature; +import org.apache.druid.testing.InitializedNullHandlingTest; +import org.junit.Assert; +import org.junit.Test; + +import javax.annotation.Nullable; +import java.util.Map; + +public class TypeCastSelectorsTest extends InitializedNullHandlingTest +{ + private final ColumnSelectorFactory testColumnSelectorFactory = new TestColumnSelectorFactory( + RowSignature.builder() + .add("x", ColumnType.STRING) + .add("y", ColumnType.STRING) + .add("z", ColumnType.STRING) + .add("da", ColumnType.DOUBLE_ARRAY) + .build(), + ImmutableMap.builder() + .put("x", "12.3") + .put("y", "abc") + .put("da", new Object[]{1.2d, 2.3d}) + .build() // z is null + ); + + @Test + public void test_readXAsLong() + { + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(testColumnSelectorFactory, "x", ColumnType.LONG); + + Assert.assertEquals(12L, selector.getLong()); + Assert.assertEquals(12d, selector.getDouble(), 0); + Assert.assertEquals(12f, selector.getFloat(), 0); + Assert.assertFalse(selector.isNull()); + Assert.assertEquals(12L, selector.getObject()); + } + + @Test + public void test_readXAsDouble() + { + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(testColumnSelectorFactory, "x", ColumnType.DOUBLE); + + Assert.assertEquals(12L, selector.getLong()); + Assert.assertEquals(12.3d, selector.getDouble(), 0); + Assert.assertEquals(12.3f, selector.getFloat(), 0); + Assert.assertFalse(selector.isNull()); + Assert.assertEquals(12.3d, selector.getObject()); + } + + @Test + public void test_readXAsFloat() + { + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(testColumnSelectorFactory, "x", ColumnType.FLOAT); + + Assert.assertEquals(12L, selector.getLong()); + Assert.assertEquals(12.3d, selector.getDouble(), 0.001); + Assert.assertEquals(12.3f, selector.getFloat(), 0); + Assert.assertFalse(selector.isNull()); + Assert.assertEquals(12.3d, selector.getObject()); + } + + @Test + public void test_readXAsLongArray() + { + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(testColumnSelectorFactory, "x", ColumnType.LONG_ARRAY); + + Assert.assertThrows(DruidException.class, selector::getLong); + Assert.assertThrows(DruidException.class, selector::getDouble); + Assert.assertThrows(DruidException.class, selector::getFloat); + Assert.assertThrows(DruidException.class, selector::isNull); + Assert.assertArrayEquals(new Object[]{12L}, (Object[]) selector.getObject()); + } + + @Test + public void test_readXAsStringArray() + { + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(testColumnSelectorFactory, "x", ColumnType.STRING_ARRAY); + + Assert.assertThrows(DruidException.class, selector::getLong); + Assert.assertThrows(DruidException.class, selector::getDouble); + Assert.assertThrows(DruidException.class, selector::getFloat); + Assert.assertThrows(DruidException.class, selector::isNull); + Assert.assertArrayEquals(new Object[]{"12.3"}, (Object[]) selector.getObject()); + } + + @Test + public void test_readYAsLong() + { + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(testColumnSelectorFactory, "y", ColumnType.LONG); + + Assert.assertEquals(0L, selector.getLong()); + Assert.assertEquals(0d, selector.getDouble(), 0); + Assert.assertEquals(0f, selector.getFloat(), 0); + Assert.assertTrue(selector.isNull()); + Assert.assertNull(selector.getObject()); + } + + @Test + public void test_readYAsDouble() + { + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(testColumnSelectorFactory, "y", ColumnType.DOUBLE); + + Assert.assertEquals(0L, selector.getLong()); + Assert.assertEquals(0d, selector.getDouble(), 0); + Assert.assertEquals(0f, selector.getFloat(), 0); + Assert.assertTrue(selector.isNull()); + Assert.assertNull(selector.getObject()); + } + + @Test + public void test_readYAsFloat() + { + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(testColumnSelectorFactory, "y", ColumnType.FLOAT); + + Assert.assertEquals(0L, selector.getLong()); + Assert.assertEquals(0d, selector.getDouble(), 0); + Assert.assertEquals(0f, selector.getFloat(), 0); + Assert.assertTrue(selector.isNull()); + Assert.assertNull(selector.getObject()); + } + + @Test + public void test_readYAsLongArray() + { + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(testColumnSelectorFactory, "y", ColumnType.LONG_ARRAY); + + Assert.assertThrows(DruidException.class, selector::getLong); + Assert.assertThrows(DruidException.class, selector::getDouble); + Assert.assertThrows(DruidException.class, selector::getFloat); + Assert.assertThrows(DruidException.class, selector::isNull); + Assert.assertArrayEquals(new Object[]{null}, (Object[]) selector.getObject()); + } + + @Test + public void test_readYAsStringArray() + { + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(testColumnSelectorFactory, "y", ColumnType.STRING_ARRAY); + + Assert.assertThrows(DruidException.class, selector::getLong); + Assert.assertThrows(DruidException.class, selector::getDouble); + Assert.assertThrows(DruidException.class, selector::getFloat); + Assert.assertThrows(DruidException.class, selector::isNull); + Assert.assertArrayEquals(new Object[]{"abc"}, (Object[]) selector.getObject()); + } + + @Test + public void test_readZAsLong() + { + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(testColumnSelectorFactory, "z", ColumnType.LONG); + + Assert.assertEquals(0L, selector.getLong()); + Assert.assertEquals(0d, selector.getDouble(), 0); + Assert.assertEquals(0f, selector.getFloat(), 0); + Assert.assertTrue(selector.isNull()); + Assert.assertNull(selector.getObject()); + } + + @Test + public void test_readZAsDouble() + { + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(testColumnSelectorFactory, "z", ColumnType.DOUBLE); + + Assert.assertEquals(0L, selector.getLong()); + Assert.assertEquals(0d, selector.getDouble(), 0); + Assert.assertEquals(0f, selector.getFloat(), 0); + Assert.assertTrue(selector.isNull()); + Assert.assertNull(selector.getObject()); + } + + @Test + public void test_readZAsFloat() + { + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(testColumnSelectorFactory, "z", ColumnType.FLOAT); + + Assert.assertEquals(0L, selector.getLong()); + Assert.assertEquals(0d, selector.getDouble(), 0); + Assert.assertEquals(0f, selector.getFloat(), 0); + Assert.assertTrue(selector.isNull()); + Assert.assertNull(selector.getObject()); + } + + @Test + public void test_readZAsLongArray() + { + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(testColumnSelectorFactory, "z", ColumnType.LONG_ARRAY); + + Assert.assertThrows(DruidException.class, selector::getLong); + Assert.assertThrows(DruidException.class, selector::getDouble); + Assert.assertThrows(DruidException.class, selector::getFloat); + Assert.assertThrows(DruidException.class, selector::isNull); + Assert.assertNull(selector.getObject()); + } + + @Test + public void test_readZAsStringArray() + { + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(testColumnSelectorFactory, "z", ColumnType.STRING_ARRAY); + + Assert.assertThrows(DruidException.class, selector::getLong); + Assert.assertThrows(DruidException.class, selector::getDouble); + Assert.assertThrows(DruidException.class, selector::getFloat); + Assert.assertThrows(DruidException.class, selector::isNull); + Assert.assertNull(selector.getObject()); + } + + @Test + public void test_readDaAsLong() + { + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(testColumnSelectorFactory, "da", ColumnType.LONG); + + Assert.assertEquals(0L, selector.getLong()); + Assert.assertEquals(0d, selector.getDouble(), 0); + Assert.assertEquals(0f, selector.getFloat(), 0); + Assert.assertTrue(selector.isNull()); + Assert.assertNull(selector.getObject()); + } + + @Test + public void test_readDaAsDouble() + { + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(testColumnSelectorFactory, "da", ColumnType.DOUBLE); + + Assert.assertEquals(0L, selector.getLong()); + Assert.assertEquals(0d, selector.getDouble(), 0); + Assert.assertEquals(0f, selector.getFloat(), 0); + Assert.assertTrue(selector.isNull()); + Assert.assertNull(selector.getObject()); + } + + @Test + public void test_readDaAsFloat() + { + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(testColumnSelectorFactory, "da", ColumnType.FLOAT); + + Assert.assertEquals(0L, selector.getLong()); + Assert.assertEquals(0d, selector.getDouble(), 0); + Assert.assertEquals(0f, selector.getFloat(), 0); + Assert.assertTrue(selector.isNull()); + Assert.assertNull(selector.getObject()); + } + + @Test + public void test_readDaAsLongArray() + { + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(testColumnSelectorFactory, "da", ColumnType.LONG_ARRAY); + + Assert.assertThrows(DruidException.class, selector::getLong); + Assert.assertThrows(DruidException.class, selector::getDouble); + Assert.assertThrows(DruidException.class, selector::getFloat); + Assert.assertThrows(DruidException.class, selector::isNull); + Assert.assertArrayEquals(new Object[]{1L, 2L}, (Object[]) selector.getObject()); + } + + @Test + public void test_readDaAsStringArray() + { + final ColumnValueSelector selector = + TypeCastSelectors.makeColumnValueSelector(testColumnSelectorFactory, "da", ColumnType.STRING_ARRAY); + + Assert.assertThrows(DruidException.class, selector::getLong); + Assert.assertThrows(DruidException.class, selector::getDouble); + Assert.assertThrows(DruidException.class, selector::getFloat); + Assert.assertThrows(DruidException.class, selector::isNull); + Assert.assertArrayEquals(new Object[]{"1.2", "2.3"}, (Object[]) selector.getObject()); + } + + /** + * Implementation that returns a fixed value per column from {@link ColumnValueSelector#getObject()}. Other + * methods, such as {@link ColumnValueSelector#getLong()} throw exceptions. This is meant to help validate + * that those other methods are *not* called. + */ + private static class TestColumnSelectorFactory implements ColumnSelectorFactory + { + private final RowSignature signature; + private final Map columnValues; + + public TestColumnSelectorFactory(final RowSignature signature, final Map columnValues) + { + this.signature = signature; + this.columnValues = columnValues; + } + + @Override + public DimensionSelector makeDimensionSelector(DimensionSpec dimensionSpec) + { + throw new UnsupportedOperationException(); + } + + @Override + public ColumnValueSelector makeColumnValueSelector(String columnName) + { + return new ColumnValueSelector() + { + @Override + public double getDouble() + { + throw new UnsupportedOperationException("Should not be called"); + } + + @Override + public float getFloat() + { + throw new UnsupportedOperationException("Should not be called"); + } + + @Override + public long getLong() + { + throw new UnsupportedOperationException("Should not be called"); + } + + @Override + public void inspectRuntimeShape(RuntimeShapeInspector inspector) + { + throw new UnsupportedOperationException("Should not be called"); + } + + @Override + public boolean isNull() + { + throw new UnsupportedOperationException("Should not be called"); + } + + @Nullable + @Override + public Object getObject() + { + return columnValues.get(columnName); + } + + @Override + public Class classOfObject() + { + throw new UnsupportedOperationException("Should not be called"); + } + }; + } + + @Nullable + @Override + public ColumnCapabilities getColumnCapabilities(String column) + { + return signature.getColumnCapabilities(column); + } + } +}