morrySnow commented on code in PR #64622:
URL: https://github.com/apache/doris/pull/64622#discussion_r3491139620


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/SearchSignatureForRound.java:
##########
@@ -18,26 +18,86 @@
 package org.apache.doris.nereids.trees.expressions.functions;
 
 import org.apache.doris.catalog.FunctionSignature;
+import org.apache.doris.nereids.CascadesContext;
+import org.apache.doris.nereids.rules.expression.ExpressionRewriteContext;
+import org.apache.doris.nereids.rules.expression.rules.FoldConstantRuleOnFE;
+import org.apache.doris.nereids.trees.expressions.Cast;
 import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLikeLiteral;
 import org.apache.doris.nereids.types.DoubleType;
 import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.nereids.types.coercion.Int32OrLessType;
+import org.apache.doris.qe.ConnectContext;
 
+import java.math.BigInteger;
+import java.util.ArrayList;
 import java.util.List;
 
 /**
- * if argument 0 is float or double, we should return double signature for 
round like function.
+ * Signature search for round-like functions (round, round_bankers, ceil, 
floor, truncate).
  */
 public interface SearchSignatureForRound extends ExplicitlyCastableSignature {
+
+    int DOUBLE_DECIMAL_RESULT_MAX_SCALE = 15;
+
     @Override
     default FunctionSignature searchSignature(List<FunctionSignature> 
signatures) {
         List<Expression> arguments = getArguments();
         if (arguments.get(0).getDataType().isFloatLikeType()) {
             if (arguments.size() == 1) {
                 return 
FunctionSignature.ret(DoubleType.INSTANCE).args(DoubleType.INSTANCE);

Review Comment:
   The outer condition changed from `isFloatLikeType()` to `isDoubleType()`. 
Previously, both FLOAT and DOUBLE inputs would be forced to a `(DOUBLE) → 
DOUBLE` signature (single-arg) or `(DOUBLE, INT) → DOUBLE` (two-arg). Now, only 
DOUBLE inputs get this treatment — FLOAT inputs fall through to the standard 
signature search. 
   
   For two-arg FLOAT this is probably fine (standard search resolves to 
`(DOUBLE, INT) → DOUBLE` via implicit promotion). But for single-arg FLOAT, the 
standard search might find `(FLOAT) → BIGINT` instead of `(DOUBLE) → DOUBLE`, 
which changes the return type. The old class javadoc says _"if argument 0 is 
float or double"_ — if the narrowing to DOUBLE-only is intentional, please 
update the javadoc to match.
   
   For reference, the DECIMAL reroute itself is scoped to DOUBLE-only via the 
inner `isDoubleType()` check, which is correct since the DECIMAL(30,15) cast 
target is sized for DOUBLE's precision.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/SearchSignatureForRound.java:
##########
@@ -18,26 +18,86 @@
 package org.apache.doris.nereids.trees.expressions.functions;
 
 import org.apache.doris.catalog.FunctionSignature;
+import org.apache.doris.nereids.CascadesContext;
+import org.apache.doris.nereids.rules.expression.ExpressionRewriteContext;
+import org.apache.doris.nereids.rules.expression.rules.FoldConstantRuleOnFE;
+import org.apache.doris.nereids.trees.expressions.Cast;
 import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLikeLiteral;
 import org.apache.doris.nereids.types.DoubleType;
 import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.nereids.types.coercion.Int32OrLessType;
+import org.apache.doris.qe.ConnectContext;
 
+import java.math.BigInteger;
+import java.util.ArrayList;
 import java.util.List;
 
 /**
- * if argument 0 is float or double, we should return double signature for 
round like function.
+ * Signature search for round-like functions (round, round_bankers, ceil, 
floor, truncate).
  */
 public interface SearchSignatureForRound extends ExplicitlyCastableSignature {
+
+    int DOUBLE_DECIMAL_RESULT_MAX_SCALE = 15;
+
     @Override
     default FunctionSignature searchSignature(List<FunctionSignature> 
signatures) {
         List<Expression> arguments = getArguments();
         if (arguments.get(0).getDataType().isFloatLikeType()) {
             if (arguments.size() == 1) {
                 return 
FunctionSignature.ret(DoubleType.INSTANCE).args(DoubleType.INSTANCE);
             } else if (arguments.size() == 2) {
+                if (arguments.get(0).getDataType().isDoubleType()

Review Comment:
   The inner `arguments.get(0).getDataType().isDoubleType()` check is redundant 
— we are already inside the outer `if 
(arguments.get(0).getDataType().isDoubleType())` block. Consider removing the 
duplicate check for readability.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/SearchSignatureForRound.java:
##########
@@ -18,26 +18,86 @@
 package org.apache.doris.nereids.trees.expressions.functions;
 
 import org.apache.doris.catalog.FunctionSignature;
+import org.apache.doris.nereids.CascadesContext;
+import org.apache.doris.nereids.rules.expression.ExpressionRewriteContext;
+import org.apache.doris.nereids.rules.expression.rules.FoldConstantRuleOnFE;
+import org.apache.doris.nereids.trees.expressions.Cast;
 import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLikeLiteral;
 import org.apache.doris.nereids.types.DoubleType;
 import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.nereids.types.coercion.Int32OrLessType;
+import org.apache.doris.qe.ConnectContext;
 
+import java.math.BigInteger;
+import java.util.ArrayList;
 import java.util.List;
 
 /**
- * if argument 0 is float or double, we should return double signature for 
round like function.
+ * Signature search for round-like functions (round, round_bankers, ceil, 
floor, truncate).
  */
 public interface SearchSignatureForRound extends ExplicitlyCastableSignature {
+
+    int DOUBLE_DECIMAL_RESULT_MAX_SCALE = 15;
+
     @Override
     default FunctionSignature searchSignature(List<FunctionSignature> 
signatures) {
         List<Expression> arguments = getArguments();
         if (arguments.get(0).getDataType().isFloatLikeType()) {
             if (arguments.size() == 1) {
                 return 
FunctionSignature.ret(DoubleType.INSTANCE).args(DoubleType.INSTANCE);
             } else if (arguments.size() == 2) {
+                if (arguments.get(0).getDataType().isDoubleType()
+                        && isOptedIntoDecimalReroute()
+                        && isNonNegativeIntegerLiteralAtMost(arguments.get(1),
+                                DOUBLE_DECIMAL_RESULT_MAX_SCALE)) {
+                    return ExplicitlyCastableSignature.super.searchSignature(
+                            withoutFloatLikeReturnTypes(signatures));
+                }
                 return 
FunctionSignature.ret(DoubleType.INSTANCE).args(DoubleType.INSTANCE, 
IntegerType.INSTANCE);
             }
         }
         return ExplicitlyCastableSignature.super.searchSignature(signatures);
     }
+
+    static boolean isOptedIntoDecimalReroute() {
+        ConnectContext ctx = ConnectContext.get();
+        return ctx != null && 
ctx.getSessionVariable().roundDoubleReturnsDecimalForConstScale;
+    }
+
+    /**
+     * True iff scale folds to an integer literal whose value lies in the 
closed range
+     * [0, maxValue].
+     */
+    static boolean isNonNegativeIntegerLiteralAtMost(Expression scale, int 
maxValue) {
+        Expression folded = scale;
+        if (!folded.isLiteral() && !folded.isSlot()) {
+            ExpressionRewriteContext ctx = new 
ExpressionRewriteContext(CascadesContext.initTempContext());
+            folded = FoldConstantRuleOnFE.evaluate(folded, ctx);
+        }
+        Expression unwrapped = folded;
+        if (unwrapped instanceof Cast && unwrapped.child(0).isLiteral()
+                && unwrapped.child(0).getDataType() instanceof 
Int32OrLessType) {
+            unwrapped = unwrapped.child(0);
+        }
+        if (!(unwrapped instanceof IntegerLikeLiteral)) {
+            return false;
+        }
+        Number number = ((IntegerLikeLiteral) unwrapped).getNumber();
+        BigInteger value = (number instanceof BigInteger)
+                ? (BigInteger) number
+                : BigInteger.valueOf(number.longValue());
+        return value.signum() >= 0 && 
value.compareTo(BigInteger.valueOf(maxValue)) <= 0;
+    }
+
+    /** Drop signatures whose return type is a float-like type, so the search 
falls onto decimal. */
+    static List<FunctionSignature> 
withoutFloatLikeReturnTypes(List<FunctionSignature> signatures) {
+        List<FunctionSignature> result = new ArrayList<>(signatures.size());
+        for (FunctionSignature signature : signatures) {
+            if (!signature.returnType.isFloatLikeType()) {

Review Comment:
   This follows the same fold-then-unwrap-Cast pattern used in 
`ComputePrecisionForRound`. Consider also adding a `null`-safety guard on the 
folded result — while `FoldConstantRuleOnFE.evaluate` should return the input 
unchanged (not null) when folding fails, an explicit guard would make the code 
more robust and self-documenting:
   ```java
   if (folded == null) {
       return false;
   }
   ```



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/SearchSignatureForRound.java:
##########
@@ -18,26 +18,86 @@
 package org.apache.doris.nereids.trees.expressions.functions;
 
 import org.apache.doris.catalog.FunctionSignature;
+import org.apache.doris.nereids.CascadesContext;
+import org.apache.doris.nereids.rules.expression.ExpressionRewriteContext;
+import org.apache.doris.nereids.rules.expression.rules.FoldConstantRuleOnFE;
+import org.apache.doris.nereids.trees.expressions.Cast;
 import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLikeLiteral;
 import org.apache.doris.nereids.types.DoubleType;
 import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.nereids.types.coercion.Int32OrLessType;
+import org.apache.doris.qe.ConnectContext;
 
+import java.math.BigInteger;
+import java.util.ArrayList;
 import java.util.List;
 
 /**
- * if argument 0 is float or double, we should return double signature for 
round like function.
+ * Signature search for round-like functions (round, round_bankers, ceil, 
floor, truncate).
  */
 public interface SearchSignatureForRound extends ExplicitlyCastableSignature {
+
+    int DOUBLE_DECIMAL_RESULT_MAX_SCALE = 15;
+
     @Override
     default FunctionSignature searchSignature(List<FunctionSignature> 
signatures) {
         List<Expression> arguments = getArguments();
         if (arguments.get(0).getDataType().isFloatLikeType()) {
             if (arguments.size() == 1) {
                 return 
FunctionSignature.ret(DoubleType.INSTANCE).args(DoubleType.INSTANCE);
             } else if (arguments.size() == 2) {
+                if (arguments.get(0).getDataType().isDoubleType()
+                        && isOptedIntoDecimalReroute()
+                        && isNonNegativeIntegerLiteralAtMost(arguments.get(1),
+                                DOUBLE_DECIMAL_RESULT_MAX_SCALE)) {
+                    return ExplicitlyCastableSignature.super.searchSignature(
+                            withoutFloatLikeReturnTypes(signatures));
+                }
                 return 
FunctionSignature.ret(DoubleType.INSTANCE).args(DoubleType.INSTANCE, 
IntegerType.INSTANCE);
             }
         }
         return ExplicitlyCastableSignature.super.searchSignature(signatures);
     }
+
+    static boolean isOptedIntoDecimalReroute() {

Review Comment:
   Nit: `DOUBLE_DECIMAL_RESULT_MAX_SCALE = 15` corresponds to the ~15-17 
significant decimal digits that IEEE-754 double precision can faithfully 
represent. A brief comment explaining why 15 (and not 17 or 18) would help 
future maintainers understand the threshold.



##########
fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java:
##########
@@ -1915,6 +1918,23 @@ public void setMaxJoinNumberOfReorder(int 
maxJoinNumberOfReorder) {
     )
     public int decimalOverflowScale = 6;
 
+    @VarAttrDef.VarAttr(name = ROUND_DOUBLE_RETURNS_DECIMAL_FOR_CONST_SCALE,

Review Comment:
   The descriptions (both CN and EN) clearly explain the trade-off. A small 
suggestion: explicitly mention the `DECIMAL(30, 15)` target type since that is 
the intermediate cast that gates overflow. Users may wonder which DECIMAL 
precision/scale is used.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/SearchSignatureForRound.java:
##########
@@ -18,26 +18,86 @@
 package org.apache.doris.nereids.trees.expressions.functions;
 
 import org.apache.doris.catalog.FunctionSignature;
+import org.apache.doris.nereids.CascadesContext;
+import org.apache.doris.nereids.rules.expression.ExpressionRewriteContext;
+import org.apache.doris.nereids.rules.expression.rules.FoldConstantRuleOnFE;
+import org.apache.doris.nereids.trees.expressions.Cast;
 import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLikeLiteral;
 import org.apache.doris.nereids.types.DoubleType;
 import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.nereids.types.coercion.Int32OrLessType;
+import org.apache.doris.qe.ConnectContext;
 
+import java.math.BigInteger;
+import java.util.ArrayList;
 import java.util.List;
 
 /**
- * if argument 0 is float or double, we should return double signature for 
round like function.
+ * Signature search for round-like functions (round, round_bankers, ceil, 
floor, truncate).
  */
 public interface SearchSignatureForRound extends ExplicitlyCastableSignature {
+
+    int DOUBLE_DECIMAL_RESULT_MAX_SCALE = 15;
+
     @Override
     default FunctionSignature searchSignature(List<FunctionSignature> 
signatures) {
         List<Expression> arguments = getArguments();
         if (arguments.get(0).getDataType().isFloatLikeType()) {
             if (arguments.size() == 1) {
                 return 
FunctionSignature.ret(DoubleType.INSTANCE).args(DoubleType.INSTANCE);
             } else if (arguments.size() == 2) {
+                if (arguments.get(0).getDataType().isDoubleType()
+                        && isOptedIntoDecimalReroute()
+                        && isNonNegativeIntegerLiteralAtMost(arguments.get(1),
+                                DOUBLE_DECIMAL_RESULT_MAX_SCALE)) {
+                    return ExplicitlyCastableSignature.super.searchSignature(
+                            withoutFloatLikeReturnTypes(signatures));
+                }
                 return 
FunctionSignature.ret(DoubleType.INSTANCE).args(DoubleType.INSTANCE, 
IntegerType.INSTANCE);
             }
         }
         return ExplicitlyCastableSignature.super.searchSignature(signatures);
     }
+
+    static boolean isOptedIntoDecimalReroute() {
+        ConnectContext ctx = ConnectContext.get();
+        return ctx != null && 
ctx.getSessionVariable().roundDoubleReturnsDecimalForConstScale;
+    }
+
+    /**
+     * True iff scale folds to an integer literal whose value lies in the 
closed range
+     * [0, maxValue].
+     */
+    static boolean isNonNegativeIntegerLiteralAtMost(Expression scale, int 
maxValue) {
+        Expression folded = scale;
+        if (!folded.isLiteral() && !folded.isSlot()) {
+            ExpressionRewriteContext ctx = new 
ExpressionRewriteContext(CascadesContext.initTempContext());
+            folded = FoldConstantRuleOnFE.evaluate(folded, ctx);
+        }
+        Expression unwrapped = folded;
+        if (unwrapped instanceof Cast && unwrapped.child(0).isLiteral()
+                && unwrapped.child(0).getDataType() instanceof 
Int32OrLessType) {
+            unwrapped = unwrapped.child(0);
+        }
+        if (!(unwrapped instanceof IntegerLikeLiteral)) {
+            return false;
+        }
+        Number number = ((IntegerLikeLiteral) unwrapped).getNumber();
+        BigInteger value = (number instanceof BigInteger)
+                ? (BigInteger) number
+                : BigInteger.valueOf(number.longValue());
+        return value.signum() >= 0 && 
value.compareTo(BigInteger.valueOf(maxValue)) <= 0;
+    }
+
+    /** Drop signatures whose return type is a float-like type, so the search 
falls onto decimal. */
+    static List<FunctionSignature> 
withoutFloatLikeReturnTypes(List<FunctionSignature> signatures) {
+        List<FunctionSignature> result = new ArrayList<>(signatures.size());
+        for (FunctionSignature signature : signatures) {
+            if (!signature.returnType.isFloatLikeType()) {
+                result.add(signature);
+            }

Review Comment:
   The Cast-unwrap logic handles `Cast(IntegerLiteral(N), IntType)`, but there 
is a subtle ordering: we unwrap *after* the fold step. If the fold step already 
reduces `Cast(IntegerLiteral(3), IntType)` to `IntegerLiteral(3)`, the unwrap 
path is never taken. Both paths produce the correct result, but the unwrap path 
acts as a safety net when folding doesn't eliminate the Cast. This is fine, 
just noting the dual-path design.



##########
fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/SearchSignatureForRoundTest.java:
##########
@@ -0,0 +1,223 @@
+// 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.doris.nereids.trees.expressions.functions;
+
+import org.apache.doris.nereids.trees.expressions.Cast;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Ceil;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Floor;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Round;
+import 
org.apache.doris.nereids.trees.expressions.functions.scalar.RoundBankers;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Truncate;
+import org.apache.doris.nereids.trees.expressions.literal.BigIntLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.DoubleLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.LargeIntLiteral;
+import org.apache.doris.nereids.types.DecimalV3Type;
+import org.apache.doris.nereids.types.DoubleType;
+import org.apache.doris.nereids.types.FloatType;
+import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.qe.ConnectContext;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import java.math.BigInteger;
+
+public class SearchSignatureForRoundTest {
+
+    private static final DoubleLiteral DOUBLE_VAL = new 
DoubleLiteral(81.56996587030717);
+
+    /** Run {@code body} with a fresh ConnectContext whose new opt-in var is 
set to {@code optIn}. */
+    private static void withOptIn(boolean optIn, Runnable body) {
+        try (MockedStatic<ConnectContext> mockedContext = 
Mockito.mockStatic(ConnectContext.class)) {
+            ConnectContext ctx = new ConnectContext();
+            ctx.getSessionVariable().roundDoubleReturnsDecimalForConstScale = 
optIn;
+            mockedContext.when(ConnectContext::get).thenReturn(ctx);
+            body.run();
+        }
+    }
+
+    private static void assertDecimalReturn(int expectedScale, Expression 
expr) {
+        Assertions.assertTrue(expr.getDataType() instanceof DecimalV3Type,
+                () -> "expected DecimalV3Type, got " + expr.getDataType());
+        DecimalV3Type t = (DecimalV3Type) expr.getDataType();
+        Assertions.assertEquals(expectedScale, t.getScale(),
+                () -> "expected scale=" + expectedScale + ", got " + t);
+    }
+
+    private static void assertDoubleReturn(Expression expr) {
+        Assertions.assertTrue(expr.getDataType() instanceof DoubleType,
+                () -> "expected DoubleType, got " + expr.getDataType());
+    }
+
+    // ---- opt-in ON: (DOUBLE, non-negative int literal <= 15) routes to 
DECIMAL ----
+
+    @Test
+    void roundDoubleWithConstScaleReturnsDecimal() {
+        withOptIn(true, () ->
+                assertDecimalReturn(2, new Round(DOUBLE_VAL, new 
IntegerLiteral(2))));
+    }
+
+    @Test
+    void roundBankersDoubleWithConstScaleReturnsDecimal() {
+        withOptIn(true, () ->
+                assertDecimalReturn(2, new RoundBankers(DOUBLE_VAL, new 
IntegerLiteral(2))));
+    }
+
+    @Test
+    void ceilDoubleWithConstScaleReturnsDecimal() {
+        withOptIn(true, () ->
+                assertDecimalReturn(2, new Ceil(DOUBLE_VAL, new 
IntegerLiteral(2))));
+    }
+
+    @Test
+    void floorDoubleWithConstScaleReturnsDecimal() {
+        withOptIn(true, () ->
+                assertDecimalReturn(2, new Floor(DOUBLE_VAL, new 
IntegerLiteral(2))));
+    }
+
+    @Test
+    void truncateDoubleWithConstScaleReturnsDecimal() {
+        withOptIn(true, () ->
+                assertDecimalReturn(2, new Truncate(DOUBLE_VAL, new 
IntegerLiteral(2))));
+    }
+
+    @Test
+    void zeroScaleAlsoReturnsDecimal() {
+        withOptIn(true, () ->
+                assertDecimalReturn(0, new Round(DOUBLE_VAL, new 
IntegerLiteral(0))));
+    }
+
+    @Test
+    void roundDoubleAtMaxPreservableScaleReturnsDecimal() {
+        // scale 15 == DOUBLE_DECIMAL.scale.
+        withOptIn(true, () ->
+                assertDecimalReturn(15, new Round(DOUBLE_VAL, new 
IntegerLiteral(15))));
+    }
+
+    @Test
+    void roundDoubleWithCastIntLiteralReturnsDecimal() {
+        withOptIn(true, () -> {
+            Cast wrapped = new Cast(new IntegerLiteral(3), 
IntegerType.INSTANCE);
+            assertDecimalReturn(3, new Round(DOUBLE_VAL, wrapped));
+        });
+    }
+
+    // ---- opt-in ON but shape doesn't match: stays DOUBLE ----
+
+    @Test
+    void roundDoubleSingleArgStaysDouble() {
+        withOptIn(true, () -> assertDoubleReturn(new Round(DOUBLE_VAL)));
+    }
+
+    @Test
+    void roundDoubleNegativeScaleStaysDouble() {
+        withOptIn(true, () ->
+                assertDoubleReturn(new Round(DOUBLE_VAL, new 
IntegerLiteral(-1))));
+    }
+
+    @Test
+    void roundDoubleScaleFromColumnStaysDouble() {
+        // When the scale comes from a column (non-literal), we cannot pick a
+        // fixed decimal scale at plan time, so we keep the original behavior.
+        withOptIn(true, () -> {
+            SlotReference scaleCol = new SlotReference("n", 
IntegerType.INSTANCE);
+            assertDoubleReturn(new Round(DOUBLE_VAL, scaleCol));
+        });
+    }
+
+    @Test
+    void roundFloatWithConstScaleStaysDouble() {
+        // FLOAT input keeps the original DOUBLE return path.
+        withOptIn(true, () -> {
+            SlotReference floatCol = new SlotReference("f", 
FloatType.INSTANCE);
+            assertDoubleReturn(new Round(floatCol, new IntegerLiteral(2)));
+        });
+    }
+
+    @Test
+    void roundDoubleScaleAboveMaxPreservableStaysDouble() {
+        // scale 17 exceeds DOUBLE_DECIMAL.scale (15).
+        withOptIn(true, () ->
+                assertDoubleReturn(new Round(DOUBLE_VAL, new 
IntegerLiteral(17))));
+    }
+
+    @Test
+    void roundDecimalKeepsExistingDecimalSignature() {
+        // The decimal-input path is independent of the new var.
+        withOptIn(true, () -> {
+            DecimalV3Type t = DecimalV3Type.createDecimalV3Type(10, 5);
+            SlotReference dec = new SlotReference("d", t);
+            assertDecimalReturn(2, new Round(dec, new IntegerLiteral(2)));
+        });
+    }
+
+    @Test
+    void roundDoubleBigIntScaleAboveIntRangeStaysDouble() {
+        withOptIn(true, () ->
+                assertDoubleReturn(new Round(DOUBLE_VAL, new 
BigIntLiteral(4294967298L))));
+    }
+
+    @Test
+    void roundDoubleBigIntScaleAtIntMaxPlusOneStaysDouble() {
+        withOptIn(true, () ->
+                assertDoubleReturn(new Round(DOUBLE_VAL, new 
BigIntLiteral(2147483648L))));

Review Comment:
   Consider adding a test for the fold path where the scale argument is a 
non-literal, non-slot expression that folds to a constant integer, e.g.:
   ```java
   // ADD(IntegerLiteral(1), IntegerLiteral(2)) folds to IntegerLiteral(3)
   new Round(DOUBLE_VAL, new Add(new IntegerLiteral(1), new IntegerLiteral(2)))
   ```
   This would exercise `FoldConstantRuleOnFE.evaluate` in the 
`!folded.isLiteral() && !folded.isSlot()` branch.



##########
regression-test/suites/query_p0/sql_functions/math_functions/test_round_double_tail.groovy:
##########
@@ -0,0 +1,79 @@
+// 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.
+
+suite("test_round_double_tail") {
+    sql "set round_double_returns_decimal_for_const_scale = true"
+
+    qt_round_const           """ select round(23900/293, 2); """
+    qt_round_bankers_const   """ select round_bankers(23900/293, 2); """
+    qt_ceil_const            """ select ceil(23900/293, 2); """
+    qt_floor_const           """ select floor(23900/293, 2); """
+    qt_truncate_const        """ select truncate(23900/293, 2); """
+
+    sql """ DROP TABLE IF EXISTS test_round_double_tail_t; """
+    sql """
+        CREATE TABLE test_round_double_tail_t (
+            id INT,
+            num BIGINT,
+            den BIGINT,
+            d DOUBLE
+        ) DISTRIBUTED BY HASH(id) BUCKETS 1
+        PROPERTIES ("replication_num" = "1");
+    """
+    sql """
+        INSERT INTO test_round_double_tail_t VALUES
+            (1, 239,  293,  cast(239  as double) / cast(293  as double) * 100),
+            (2, 458,  486,  cast(458  as double) / cast(486  as double) * 100),
+            (3, 1033, 1101, cast(1033 as double) / cast(1101 as double) * 100),
+            (4, 1040, 1101, cast(1040 as double) / cast(1101 as double) * 100),
+            (5, 140,  172,  cast(140  as double) / cast(172  as double) * 100);
+    """
+
+    qt_round_col          """ SELECT id, round(d, 2)         FROM 
test_round_double_tail_t ORDER BY id; """
+    qt_round_bankers_col  """ SELECT id, round_bankers(d, 2) FROM 
test_round_double_tail_t ORDER BY id; """
+    qt_ceil_col           """ SELECT id, ceil(d, 2)          FROM 
test_round_double_tail_t ORDER BY id; """
+    qt_floor_col          """ SELECT id, floor(d, 2)         FROM 
test_round_double_tail_t ORDER BY id; """
+    qt_truncate_col       """ SELECT id, truncate(d, 2)      FROM 
test_round_double_tail_t ORDER BY id; """
+
+    qt_round_concat_pct """
+        SELECT id, concat(round(cast(num as double) * 100 / cast(den as 
double), 2), '%')
+        FROM test_round_double_tail_t
+        ORDER BY id;
+    """
+
+    qt_round_single_arg      """ SELECT id, round(d)     FROM 
test_round_double_tail_t ORDER BY id; """
+    qt_round_negative_scale  """ SELECT id, round(d, -1) FROM 
test_round_double_tail_t ORDER BY id; """
+
+    // Scale from a column also stays double.
+    sql """ DROP TABLE IF EXISTS test_round_double_tail_scale_col; """
+    sql """
+        CREATE TABLE test_round_double_tail_scale_col (
+            id INT,
+            d  DOUBLE,
+            n  INT
+        ) DISTRIBUTED BY HASH(id) BUCKETS 1
+        PROPERTIES ("replication_num" = "1");
+    """
+    sql """
+        INSERT INTO test_round_double_tail_scale_col VALUES

Review Comment:
   The `round_scale_col` query correctly shows the old DOUBLE behavior with 
tail digits when the scale comes from a column — this is expected since 
column-based scales cannot be resolved at plan time.
   
   Consider also adding a regression test for the overflow case mentioned in 
the PR description (e.g., `round(1e16, 2)` or `round(1e300, 2)`) to document 
and lock in the NULL/ARITHMETIC_OVERFLOW behavior when the opt-in is enabled. 
Even a simple test like:
   ```groovy
   qt_round_overflow """ select round(1e16, 2); """
   ```
   would be valuable for regression prevention.



##########
fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/SearchSignatureForRoundTest.java:
##########
@@ -0,0 +1,223 @@
+// 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.doris.nereids.trees.expressions.functions;
+
+import org.apache.doris.nereids.trees.expressions.Cast;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Ceil;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Floor;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Round;
+import 
org.apache.doris.nereids.trees.expressions.functions.scalar.RoundBankers;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Truncate;
+import org.apache.doris.nereids.trees.expressions.literal.BigIntLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.DoubleLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.LargeIntLiteral;
+import org.apache.doris.nereids.types.DecimalV3Type;
+import org.apache.doris.nereids.types.DoubleType;
+import org.apache.doris.nereids.types.FloatType;
+import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.qe.ConnectContext;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import java.math.BigInteger;
+
+public class SearchSignatureForRoundTest {
+
+    private static final DoubleLiteral DOUBLE_VAL = new 
DoubleLiteral(81.56996587030717);
+
+    /** Run {@code body} with a fresh ConnectContext whose new opt-in var is 
set to {@code optIn}. */
+    private static void withOptIn(boolean optIn, Runnable body) {
+        try (MockedStatic<ConnectContext> mockedContext = 
Mockito.mockStatic(ConnectContext.class)) {
+            ConnectContext ctx = new ConnectContext();
+            ctx.getSessionVariable().roundDoubleReturnsDecimalForConstScale = 
optIn;
+            mockedContext.when(ConnectContext::get).thenReturn(ctx);
+            body.run();
+        }
+    }
+
+    private static void assertDecimalReturn(int expectedScale, Expression 
expr) {
+        Assertions.assertTrue(expr.getDataType() instanceof DecimalV3Type,
+                () -> "expected DecimalV3Type, got " + expr.getDataType());
+        DecimalV3Type t = (DecimalV3Type) expr.getDataType();
+        Assertions.assertEquals(expectedScale, t.getScale(),
+                () -> "expected scale=" + expectedScale + ", got " + t);
+    }
+
+    private static void assertDoubleReturn(Expression expr) {
+        Assertions.assertTrue(expr.getDataType() instanceof DoubleType,
+                () -> "expected DoubleType, got " + expr.getDataType());
+    }
+
+    // ---- opt-in ON: (DOUBLE, non-negative int literal <= 15) routes to 
DECIMAL ----
+
+    @Test
+    void roundDoubleWithConstScaleReturnsDecimal() {
+        withOptIn(true, () ->
+                assertDecimalReturn(2, new Round(DOUBLE_VAL, new 
IntegerLiteral(2))));
+    }
+
+    @Test
+    void roundBankersDoubleWithConstScaleReturnsDecimal() {
+        withOptIn(true, () ->
+                assertDecimalReturn(2, new RoundBankers(DOUBLE_VAL, new 
IntegerLiteral(2))));
+    }
+
+    @Test
+    void ceilDoubleWithConstScaleReturnsDecimal() {
+        withOptIn(true, () ->
+                assertDecimalReturn(2, new Ceil(DOUBLE_VAL, new 
IntegerLiteral(2))));
+    }
+
+    @Test
+    void floorDoubleWithConstScaleReturnsDecimal() {
+        withOptIn(true, () ->
+                assertDecimalReturn(2, new Floor(DOUBLE_VAL, new 
IntegerLiteral(2))));
+    }
+
+    @Test
+    void truncateDoubleWithConstScaleReturnsDecimal() {
+        withOptIn(true, () ->
+                assertDecimalReturn(2, new Truncate(DOUBLE_VAL, new 
IntegerLiteral(2))));
+    }
+
+    @Test
+    void zeroScaleAlsoReturnsDecimal() {
+        withOptIn(true, () ->
+                assertDecimalReturn(0, new Round(DOUBLE_VAL, new 
IntegerLiteral(0))));
+    }
+
+    @Test
+    void roundDoubleAtMaxPreservableScaleReturnsDecimal() {
+        // scale 15 == DOUBLE_DECIMAL.scale.
+        withOptIn(true, () ->
+                assertDecimalReturn(15, new Round(DOUBLE_VAL, new 
IntegerLiteral(15))));
+    }
+
+    @Test
+    void roundDoubleWithCastIntLiteralReturnsDecimal() {
+        withOptIn(true, () -> {
+            Cast wrapped = new Cast(new IntegerLiteral(3), 
IntegerType.INSTANCE);
+            assertDecimalReturn(3, new Round(DOUBLE_VAL, wrapped));
+        });
+    }
+
+    // ---- opt-in ON but shape doesn't match: stays DOUBLE ----
+
+    @Test

Review Comment:
   Nice test — `Cast(IntegerLiteral(3), IntegerType.INSTANCE)` specifically 
validates the Cast-unwrap path in `isNonNegativeIntegerLiteralAtMost`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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


Reply via email to