github-actions[bot] commented on code in PR #69461:
URL: https://github.com/apache/doris/pull/69461#discussion_r4355706224


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownScoreTopNIntoOlapScan.java:
##########
@@ -295,20 +298,79 @@ private Optional<ScoreRangeInfo> 
extractScoreRangeInfo(Expression predicate) {
             }
         }
 
+        // A cast of score() changes the comparison semantics: CAST(score() AS 
DECIMALV3(p,s))
+        // rounds before comparing. Convert the rounded threshold back to an 
equivalent raw
+        // score threshold instead of dropping the cast, which would drop rows 
whose raw score
+        // rounds up to the requested value (DORIS-28583).
+        if (left instanceof Cast && isNumericLiteral(right)) {
+            Optional<ScoreRangeInfo> adjusted =
+                    extractCastScoreRangeInfo(comp, (Cast) left, right, false);
+            if (adjusted.isPresent()) {
+                return adjusted;
+            }
+        }
+
+        if (right instanceof Cast && isNumericLiteral(left)) {
+            Optional<ScoreRangeInfo> adjusted =
+                    extractCastScoreRangeInfo(comp, (Cast) right, left, true);
+            if (adjusted.isPresent()) {
+                return adjusted;
+            }
+        }
+
         return Optional.empty();
     }
 
     /**
-     * Check if the expression is a Score function, possibly wrapped in Cast 
expressions.
-     * The optimizer may wrap score() in Cast for type coercion (e.g., score() 
>= 4.0 may become
-     * CAST(score() AS DECIMAL) >= 4.0).
+     * Convert a rounded score comparison, such as
+     * CAST(score() AS DECIMALV3(2,1)) >= 0.5, into an equivalent raw score 
range.
+     *
+     * The cast rounds the score half away from zero, so the rounded value 
reaches the threshold
+     * for raw scores up to half of the last kept digit below it. Dropping the 
cast but keeping
+     * the literal threshold compares the raw score instead and loses those 
rows.
+     */
+    private Optional<ScoreRangeInfo> 
extractCastScoreRangeInfo(ComparisonPredicate comp, Cast cast,
+            Expression literal, boolean reversed) {
+        if (!isScoreExpression(cast.child())) {
+            return Optional.empty();
+        }
+        int scale;
+        if (cast.getDataType() instanceof DecimalV3Type) {
+            scale = ((DecimalV3Type) cast.getDataType()).getScale();
+        } else if (cast.getDataType().isIntegerLikeType()) {

Review Comment:
   [P1] Use truncation semantics for integer score casts
   
   Doris FLOAT-to-integer casts truncate (`CastToInt::from_float` uses 
`std::trunc`), so they cannot use the decimal half-step inverse. A concrete 
reduced input plan is:
   
   ```text
   TopN(s DESC, 10)
     Project(id, score() AS s)
       Filter(title MATCH_ANY 'apple' AND CAST(score() AS INT) >= 1)
         Scan(t)
   ```
   
   With equal-length rows `(1,'apple'), (2,'banana')`, the matching score is 
about 0.693 and its INT cast is 0. This branch generates raw `score >= 0.5`, 
and `pushDown` deletes the original cast predicate, so row 1 is now returned 
incorrectly. Previously raw `score >= 1` rejected it. TRY_CAST and the reversed 
comparison are affected too. Derive integer bounds from truncation separately 
from decimal rounding, or retain the exact predicate after materializing score; 
add integer-cast oracle coverage.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownScoreTopNIntoOlapScan.java:
##########
@@ -295,20 +298,79 @@ private Optional<ScoreRangeInfo> 
extractScoreRangeInfo(Expression predicate) {
             }
         }
 
+        // A cast of score() changes the comparison semantics: CAST(score() AS 
DECIMALV3(p,s))
+        // rounds before comparing. Convert the rounded threshold back to an 
equivalent raw
+        // score threshold instead of dropping the cast, which would drop rows 
whose raw score
+        // rounds up to the requested value (DORIS-28583).
+        if (left instanceof Cast && isNumericLiteral(right)) {
+            Optional<ScoreRangeInfo> adjusted =
+                    extractCastScoreRangeInfo(comp, (Cast) left, right, false);
+            if (adjusted.isPresent()) {
+                return adjusted;
+            }
+        }
+
+        if (right instanceof Cast && isNumericLiteral(left)) {
+            Optional<ScoreRangeInfo> adjusted =
+                    extractCastScoreRangeInfo(comp, (Cast) right, left, true);
+            if (adjusted.isPresent()) {
+                return adjusted;
+            }
+        }
+
         return Optional.empty();
     }
 
     /**
-     * Check if the expression is a Score function, possibly wrapped in Cast 
expressions.
-     * The optimizer may wrap score() in Cast for type coercion (e.g., score() 
>= 4.0 may become
-     * CAST(score() AS DECIMAL) >= 4.0).
+     * Convert a rounded score comparison, such as
+     * CAST(score() AS DECIMALV3(2,1)) >= 0.5, into an equivalent raw score 
range.
+     *
+     * The cast rounds the score half away from zero, so the rounded value 
reaches the threshold
+     * for raw scores up to half of the last kept digit below it. Dropping the 
cast but keeping
+     * the literal threshold compares the raw score instead and loses those 
rows.
+     */
+    private Optional<ScoreRangeInfo> 
extractCastScoreRangeInfo(ComparisonPredicate comp, Cast cast,
+            Expression literal, boolean reversed) {
+        if (!isScoreExpression(cast.child())) {
+            return Optional.empty();
+        }
+        int scale;
+        if (cast.getDataType() instanceof DecimalV3Type) {
+            scale = ((DecimalV3Type) cast.getDataType()).getScale();
+        } else if (cast.getDataType().isIntegerLikeType()) {
+            scale = 0;
+        } else {
+            return Optional.empty();
+        }
+        TExprOpcode op = reversed ? getReversedMinScoreOpcode(comp) : 
getMinScoreOpcode(comp);
+        if (op == null) {
+            return Optional.empty();
+        }
+        double halfStep = 0.5 * Math.pow(10.0, -scale);
+        double threshold = extractNumericValue(literal);
+        // round_s(score) >= t  <=>  score >= t - halfStep
+        // round_s(score) >  t  <=>  score >= t + halfStep
+        double rawThreshold = op == TExprOpcode.GE ? threshold - halfStep : 
threshold + halfStep;
+        return Optional.of(new ScoreRangeInfo(TExprOpcode.GE, rawThreshold));
+    }
+
+    /**
+     * Check if the expression is a Score function, allowing only a value 
preserving
+     * float -> double widening cast (which the analyzer may insert for type 
coercion).
+     * Rounding casts are handled by extractCastScoreRangeInfo.
      */
     private boolean isScoreExpression(Expression expr) {
         if (expr instanceof Score) {
             return true;
         }
         if (expr instanceof Cast) {
-            return isScoreExpression(((Cast) expr).child());
+            Cast cast = (Cast) expr;
+            // Only the value preserving float -> double widening may be 
dropped. Casts such as
+            // CAST(score() AS DECIMALV3(2,1)) round the score, so using the 
raw score with the
+            // rounded threshold is not equivalent to the original predicate.
+            return cast.child().getDataType() instanceof FloatType

Review Comment:
   [P2] Keep coerced numeric cast predicates evaluable
   
   Comparing the decimal-cast score with a DOUBLE literal adds another cast 
during analysis:
   
   ```text
   TopN(s DESC, 10)
     Project(id, score() AS s)
       Filter(title MATCH_ANY 'apple' AND
              CAST(CAST(score() AS DECIMALV3(2,1)) AS DOUBLE) >= DOUBLE(0.5))
         Scan(t)
   ```
   
   For example, the SQL predicate `CAST(score() AS DECIMALV3(2,1)) >= CAST(0.5 
AS DOUBLE)` produces this tree in both comparison-coercion modes. The wrapper 
survives simplification, but the new recognizer rejects its DECIMAL child, 
`extractCastScoreRangeInfo` also returns empty, and `pushDown` throws the 
score-range usage error. This previously accepted query now fails instead of 
filtering rounded scores. The narrowed target set also rejects direct LARGEINT 
casts because `isIntegerLikeType()` excludes LARGEINT. Recognize supported 
coercion chains while preserving the inner cast semantics, or evaluate the 
original predicate above materialized score instead of failing analysis.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownScoreTopNIntoOlapScan.java:
##########
@@ -295,20 +298,79 @@ private Optional<ScoreRangeInfo> 
extractScoreRangeInfo(Expression predicate) {
             }
         }
 
+        // A cast of score() changes the comparison semantics: CAST(score() AS 
DECIMALV3(p,s))
+        // rounds before comparing. Convert the rounded threshold back to an 
equivalent raw
+        // score threshold instead of dropping the cast, which would drop rows 
whose raw score
+        // rounds up to the requested value (DORIS-28583).
+        if (left instanceof Cast && isNumericLiteral(right)) {
+            Optional<ScoreRangeInfo> adjusted =
+                    extractCastScoreRangeInfo(comp, (Cast) left, right, false);
+            if (adjusted.isPresent()) {
+                return adjusted;
+            }
+        }
+
+        if (right instanceof Cast && isNumericLiteral(left)) {
+            Optional<ScoreRangeInfo> adjusted =
+                    extractCastScoreRangeInfo(comp, (Cast) right, left, true);
+            if (adjusted.isPresent()) {
+                return adjusted;
+            }
+        }
+
         return Optional.empty();
     }
 
     /**
-     * Check if the expression is a Score function, possibly wrapped in Cast 
expressions.
-     * The optimizer may wrap score() in Cast for type coercion (e.g., score() 
>= 4.0 may become
-     * CAST(score() AS DECIMAL) >= 4.0).
+     * Convert a rounded score comparison, such as
+     * CAST(score() AS DECIMALV3(2,1)) >= 0.5, into an equivalent raw score 
range.
+     *
+     * The cast rounds the score half away from zero, so the rounded value 
reaches the threshold
+     * for raw scores up to half of the last kept digit below it. Dropping the 
cast but keeping
+     * the literal threshold compares the raw score instead and loses those 
rows.
+     */
+    private Optional<ScoreRangeInfo> 
extractCastScoreRangeInfo(ComparisonPredicate comp, Cast cast,
+            Expression literal, boolean reversed) {
+        if (!isScoreExpression(cast.child())) {
+            return Optional.empty();
+        }
+        int scale;
+        if (cast.getDataType() instanceof DecimalV3Type) {
+            scale = ((DecimalV3Type) cast.getDataType()).getScale();
+        } else if (cast.getDataType().isIntegerLikeType()) {
+            scale = 0;
+        } else {
+            return Optional.empty();
+        }
+        TExprOpcode op = reversed ? getReversedMinScoreOpcode(comp) : 
getMinScoreOpcode(comp);
+        if (op == null) {
+            return Optional.empty();
+        }
+        double halfStep = 0.5 * Math.pow(10.0, -scale);
+        double threshold = extractNumericValue(literal);
+        // round_s(score) >= t  <=>  score >= t - halfStep
+        // round_s(score) >  t  <=>  score >= t + halfStep
+        double rawThreshold = op == TExprOpcode.GE ? threshold - halfStep : 
threshold + halfStep;

Review Comment:
   [P1] Preserve strictness when the decimal half-step rounds away
   
   For `CAST(score() AS DECIMALV3(21,19)) > 1.0000000000000000000`, `halfStep` 
is `5e-20`; double addition makes `rawThreshold` exactly 1.0, and the 
unconditional GE admits equality. This is reachable with the existing 
indexed-table shape:
   
   ```text
   TopN(s DESC, 100)
     Project(id, score() AS s)
       Filter(SEARCH('*', '{"default_field":"title","mode":"lucene"}') AND
              CAST(score() AS DECIMALV3(21,19)) > 1.0000000000000000000)
         Scan(t)
   ```
   
   SEARCH match-all uses `AllScorer`, which returns exactly `1.0F`. Its decimal 
cast equals 1, so the input predicate rejects every row; the rewritten scan 
uses GE 1 and returns them after the cast predicate is removed. The previous GT 
1 rejected them correctly. Derive the bound in the actual score domain with 
strictness preserved, or keep the exact residual predicate when a double bound 
cannot encode it. Cover high-scale > and reversed < at equality.



-- 
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