diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/ComputeSignatureHelper.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/ComputeSignatureHelper.java index 0d0abe92789834..7a441d7578557d 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/ComputeSignatureHelper.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/ComputeSignatureHelper.java @@ -56,11 +56,14 @@ import java.util.Optional; import java.util.Set; import java.util.function.BiFunction; -import java.util.stream.Collectors; /** ComputeSignatureHelper */ public class ComputeSignatureHelper { + private static final String MAP_KEY = "key"; + private static final String MAP_VALUE = "value"; + private static final String ARRAY_ITEM = "array"; + /** implementAbstractReturnType */ public static FunctionSignature implementFollowToArgumentReturnType( FunctionSignature signature, List arguments) { @@ -471,7 +474,7 @@ public static FunctionSignature computePrecision( } if (hasDecimalV3Type) { // do decimal v3 precision - signature = defaultDecimalV3PrecisionPromotion(signature, arguments); + signature = defaultDecimalV3PrecisionPromotion(signature, arguments, computeSignature); } return signature; } @@ -566,52 +569,363 @@ private static FunctionSignature defaultTimePrecisionPromotion(FunctionSignature } private static FunctionSignature defaultDecimalV3PrecisionPromotion( - FunctionSignature signature, List arguments) { - DecimalV3Type finalType = null; + FunctionSignature signature, List arguments, ComputeSignature computeSignature) { + // The wider type across all decimal slots, used for decimal slots that are not + // inside a MAP (keeping the original behavior), for the placeholder return type, + // and for MAP-nested leaves whose group has no concrete type information. + DecimalV3Type widerType = null; + + // Decimal leaves inside a MAP are independent type variables: they must keep + // their own precision/scale instead of being merged into one wider type, + // otherwise widening one leaf (e.g. the scale of a big integral key) may overflow + // the other leaf. They are grouped by the full structural path through nested + // containers (e.g. "key", "value", "value/array", "value/key") and the resolved + // leaf type, so the leaves of different (or repeated) MAP arguments on the same + // path aggregate while leaves on different paths stay independent. + Map groupWider = Maps.newHashMap(); + + // The template signature carrying the original Any/Follow slots that the resolved + // signature was derived from. It lets us link a top-level scalar slot with the MAP + // leaf it belongs to by the original Any/Follow group identity (the index) instead + // of the resolved concrete type, which can collide when independent slots resolve + // to the same type (e.g. the key and the value of a MAP both becoming DECIMAL(10,3)). + FunctionSignature template = findDecimalV3Template(computeSignature, signature); + + // The outermost MAP leaf group of each Any/Follow index (from the template), used + // to link a top-level scalar slot (e.g. map_contains_value's probe, element_at's + // lookup) with the MAP leaf that carries the same index. + Map indexToMapLeafGroup = Maps.newHashMap(); + + // Fallback used when the template can not be recovered: the outermost MAP leaf + // group of each resolved type, used to link a top-level scalar slot with the MAP + // leaf it was resolved from (after Any/Follow resolution both carry the same type). + Map mapLeafGroupByType = Maps.newHashMap(); + + // Top-level scalar decimal leaves with a concrete resolved type, whose promoted + // type must also be folded into the linked MAP leaf group. + List scalarLeaves = Lists.newArrayList(); + + // Top-level scalar decimal slots are independent logical type variables + // (e.g. the key/value of map_agg(k, v) are Any(0) and Any(1)); group them by + // the resolved type so the slots of one logical group aggregate while the slots + // of different groups keep their own precision/scale. + Map scalarGroupWider = Maps.newHashMap(); + + DecimalV3Type[] widerHolder = new DecimalV3Type[1]; for (int i = 0; i < arguments.size(); i++) { - DataType targetType; - if (i >= signature.argumentsTypes.size()) { - Preconditions.checkState(signature.getVarArgType().isPresent(), - "argument size larger than signature"); - targetType = signature.getVarArgType().get(); + DataType targetType = getSignatureArgumentType(signature, i); + DataType templateType = template == null ? null : getSignatureArgumentType(template, i); + collectDecimalLeaf(targetType, arguments.get(i).getDataType(), arguments.get(i), + "", templateType, indexToMapLeafGroup, mapLeafGroupByType, groupWider, + scalarGroupWider, scalarLeaves, widerHolder); + } + widerType = widerHolder[0]; + if (widerType == null) { + return signature; + } + + // Fold the promoted type of every top-level scalar slot into the MAP leaf group it + // is linked with (by the original Any/Follow identity when available, otherwise by + // the resolved type), so the MAP leaf and the scalar slot linked with it are + // promoted to one type. + for (DecimalLeaf scalarLeaf : scalarLeaves) { + String linkedGroup; + if (scalarLeaf.index >= 0) { + linkedGroup = indexToMapLeafGroup.get(scalarLeaf.index); } else { - targetType = signature.getArgType(i); + linkedGroup = mapLeafGroupByType.get(scalarLeaf.resolvedType); } - List argTypes = extractArgumentTypeBySignature(DecimalV3Type.class, targetType, - arguments.get(i).getDataType()); - if (argTypes.isEmpty()) { - continue; + if (linkedGroup != null) { + groupWider.merge(linkedGroup, scalarLeaf.promotedType, + ComputeSignatureHelper::mergeDecimalV3Type); } + } - for (DataType argType : argTypes) { - Expression arg = arguments.get(i); - DecimalV3Type decimalV3Type; - if (arg.isLiteral() && arg.getDataType().isIntegralType()) { - // create decimalV3 with minimum scale enough to hold the integral literal - decimalV3Type = DecimalV3Type.createDecimalV3Type(new BigDecimal(((Literal) arg).getStringValue())); - } else { - decimalV3Type = DecimalV3Type.forType(argType); + List newArgTypes = Lists.newArrayListWithCapacity(signature.argumentsTypes.size()); + for (int i = 0; i < signature.argumentsTypes.size(); i++) { + DataType templateType = template == null ? null : getSignatureArgumentType(template, i); + newArgTypes.add(replaceDecimalV3Leaf(signature.argumentsTypes.get(i), "", templateType, + indexToMapLeafGroup, mapLeafGroupByType, groupWider, scalarGroupWider, widerType)); + } + signature = signature.withArgumentTypes(signature.hasVarArgs, newArgTypes); + if (signature.returnType instanceof DecimalV3Type + && ((DecimalV3Type) signature.returnType).getPrecision() <= 0) { + signature = signature.withReturnType(widerType); + } + return signature; + } + + private static DataType getSignatureArgumentType(FunctionSignature signature, int index) { + if (index >= signature.argumentsTypes.size()) { + Preconditions.checkState(signature.getVarArgType().isPresent(), + "argument size larger than signature"); + return signature.getVarArgType().get(); + } + return signature.getArgType(index); + } + + /** + * Compute the promoted DecimalV3Type for one decimal slot from its argument type. + */ + private static DecimalV3Type promotedDecimalV3Type(Expression arg, DataType argType) { + if (arg.isLiteral() && arg.getDataType().isIntegralType()) { + // create decimalV3 with minimum scale enough to hold the integral literal + return DecimalV3Type.createDecimalV3Type(new BigDecimal(((Literal) arg).getStringValue())); + } + return DecimalV3Type.forType(argType); + } + + /** + * Collect every decimal leaf of one argument and fold its promoted type into the + * corresponding group. {@code path} is the full structural path through nested + * containers (empty for a top-level slot, {@link #MAP_KEY}/{@link #MAP_VALUE} for + * the key/value of a MAP, {@link #ARRAY_ITEM} for an ARRAY item), so an ARRAY nested + * in a MAP value (e.g. "value/array") or the key/value of a nested MAP (e.g. + * "value/key") keep the enclosing group instead of being merged with the outer + * leaves. {@code templateType} is the corresponding slot of the template signature + * that still carries the original Any/Follow identity of this leaf. {@code widerHolder} + * accumulates the wider type across all decimal leaves. + */ + private static void collectDecimalLeaf(DataType sigType, DataType argType, Expression arg, + String path, DataType templateType, Map indexToMapLeafGroup, + Map mapLeafGroupByType, + Map groupWider, Map scalarGroupWider, + List scalarLeaves, DecimalV3Type[] widerHolder) { + if (sigType instanceof DecimalV3Type) { + DecimalV3Type sigDecimal = (DecimalV3Type) sigType; + DecimalV3Type promoted = null; + if (!(argType instanceof NullType)) { + promoted = promotedDecimalV3Type(arg, argType); + widerHolder[0] = mergeDecimalV3Type(widerHolder[0], promoted); + } + if (path.isEmpty()) { + // top-level scalar slot: a concrete resolved type may be linked with a + // MAP leaf below by the original Any/Follow identity, and otherwise the + // slots of the same resolved type form one logical group (e.g. the two + // arguments of map_agg) and stay independent from the slots of other groups + if (promoted != null && sigDecimal.getPrecision() > 0) { + scalarLeaves.add(new DecimalLeaf(sigDecimal, promoted, anyFollowIndex(templateType))); + scalarGroupWider.merge(sigDecimal, promoted, + ComputeSignatureHelper::mergeDecimalV3Type); } - if (finalType == null) { - finalType = decimalV3Type; + } else if (isMapNested(path) && promoted != null) { + String groupKey = path + ":" + sigDecimal; + groupWider.merge(groupKey, promoted, ComputeSignatureHelper::mergeDecimalV3Type); + int index = anyFollowIndex(templateType); + if (index >= 0) { + // keep the outermost group (shortest path) for linking by the index + indexToMapLeafGroup.putIfAbsent(index, groupKey); } else { - finalType = (DecimalV3Type) DecimalV3Type.widerDecimalV3Type(finalType, decimalV3Type, false); + // fallback: keep the outermost group (shortest path, key before value) + // for linking by the resolved type + mapLeafGroupByType.putIfAbsent(sigDecimal, groupKey); + } + } + // other leaves (e.g. ARRAY items not nested in a MAP) keep the original + // behavior of the single wider type + return; + } else if (sigType instanceof MapType) { + MapType mapType = (MapType) sigType; + DataType templateKey = templateType instanceof MapType + ? ((MapType) templateType).getKeyType() : null; + DataType templateValue = templateType instanceof MapType + ? ((MapType) templateType).getValueType() : null; + if (argType instanceof MapType) { + MapType argMapType = (MapType) argType; + collectDecimalLeaf(mapType.getKeyType(), argMapType.getKeyType(), arg, + appendPath(path, MAP_KEY), templateKey, indexToMapLeafGroup, mapLeafGroupByType, + groupWider, scalarGroupWider, scalarLeaves, widerHolder); + collectDecimalLeaf(mapType.getValueType(), argMapType.getValueType(), arg, + appendPath(path, MAP_VALUE), templateValue, indexToMapLeafGroup, mapLeafGroupByType, + groupWider, scalarGroupWider, scalarLeaves, widerHolder); + } else if (argType instanceof NullType) { + collectDecimalLeaf(mapType.getKeyType(), argType, arg, + appendPath(path, MAP_KEY), templateKey, indexToMapLeafGroup, mapLeafGroupByType, + groupWider, scalarGroupWider, scalarLeaves, widerHolder); + collectDecimalLeaf(mapType.getValueType(), argType, arg, + appendPath(path, MAP_VALUE), templateValue, indexToMapLeafGroup, mapLeafGroupByType, + groupWider, scalarGroupWider, scalarLeaves, widerHolder); + } + return; + } else if (sigType instanceof ArrayType) { + DataType itemArgType; + if (argType instanceof ArrayType) { + itemArgType = ((ArrayType) argType).getItemType(); + } else if (argType instanceof NullType) { + itemArgType = argType; + } else { + return; + } + // carry the enclosing MAP path through the ARRAY so items nested in a MAP + // value stay in the value group + DataType templateItem = templateType instanceof ArrayType + ? ((ArrayType) templateType).getItemType() : null; + collectDecimalLeaf(((ArrayType) sigType).getItemType(), itemArgType, arg, + appendPath(path, ARRAY_ITEM), templateItem, indexToMapLeafGroup, mapLeafGroupByType, + groupWider, scalarGroupWider, scalarLeaves, widerHolder); + } + // StructType and other types are not supported + } + + /** + * Replace every decimal leaf in {@code sigType}: leaves inside a MAP use the wider + * type of their own structural group, top-level scalar slots use the wider type of + * their own logical group (slots of the same resolved type), and all other leaves + * (e.g. ARRAY items not nested in a MAP) keep the original behavior of using the + * single wider type across all decimal slots. + */ + private static DataType replaceDecimalV3Leaf(DataType sigType, String path, DataType templateType, + Map indexToMapLeafGroup, Map mapLeafGroupByType, + Map groupWider, Map scalarGroupWider, + DecimalV3Type widerType) { + if (sigType instanceof DecimalV3Type) { + DecimalV3Type sigDecimal = (DecimalV3Type) sigType; + if (path.isEmpty()) { + // a top-level scalar slot linked with a MAP leaf keeps the type of that + // leaf (e.g. map_contains_value's probe / element_at's lookup must match + // the MAP value/key type). The link is resolved by the original Any/Follow + // identity, falling back to the resolved type when the template can not be + // recovered. + if (sigDecimal.getPrecision() > 0) { + String linkedGroup = null; + int index = anyFollowIndex(templateType); + if (index >= 0) { + linkedGroup = indexToMapLeafGroup.get(index); + } else { + linkedGroup = mapLeafGroupByType.get(sigDecimal); + } + if (linkedGroup != null) { + DecimalV3Type linkedWider = groupWider.get(linkedGroup); + if (linkedWider != null) { + return linkedWider; + } + } + // independent logical Any groups (e.g. the key/value arguments of + // map_agg) keep their own precision/scale instead of being merged + // into one wider type + DecimalV3Type scalarWider = scalarGroupWider.get(sigDecimal); + if (scalarWider != null) { + return scalarWider; + } } + return widerType; + } + if (isMapNested(path)) { + DecimalV3Type groupType = groupWider.get(path + ":" + sigDecimal); + return groupType != null ? groupType : widerType; } + // other leaves (e.g. ARRAY items not nested in a MAP) keep the original + // behavior of the single wider type + return widerType; + } else if (sigType instanceof ArrayType) { + DataType templateItem = templateType instanceof ArrayType + ? ((ArrayType) templateType).getItemType() : null; + return ArrayType.of(replaceDecimalV3Leaf(((ArrayType) sigType).getItemType(), + appendPath(path, ARRAY_ITEM), templateItem, indexToMapLeafGroup, mapLeafGroupByType, + groupWider, scalarGroupWider, widerType)); + } else if (sigType instanceof MapType) { + MapType mapType = (MapType) sigType; + DataType templateKey = templateType instanceof MapType + ? ((MapType) templateType).getKeyType() : null; + DataType templateValue = templateType instanceof MapType + ? ((MapType) templateType).getValueType() : null; + return MapType.of( + replaceDecimalV3Leaf(mapType.getKeyType(), appendPath(path, MAP_KEY), + templateKey, indexToMapLeafGroup, mapLeafGroupByType, groupWider, + scalarGroupWider, widerType), + replaceDecimalV3Leaf(mapType.getValueType(), appendPath(path, MAP_VALUE), + templateValue, indexToMapLeafGroup, mapLeafGroupByType, groupWider, + scalarGroupWider, widerType)); } - DecimalV3Type argType = finalType; - if (finalType == null) { - return signature; + return sigType; + } + + private static String appendPath(String path, String segment) { + return path.isEmpty() ? segment : path + "/" + segment; + } + + private static boolean isMapNested(String path) { + return path.contains(MAP_KEY) || path.contains(MAP_VALUE); + } + + private static DecimalV3Type mergeDecimalV3Type(DecimalV3Type left, DecimalV3Type right) { + if (left == null) { + return right; } - List newArgTypes = signature.argumentsTypes.stream() - .map(at -> TypeCoercionUtils.replaceDecimalV3WithTarget(at, argType)) - .collect(Collectors.toList()); - signature = signature.withArgumentTypes(signature.hasVarArgs, newArgTypes); - if (signature.returnType instanceof DecimalV3Type - && ((DecimalV3Type) signature.returnType).getPrecision() <= 0) { - signature = signature.withReturnType(argType); + return (DecimalV3Type) DecimalV3Type.widerDecimalV3Type(left, right, false); + } + + /** A top-level scalar decimal leaf that may be linked with a MAP key/value leaf. */ + private static class DecimalLeaf { + final DecimalV3Type resolvedType; + final DecimalV3Type promotedType; + final int index; + + DecimalLeaf(DecimalV3Type resolvedType, DecimalV3Type promotedType, int index) { + this.resolvedType = resolvedType; + this.promotedType = promotedType; + this.index = index; } - return signature; + } + + /** + * The index of the original Any/Follow slot this (template) type carries, or -1 when + * it is not an Any/Follow slot. {@link AnyDataType#INSTANCE_WITHOUT_INDEX} has index + * -1, so MAP leaves declared without an index never take part in the scalar linking. + */ + private static int anyFollowIndex(DataType dataType) { + if (dataType instanceof AnyDataType) { + return ((AnyDataType) dataType).getIndex(); + } else if (dataType instanceof FollowToAnyDataType) { + return ((FollowToAnyDataType) dataType).getIndex(); + } + return -1; + } + + /** + * Recover the original signature (still carrying the Any/Follow slots) that the given + * resolved {@code signature} was derived from, by matching the arity and the slots + * that do not contain Any/Follow. Returns null when it can not be recovered, in which + * case the scalar linking falls back to the resolved concrete type. + */ + private static FunctionSignature findDecimalV3Template(ComputeSignature computeSignature, + FunctionSignature signature) { + List signatures = computeSignature.getSignatures(); + if (signatures == null) { + return null; + } + for (FunctionSignature candidate : signatures) { + if (candidate.hasVarArgs != signature.hasVarArgs || candidate.arity != signature.arity) { + continue; + } + boolean matched = true; + for (int i = 0; i < candidate.argumentsTypes.size(); i++) { + DataType candidateType = candidate.argumentsTypes.get(i); + if (containsAnyOrFollow(candidateType)) { + continue; + } + if (!candidateType.equals(signature.argumentsTypes.get(i))) { + matched = false; + break; + } + } + if (matched) { + return candidate; + } + } + return null; + } + + private static boolean containsAnyOrFollow(DataType dataType) { + if (dataType instanceof AnyDataType || dataType instanceof FollowToAnyDataType) { + return true; + } else if (dataType instanceof ArrayType) { + return containsAnyOrFollow(((ArrayType) dataType).getItemType()); + } else if (dataType instanceof MapType) { + return containsAnyOrFollow(((MapType) dataType).getKeyType()) + || containsAnyOrFollow(((MapType) dataType).getValueType()); + } + return false; } private static List extractArgumentTypeBySignature(Class targetType, diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/CreateStruct.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/CreateStruct.java index 9e89da0fd87319..1d7102314edd99 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/CreateStruct.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/CreateStruct.java @@ -21,6 +21,7 @@ import org.apache.doris.nereids.exceptions.AnalysisException; import org.apache.doris.nereids.trees.expressions.Expression; import org.apache.doris.nereids.trees.expressions.functions.AlwaysNotNullable; +import org.apache.doris.nereids.trees.expressions.functions.ComputePrecision; import org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature; import org.apache.doris.nereids.trees.expressions.functions.ExpressionTrait; import org.apache.doris.nereids.trees.expressions.literal.StructLiteral; @@ -36,7 +37,7 @@ * ScalarFunction 'struct'. */ public class CreateStruct extends ScalarFunction - implements ExplicitlyCastableSignature, AlwaysNotNullable { + implements ExplicitlyCastableSignature, AlwaysNotNullable, ComputePrecision { public static final List SIGNATURES = ImmutableList.of( FunctionSignature.ret(StructType.SYSTEM_DEFAULT).args() @@ -54,6 +55,16 @@ private CreateStruct(ScalarFunctionParams functionParams) { super(functionParams); } + // The fields of a struct are independent type variables and their types are already + // resolved from the arguments by getSignatures(). The default decimal v3 precision + // promotion would merge all argument slots into one wider type and insert a lossy cast + // (e.g. widening the scale of a DECIMAL(76,0) field truncates the decimals of an + // ARRAY field), so skip it, same as named_struct. + @Override + public FunctionSignature computePrecision(FunctionSignature signature) { + return signature; + } + @Override public void checkLegalityBeforeTypeCoercion() { if (arity() == 0) { diff --git a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/ComputeSignatureHelperTest.java b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/ComputeSignatureHelperTest.java index e0e1f0f30b6be9..06d1cc4c7efa4c 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/ComputeSignatureHelperTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/ComputeSignatureHelperTest.java @@ -50,6 +50,7 @@ import org.apache.doris.nereids.types.coercion.FollowToAnyDataType; import org.apache.doris.nereids.types.coercion.FollowToArgumentType; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; @@ -327,6 +328,7 @@ void testArrayDecimalV3ComputePrecision() { new ArrayLiteral(Lists.newArrayList(new IntegerLiteral(0)))); signature = ComputeSignatureHelper.computePrecision(new FakeComputeSignature(), signature, arguments); Assertions.assertTrue(signature.getArgType(0) instanceof ArrayType); + // non-MAP decimal slots keep the original behavior of using the wider type Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(7, 4), ((ArrayType) signature.getArgType(0)).getItemType()); Assertions.assertTrue(signature.getArgType(1) instanceof ArrayType); @@ -352,19 +354,292 @@ void testMapDecimalV3ComputePrecision() { new DecimalV3Literal(new BigDecimal("123.123"))); signature = ComputeSignatureHelper.computePrecision(new FakeComputeSignature(), signature, arguments); Assertions.assertTrue(signature.getArgType(0) instanceof MapType); - Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(8, 5), + // key and value are independent decimal slots and keep their own precision/scale + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(5, 4), ((MapType) signature.getArgType(0)).getKeyType()); - Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(8, 5), + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(7, 5), ((MapType) signature.getArgType(0)).getValueType()); Assertions.assertTrue(signature.getArgType(1) instanceof MapType); - Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(8, 5), + // a NULL MAP argument falls back to the wider type of the corresponding group + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(5, 4), ((MapType) signature.getArgType(1)).getKeyType()); - Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(8, 5), + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(7, 5), ((MapType) signature.getArgType(1)).getValueType()); + // non-MAP decimal slots keep the original behavior of using the wider type Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(8, 5), signature.getArgType(2)); } + @Test + void testMapDecimalV3ComputePrecisionKeepKeyValueIndependent() { + FunctionSignature signature = FunctionSignature.ret(ArrayType.of(DecimalV3Type.WILDCARD)) + .args(MapType.of(DecimalV3Type.WILDCARD, DecimalV3Type.WILDCARD)); + Map map = Maps.newLinkedHashMap(); + map.put(new DecimalV3Literal(new BigDecimal("12345678901234567890")), + new DecimalV3Literal(new BigDecimal("0.125000000000000000"))); + List arguments = Lists.newArrayList(new MapLiteral(map)); + signature = ComputeSignatureHelper.computePrecision(new FakeComputeSignature(), signature, arguments); + Assertions.assertTrue(signature.getArgType(0) instanceof MapType); + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(20, 0), + ((MapType) signature.getArgType(0)).getKeyType()); + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(18, 18), + ((MapType) signature.getArgType(0)).getValueType()); + } + + @Test + void testVarArgMapDecimalV3ComputePrecision() { + // a variadic MAP argument: the key leaves of all repeated arguments aggregate + // while key and value stay independent instead of being merged into one wider type + FunctionSignature signature = FunctionSignature.ret(BooleanType.INSTANCE) + .varArgs(MapType.of(DecimalV3Type.WILDCARD, DecimalV3Type.WILDCARD)); + Map map = Maps.newLinkedHashMap(); + map.put(new DecimalV3Literal(new BigDecimal("12345678901234567890")), + new DecimalV3Literal(new BigDecimal("0.125000000000000000"))); + List arguments = Lists.newArrayList(new MapLiteral(map)); + signature = ComputeSignatureHelper.computePrecision(new FakeComputeSignature(), signature, arguments); + Assertions.assertTrue(signature.getArgType(0) instanceof MapType); + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(20, 0), + ((MapType) signature.getArgType(0)).getKeyType()); + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(18, 18), + ((MapType) signature.getArgType(0)).getValueType()); + } + + @Test + void testTopLevelScalarDecimalGroupsKeepIndependent() { + // Simulate the signature after Any/Follow resolution of map_agg(k, v): the key + // and the value are independent top-level decimal slots (Any(0) and Any(1)) and + // must keep their own precision/scale instead of being merged into one wider + // type, otherwise the key (DECIMAL(38,0)) is widened to a scale that cannot hold + // a 38-digit integral key and the value (DECIMAL(38,18)) is truncated before + // aggregation. + FunctionSignature signature = FunctionSignature.ret(DecimalV3Type.createDecimalV3Type(38, 0)) + .args(DecimalV3Type.createDecimalV3Type(38, 0), + DecimalV3Type.createDecimalV3Type(38, 18)); + List arguments = Lists.newArrayList( + new DecimalV3Literal(new BigDecimal("99999999999999999999999999999999999999")), + new DecimalV3Literal(new BigDecimal("99999999999999999999.125000000000000000"))); + signature = ComputeSignatureHelper.computePrecision(new FakeComputeSignature(), signature, arguments); + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(38, 0), signature.getArgType(0)); + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(38, 18), signature.getArgType(1)); + } + + @Test + void testTopLevelScalarDecimalSameResolvedTypeMerges() { + // top-level scalar slots resolved to the same type (e.g. the operands of + // greatest/least after common-type resolution) form one logical group and are + // promoted together to the wider type of the group + FunctionSignature signature = FunctionSignature.ret(DecimalV3Type.createDecimalV3Type(7, 2)) + .args(DecimalV3Type.createDecimalV3Type(7, 2), DecimalV3Type.createDecimalV3Type(7, 2)); + List arguments = Lists.newArrayList( + new DecimalV3Literal(new BigDecimal("123.45")), + new DecimalV3Literal(new BigDecimal("1234.5678"))); + signature = ComputeSignatureHelper.computePrecision(new FakeComputeSignature(), signature, arguments); + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(8, 4), signature.getArgType(0)); + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(8, 4), signature.getArgType(1)); + } + + @Test + void testFieldDecimalV3VarArgOneType() { + // field declares varArgs(DECIMALV3, DECIMALV3): its fixed first operand and the + // repeated tail are one comparison type and must be promoted to one type + FunctionSignature signature = FunctionSignature.ret(IntegerType.INSTANCE) + .varArgs(DecimalV3Type.WILDCARD, DecimalV3Type.WILDCARD); + List arguments = Lists.newArrayList( + new DecimalV3Literal(new BigDecimal("1.20")), + new DecimalV3Literal(new BigDecimal("1.200"))); + signature = ComputeSignatureHelper.computePrecision(new FakeComputeSignature(), signature, arguments); + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(4, 3), signature.getArgType(0)); + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(4, 3), signature.getArgType(1)); + } + + @Test + void testElementAtLinkedDecimalV3ComputePrecision() { + // Simulate the signature after Any/Follow resolution of element_at: the MAP key + // and the lookup slot are resolved to the same DECIMAL(9, 2), while the lookup + // argument is a wider DECIMAL(10, 3). Both must be promoted to DECIMAL(10, 3) + // so the BE compares columns of the same concrete decimal class. + FunctionSignature signature = FunctionSignature.ret(DecimalV3Type.createDecimalV3Type(5, 2)) + .args(MapType.of(DecimalV3Type.createDecimalV3Type(9, 2), + DecimalV3Type.createDecimalV3Type(5, 2)), + DecimalV3Type.createDecimalV3Type(9, 2)); + Map map = Maps.newLinkedHashMap(); + map.put(new DecimalV3Literal(new BigDecimal("1234567.89")), + new DecimalV3Literal(new BigDecimal("123.45"))); + List arguments = Lists.newArrayList( + new MapLiteral(map), + new DecimalV3Literal(new BigDecimal("1234567.891"))); + signature = ComputeSignatureHelper.computePrecision(new FakeComputeSignature(), signature, arguments); + Assertions.assertTrue(signature.getArgType(0) instanceof MapType); + // the MAP key and the linked lookup slot are promoted to one type + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(10, 3), + ((MapType) signature.getArgType(0)).getKeyType()); + // the MAP value keeps its own type + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(5, 2), + ((MapType) signature.getArgType(0)).getValueType()); + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(10, 3), signature.getArgType(1)); + } + + @Test + void testElementAtNullLookupLinkedDecimalV3ComputePrecision() { + // a NULL lookup must fall back to the MAP key group instead of the wider type of + // an unrelated MAP leaf (the value), so it can still match the MAP key type + FunctionSignature signature = FunctionSignature.ret(DecimalV3Type.createDecimalV3Type(18, 18)) + .args(MapType.of(DecimalV3Type.createDecimalV3Type(9, 2), + DecimalV3Type.createDecimalV3Type(18, 18)), + DecimalV3Type.createDecimalV3Type(9, 2)); + Map map = Maps.newLinkedHashMap(); + map.put(new DecimalV3Literal(new BigDecimal("1234567.89")), + new DecimalV3Literal(new BigDecimal("0.000000000000000001"))); + List arguments = Lists.newArrayList( + new MapLiteral(map), + new NullLiteral()); + signature = ComputeSignatureHelper.computePrecision(new FakeComputeSignature(), signature, arguments); + Assertions.assertTrue(signature.getArgType(0) instanceof MapType); + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(9, 2), + ((MapType) signature.getArgType(0)).getKeyType()); + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(18, 18), + ((MapType) signature.getArgType(0)).getValueType()); + // the NULL lookup keeps the MAP key type instead of the wider type + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(9, 2), signature.getArgType(1)); + } + + @Test + void testMapContainsValueLinkedProbeKeepsValueGroup() { + // map_contains_value(m, x): the MAP value is Any(0) and the probe x is Follow(0). + // Any resolution widens both to DECIMAL(10,3), which also happens to equal the + // independent MAP key type. The probe must still be linked to the value group by + // the original Any/Follow index (not the resolved type), otherwise the value + // regresses to DECIMAL(9,2) and the BE compares a Decimal32 value array with a + // Decimal64 probe. + FunctionSignature template = FunctionSignature.ret(BooleanType.INSTANCE) + .args(MapType.of(AnyDataType.INSTANCE_WITHOUT_INDEX, new AnyDataType(0)), + new FollowToAnyDataType(0)); + // resolved signature (after implementAnyDataTypeWithIndex) + FunctionSignature signature = FunctionSignature.ret(BooleanType.INSTANCE) + .args(MapType.of(DecimalV3Type.createDecimalV3Type(10, 3), + DecimalV3Type.createDecimalV3Type(10, 3)), + DecimalV3Type.createDecimalV3Type(10, 3)); + Map map = Maps.newLinkedHashMap(); + map.put(new DecimalV3Literal(new BigDecimal("1234567.890")), + new DecimalV3Literal(new BigDecimal("12.34"))); + List arguments = Lists.newArrayList( + new MapLiteral(map), + new DecimalV3Literal(new BigDecimal("1234567.890"))); + signature = ComputeSignatureHelper.computePrecision(new FakeComputeSignature(template), signature, arguments); + Assertions.assertTrue(signature.getArgType(0) instanceof MapType); + MapType mapType = (MapType) signature.getArgType(0); + // the MAP key keeps DECIMAL(10,3) + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(10, 3), mapType.getKeyType()); + // the MAP value is promoted together with the linked probe to DECIMAL(10,3) + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(10, 3), mapType.getValueType()); + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(10, 3), signature.getArgType(1)); + } + + @Test + void testMapContainsEntryLinkedScalarsKeepOwnGroups() { + // map_contains_entry(m, k, v): the MAP key/value are Any(0)/Any(1) and k/v are + // Follow(0)/Follow(1). After resolution the key and the value can both become + // DECIMAL(10,3); k/v must still link to their own groups by the Any/Follow index + // instead of the resolved type, otherwise the value regresses to DECIMAL(9,2) + // while v stays DECIMAL(10,3). + FunctionSignature template = FunctionSignature.ret(BooleanType.INSTANCE) + .args(MapType.of(new AnyDataType(0), new AnyDataType(1)), + new FollowToAnyDataType(0), new FollowToAnyDataType(1)); + FunctionSignature signature = FunctionSignature.ret(BooleanType.INSTANCE) + .args(MapType.of(DecimalV3Type.createDecimalV3Type(10, 3), + DecimalV3Type.createDecimalV3Type(10, 3)), + DecimalV3Type.createDecimalV3Type(10, 3), + DecimalV3Type.createDecimalV3Type(10, 3)); + Map map = Maps.newLinkedHashMap(); + map.put(new DecimalV3Literal(new BigDecimal("1234567.890")), + new DecimalV3Literal(new BigDecimal("12.34"))); + List arguments = Lists.newArrayList( + new MapLiteral(map), + new DecimalV3Literal(new BigDecimal("1234567.890")), + new DecimalV3Literal(new BigDecimal("2345678.901"))); + signature = ComputeSignatureHelper.computePrecision(new FakeComputeSignature(template), signature, arguments); + Assertions.assertTrue(signature.getArgType(0) instanceof MapType); + MapType mapType = (MapType) signature.getArgType(0); + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(10, 3), mapType.getKeyType()); + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(10, 3), mapType.getValueType()); + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(10, 3), signature.getArgType(1)); + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(10, 3), signature.getArgType(2)); + } + + @Test + void testMapNestedArrayDecimalV3ComputePrecision() { + // the item of an ARRAY nested in a MAP value keeps its own precision/scale + // instead of being merged with the MAP key into the global wider type, otherwise + // expectedInputTypes() inserts a lossy cast before map_values() executes + FunctionSignature signature = FunctionSignature.ret(BooleanType.INSTANCE) + .args(MapType.of(DecimalV3Type.WILDCARD, ArrayType.of(DecimalV3Type.WILDCARD))); + Map map = Maps.newLinkedHashMap(); + map.put(new DecimalV3Literal(new BigDecimal("12345678901234567890123456789012345678")), + new ArrayLiteral(Lists.newArrayList( + new DecimalV3Literal(new BigDecimal("0.123456789012345678"))))); + List arguments = Lists.newArrayList(new MapLiteral(map)); + signature = ComputeSignatureHelper.computePrecision(new FakeComputeSignature(), signature, arguments); + Assertions.assertTrue(signature.getArgType(0) instanceof MapType); + MapType mapType = (MapType) signature.getArgType(0); + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(38, 0), mapType.getKeyType()); + Assertions.assertTrue(mapType.getValueType() instanceof ArrayType); + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(18, 18), + ((ArrayType) mapType.getValueType()).getItemType()); + } + + @Test + void testNestedMapDecimalV3ComputePrecision() { + // the key/value of a MAP nested in a MAP value keep their own groups instead of + // being merged with the outer MAP leaves + FunctionSignature signature = FunctionSignature.ret(BooleanType.INSTANCE) + .args(MapType.of(DecimalV3Type.WILDCARD, + MapType.of(DecimalV3Type.WILDCARD, DecimalV3Type.WILDCARD))); + Map inner = Maps.newLinkedHashMap(); + inner.put(new DecimalV3Literal(new BigDecimal("12.34")), + new DecimalV3Literal(new BigDecimal("123.456"))); + Map outer = Maps.newLinkedHashMap(); + outer.put(new DecimalV3Literal(new BigDecimal("12345678901234567890")), new MapLiteral(inner)); + List arguments = Lists.newArrayList(new MapLiteral(outer)); + signature = ComputeSignatureHelper.computePrecision(new FakeComputeSignature(), signature, arguments); + Assertions.assertTrue(signature.getArgType(0) instanceof MapType); + MapType outerType = (MapType) signature.getArgType(0); + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(20, 0), outerType.getKeyType()); + Assertions.assertTrue(outerType.getValueType() instanceof MapType); + MapType innerType = (MapType) outerType.getValueType(); + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(4, 2), innerType.getKeyType()); + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(6, 3), innerType.getValueType()); + } + + @Test + void testNestedMapLinkedScalarDoesNotPromoteInnerKey() { + // a scalar linked with the outer MAP key must not promote an unrelated inner key + // that happens to share the same resolved type + FunctionSignature signature = FunctionSignature.ret(DecimalV3Type.createDecimalV3Type(5, 2)) + .args(MapType.of(DecimalV3Type.createDecimalV3Type(9, 2), + MapType.of(DecimalV3Type.createDecimalV3Type(9, 2), + DecimalV3Type.createDecimalV3Type(5, 2))), + DecimalV3Type.createDecimalV3Type(9, 2)); + Map inner = Maps.newLinkedHashMap(); + inner.put(new DecimalV3Literal(new BigDecimal("1234567.89")), + new DecimalV3Literal(new BigDecimal("123.45"))); + Map outer = Maps.newLinkedHashMap(); + outer.put(new DecimalV3Literal(new BigDecimal("1234567.89")), new MapLiteral(inner)); + List arguments = Lists.newArrayList( + new MapLiteral(outer), + new DecimalV3Literal(new BigDecimal("1234567.891"))); + signature = ComputeSignatureHelper.computePrecision(new FakeComputeSignature(), signature, arguments); + Assertions.assertTrue(signature.getArgType(0) instanceof MapType); + MapType outerType = (MapType) signature.getArgType(0); + // the outer key is promoted together with the linked lookup slot + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(10, 3), outerType.getKeyType()); + Assertions.assertTrue(outerType.getValueType() instanceof MapType); + MapType innerType = (MapType) outerType.getValueType(); + // the inner key with the same resolved type is NOT promoted by the outer lookup + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(9, 2), innerType.getKeyType()); + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(5, 2), innerType.getValueType()); + Assertions.assertEquals(DecimalV3Type.createDecimalV3Type(10, 3), signature.getArgType(1)); + } + @Test void testArrayDateTimeV2ComputePrecision() { FunctionSignature signature = FunctionSignature.ret(BooleanType.INSTANCE) @@ -568,6 +843,16 @@ void testDateV1AndDateTimeV1TypeConversion() { } private static class FakeComputeSignature implements ComputeSignature { + private final FunctionSignature template; + + FakeComputeSignature() { + this(null); + } + + FakeComputeSignature(FunctionSignature template) { + this.template = template; + } + @Override public List children() { return null; @@ -600,7 +885,7 @@ public Expression withChildren(List children) { @Override public List getSignatures() { - return null; + return template == null ? null : ImmutableList.of(template); } @Override diff --git a/regression-test/data/nereids_function_p0/agg_function/nereids_agg_fn_map_decimal_precision.out b/regression-test/data/nereids_function_p0/agg_function/nereids_agg_fn_map_decimal_precision.out new file mode 100644 index 00000000000000..8f9793cb7d0583 --- /dev/null +++ b/regression-test/data/nereids_function_p0/agg_function/nereids_agg_fn_map_decimal_precision.out @@ -0,0 +1,12 @@ +-- This file is automatically generated. You should know what you did if you want to edit this +-- !map_agg_decimal_precision -- +{99999999999999999999999999999999999999:0.125000000000000000} + +-- !map_agg_v2_decimal_precision -- +{99999999999999999999999999999999999999:0.125000000000000000} + +-- !map_agg_group_by_decimal_precision -- +1 {99999999999999999999999999999999999999:0.125000000000000000} +2 {12345678901234567890123456789012345678:2.000000000000000001} +3 {12345678901234567890123456789012345679:3.000000000000000000} + diff --git a/regression-test/data/nereids_function_p0/scalar_function/nereids_scalar_fn_map_decimal_precision.out b/regression-test/data/nereids_function_p0/scalar_function/nereids_scalar_fn_map_decimal_precision.out new file mode 100644 index 00000000000000..8e3c8326b936be --- /dev/null +++ b/regression-test/data/nereids_function_p0/scalar_function/nereids_scalar_fn_map_decimal_precision.out @@ -0,0 +1,63 @@ +-- This file is automatically generated. You should know what you did if you want to edit this +-- !map_keys_decimal256 -- +{9999999999999999999999999999999999999999999999999999999999999999999999999999:0.125000000000000000} [9999999999999999999999999999999999999999999999999999999999999999999999999999] [9999999999999999999999999999999999999999999999999999999999999999999999999999] + +-- !unnest_map_decimal256 -- +1.2500 2.125000000000000000 + +-- !explode_map_decimal256 -- +1.2500 2.125000000000000000 + +-- !map_contains_key_decimal256 -- +true + +-- !map_values_decimal256 -- +[0.125000000000000000] + +-- !map_entries_decimal256 -- +[{"key":9999999999999999999999999999999999999999999999999999999999999999999999999999, "value":0.125000000000000000}] + +-- !element_at_decimal256 -- +2.125000000000000000 + +-- !element_at_wider_lookup -- +1 12.34 +2 \N + +-- !map_contains_key_wider_lookup -- +1 true +2 false + +-- !element_at_null_lookup -- +1 \N +2 \N + +-- !field_decimal -- +1 + +-- !map_values_nested_array -- +[[0.123456789012345678]] + +-- !map_keys_nested_array -- +[12345678901234567890123456789012345678] + +-- !map_values_nested_map -- +[{1234567.89:12.34}] + +-- !map_keys_nested_map -- +[12345678901234567890123456789012345678] + +-- !basic_decimal256 -- +123.456 123.46 + +-- !struct_independent_fields -- +{"col1":9999999999999999999999999999999999999999999999999999999999999999999999999999, "col2":[0.125000000000000000]} + +-- !map_contains_value_collision -- +1 false +2 false + +-- !map_contains_entry_collision -- +1 true +2 false + diff --git a/regression-test/suites/nereids_function_p0/agg_function/nereids_agg_fn_map_decimal_precision.groovy b/regression-test/suites/nereids_function_p0/agg_function/nereids_agg_fn_map_decimal_precision.groovy new file mode 100644 index 00000000000000..fa7d565f9af0ef --- /dev/null +++ b/regression-test/suites/nereids_function_p0/agg_function/nereids_agg_fn_map_decimal_precision.groovy @@ -0,0 +1,60 @@ +// 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. + +// map_agg/map_agg_v2 expose their MAP key/value as independent top-level Any(0)/Any(1) +// arguments. The default decimal v3 precision promotion must keep those logical groups +// independent: merging them into one wider type would widen the DECIMAL(38,0) key to a +// scale that cannot hold a 38-digit integral key (so the entry disappears as a NULL key) +// and truncate twelve fractional digits from the DECIMAL(38,18) value before aggregation. +suite("nereids_agg_fn_map_decimal_precision") { + sql "set enable_nereids_planner=true" + sql "set enable_fallback_to_original_planner=false" + + sql "drop table if exists map_agg_dec_precision" + sql """ + create table map_agg_dec_precision ( + g int, + k decimal(38, 0), + v decimal(38, 18) + ) + duplicate key(g) + distributed by hash(g) buckets 1 + properties("replication_num" = "1") + """ + sql """ + insert into map_agg_dec_precision values + (1, 99999999999999999999999999999999999999, 0.125000000000000000), + (2, 12345678901234567890123456789012345678, 2.000000000000000001), + (3, 12345678901234567890123456789012345679, 3.000000000000000000) + """ + + // the 38-digit integral key must be preserved, and the value must keep all 18 + // fractional digits. map_agg is registered as an alias of MapAggV2, so exercise + // the distinct MapAggV1 typed-column path here and map_agg_v2 in the next query. + order_qt_map_agg_decimal_precision """ + select map_agg_v1(k, v) from map_agg_dec_precision where g = 1; + """ + + order_qt_map_agg_v2_decimal_precision """ + select map_agg_v2(k, v) from map_agg_dec_precision where g = 1; + """ + + // every group has a unique key, so no unordered duplicate-key winner is asserted + order_qt_map_agg_group_by_decimal_precision """ + select g, map_agg(k, v) from map_agg_dec_precision group by g order by g; + """ +} diff --git a/regression-test/suites/nereids_function_p0/scalar_function/nereids_scalar_fn_map_decimal_precision.groovy b/regression-test/suites/nereids_function_p0/scalar_function/nereids_scalar_fn_map_decimal_precision.groovy new file mode 100644 index 00000000000000..db6624154ef23a --- /dev/null +++ b/regression-test/suites/nereids_function_p0/scalar_function/nereids_scalar_fn_map_decimal_precision.groovy @@ -0,0 +1,227 @@ +// 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. + +// The key and the value of a MAP are independent decimal slots. The default +// decimal v3 precision promotion must keep their precision/scale independent +// instead of merging them into one type, otherwise widening the scale of a big +// integral key silently converts it to NULL (e.g. in map_keys) and UNNEST(MAP)/ +// explode_map may fail the type check. +suite("nereids_scalar_fn_map_decimal_precision") { + sql "set enable_decimal256 = true;" + + // 1. map_keys must keep the big integral key instead of turning it into NULL + order_qt_map_keys_decimal256 """ + SELECT + CAST(MAP( + CAST('9999999999999999999999999999999999999999999999999999999999999999999999999999' AS DECIMAL(76,0)), + CAST('0.125000000000000000' AS DECIMAL(76,18)) + ) AS STRING) AS source_map, + CAST(MAP_KEYS(MAP( + CAST('9999999999999999999999999999999999999999999999999999999999999999999999999999' AS DECIMAL(76,0)), + CAST('0.125000000000000000' AS DECIMAL(76,18)) + )) AS STRING) AS actual_keys, + CAST(ARRAY(CAST('9999999999999999999999999999999999999999999999999999999999999999999999999999' AS DECIMAL(76,0))) AS STRING) AS expected_keys; + """ + + // 2. UNNEST(MAP) must pass the type check and return independent key/value + order_qt_unnest_map_decimal256 """ + SELECT key_out, value_out + FROM UNNEST(MAP( + CAST('1.2500' AS DECIMAL(16,4)), + CAST('2.125000000000000000' AS DECIMAL(76,18)) + )) AS expanded(key_out, value_out); + """ + + // 3. explode_map keeps independent key/value decimals + order_qt_explode_map_decimal256 """ + SELECT k, v + FROM (SELECT 1) x + LATERAL VIEW EXPLODE_MAP(MAP( + CAST('1.2500' AS DECIMAL(16,4)), + CAST('2.125000000000000000' AS DECIMAL(76,18)) + )) t AS k, v; + """ + + // 4. map_contains_key can find the big integral key + order_qt_map_contains_key_decimal256 """ + SELECT MAP_CONTAINS_KEY(MAP( + CAST('9999999999999999999999999999999999999999999999999999999999999999999999999999' AS DECIMAL(76,0)), + CAST('0.125000000000000000' AS DECIMAL(76,18)) + ), CAST('9999999999999999999999999999999999999999999999999999999999999999999999999999' AS DECIMAL(76,0))) AS ck; + """ + + // 5. map_values keeps the value + order_qt_map_values_decimal256 """ + SELECT CAST(MAP_VALUES(MAP( + CAST('9999999999999999999999999999999999999999999999999999999999999999999999999999' AS DECIMAL(76,0)), + CAST('0.125000000000000000' AS DECIMAL(76,18)) + )) AS STRING) AS actual_values; + """ + + // 6. map_entries keeps independent key/value decimals + order_qt_map_entries_decimal256 """ + SELECT CAST(MAP_ENTRIES(MAP( + CAST('9999999999999999999999999999999999999999999999999999999999999999999999999999' AS DECIMAL(76,0)), + CAST('0.125000000000000000' AS DECIMAL(76,18)) + )) AS STRING) AS entries; + """ + + // 7. element_at over a map with independent key/value decimals + order_qt_element_at_decimal256 """ + SELECT ELEMENT_AT(MAP( + CAST('1.2500' AS DECIMAL(16,4)), + CAST('2.125000000000000000' AS DECIMAL(76,18)) + ), CAST('1.2500' AS DECIMAL(16,4))) AS value; + """ + + // 8. element_at / map_contains_key with a wider lookup on a Decimal32 key column. + // The lookup slot follows the MAP key type, so both must be promoted to one type + // (across the DECIMAL32 -> DECIMAL64 storage-width boundary) instead of being + // widened independently, otherwise the BE compares columns of different concrete + // decimal classes. + sql "drop table if exists fn_test_map_decimal_precision" + sql """ + create table fn_test_map_decimal_precision ( + id int null, + m map null + ) engine=olap + distributed by hash(id) buckets 1 + properties('replication_num' = '1') + """ + sql """ + insert into fn_test_map_decimal_precision values + (1, map(cast('1234567.89' as decimal(9,2)), cast('12.34' as decimal(5,2)))), + (2, map(cast('1.23' as decimal(9,2)), cast('0.01' as decimal(5,2)))); + """ + order_qt_element_at_wider_lookup """ + select id, element_at(m, cast('1234567.890' as decimal(10,3))) + from fn_test_map_decimal_precision order by id + """ + order_qt_map_contains_key_wider_lookup """ + select id, map_contains_key(m, cast('1234567.890' as decimal(10,3))) + from fn_test_map_decimal_precision order by id + """ + // a NULL lookup must fall back to the MAP key group instead of the wider type of + // an unrelated MAP leaf + order_qt_element_at_null_lookup """ + select id, element_at(m, null) + from fn_test_map_decimal_precision order by id + """ + + // 9. field declares varArgs(DECIMALV3, DECIMALV3): its fixed first operand and the + // repeated tail are one comparison type and must keep one promoted type + order_qt_field_decimal """ + select field(cast('1.20' as decimal(3,2)), cast('1.200' as decimal(4,3)), + cast('2.000' as decimal(4,3))) + """ + + // 10. nonconstant nested containers: the item of an ARRAY nested in a MAP value + // keeps its own precision/scale instead of being merged with the key into the wider + // type, otherwise map_values() discards the low-order fractional digits + sql "drop table if exists fn_test_map_nested_decimal" + sql """ + create table fn_test_map_nested_decimal ( + id int null, + m map> null + ) engine=olap + distributed by hash(id) buckets 1 + properties('replication_num' = '1') + """ + sql """ + insert into fn_test_map_nested_decimal values + (1, map(cast('12345678901234567890123456789012345678' as decimal(38,0)), + array(cast('0.123456789012345678' as decimal(38,18))))); + """ + order_qt_map_values_nested_array """ + select cast(map_values(m) as string) as v from fn_test_map_nested_decimal order by id + """ + order_qt_map_keys_nested_array """ + select cast(map_keys(m) as string) as k from fn_test_map_nested_decimal order by id + """ + + // 11. nonconstant nested MAP: the key/value of a MAP nested in a MAP value keep + // their own precision/scale instead of being merged with the outer MAP leaves + sql "drop table if exists fn_test_nested_map_decimal" + sql """ + create table fn_test_nested_map_decimal ( + id int null, + m map> null + ) engine=olap + distributed by hash(id) buckets 1 + properties('replication_num' = '1') + """ + sql """ + insert into fn_test_nested_map_decimal values + (1, map(cast('12345678901234567890123456789012345678' as decimal(38,0)), + map(cast('1234567.89' as decimal(9,2)), cast('12.34' as decimal(5,2))))); + """ + order_qt_map_values_nested_map """ + select cast(map_values(m) as string) as v from fn_test_nested_map_decimal order by id + """ + order_qt_map_keys_nested_map """ + select cast(map_keys(m) as string) as k from fn_test_nested_map_decimal order by id + """ + + // 12. basic decimal v3 precision promotion (single slot) is not affected + order_qt_basic_decimal256 """ + SELECT ABS(CAST('123.456' AS DECIMAL(10,3))) AS abs_v, + ROUND(CAST('123.456' AS DECIMAL(10,3)), 2) AS round_v; + """ + + // 13. struct(...) fields are independent type variables: the default decimal v3 + // precision promotion must not merge them (e.g. widening the scale of a DECIMAL(76,0) + // field would truncate the decimals of an ARRAY field) + order_qt_struct_independent_fields """ + SELECT CAST(STRUCT( + CAST('9999999999999999999999999999999999999999999999999999999999999999999999999999' AS DECIMAL(76,0)), + ARRAY(CAST('0.125000000000000000' AS DECIMAL(76,18))) + ) AS STRING) AS s; + """ + + // 14. nonconstant map_contains_value / map_contains_entry: the probe (Follow(0)) and + // the MAP value (Any(0)) are one logical group. When the probe and the independent key + // resolve to the same type (DECIMAL(10,3)), the probe must still be linked to the value + // group by the original Any/Follow index, otherwise the value regresses to DECIMAL(9,2) + // and the BE compares a Decimal32 value array with a Decimal64 probe. + sql "drop table if exists fn_test_map_contains_collision_decimal" + sql """ + create table fn_test_map_contains_collision_decimal ( + id int null, + m map null, + x decimal(10,3) null, + k decimal(10,3) null, + v decimal(9,2) null + ) engine=olap + distributed by hash(id) buckets 1 + properties('replication_num' = '1') + """ + sql """ + insert into fn_test_map_contains_collision_decimal values + (1, map(cast('1234567.890' as decimal(10,3)), cast('12.34' as decimal(9,2))), + cast('1234567.890' as decimal(10,3)), + cast('1234567.890' as decimal(10,3)), cast('12.34' as decimal(9,2))), + (2, map(cast('1.230' as decimal(10,3)), cast('0.01' as decimal(9,2))), + cast('9.999' as decimal(10,3)), + cast('9.999' as decimal(10,3)), cast('0.02' as decimal(9,2))); + """ + order_qt_map_contains_value_collision """ + select id, map_contains_value(m, x) as r from fn_test_map_contains_collision_decimal order by id + """ + order_qt_map_contains_entry_collision """ + select id, map_contains_entry(m, k, v) as r from fn_test_map_contains_collision_decimal order by id + """ +}