diff --git a/core/src/main/java/org/apache/calcite/rex/RexSimplify.java b/core/src/main/java/org/apache/calcite/rex/RexSimplify.java index 386964963ce..7aead67cb61 100644 --- a/core/src/main/java/org/apache/calcite/rex/RexSimplify.java +++ b/core/src/main/java/org/apache/calcite/rex/RexSimplify.java @@ -794,6 +794,24 @@ private > RexNode simplifyComparison(RexCall e, } } + // Simplify comparisons involving RAND() function. + // RAND() returns values in [0, 1), so certain comparisons can be reduced. + // Short-circuit the (potentially recursive) checks below when neither + // operand mentions RAND(), which is the common case. + if (findRandCall(o0) != null || findRandCall(o1) != null) { + final @Nullable RexNode randSimplified = + simplifyRandComparison(e.getKind(), o0, o1); + if (randSimplified != null) { + return randSimplified; + } + + // Normalize comparisons with arithmetic on RAND(), e.g. RAND() * 3 < 3 → RAND() < 1.0 + final @Nullable RexNode normalized = normalizeRandComparison(e); + if (normalized != null && !normalized.equals(e)) { + return simplify(normalized, unknownAs); + } + } + RexNode node = simplifyComparisonWithNull(e, unknownAs); if (node instanceof RexLiteral) { return node; @@ -3744,4 +3762,288 @@ void addSarg(Sarg sarg, boolean negate, RelDataType type) { } } } + + /** Checks if a RexNode is a numeric literal (possibly wrapped in CAST). */ + private static boolean isNumericLiteral(RexNode node) { + RexNode stripped = RexUtil.removeCast(node); + return stripped.isA(SqlKind.LITERAL) + && SqlTypeUtil.isNumeric(stripped.getType()); + } + + /** Extracts an exact {@link BigDecimal} value from a numeric literal. */ + private static BigDecimal getLiteralBigDecimal(RexNode node) { + RexNode stripped = RexUtil.removeCast(node); + assert stripped instanceof RexLiteral; + Comparable value = ((RexLiteral) stripped).getValue(); + if (value instanceof BigDecimal) { + return (BigDecimal) value; + } + if (value instanceof Double) { + return BigDecimal.valueOf((Double) value); + } + throw new AssertionError("Unexpected literal value: " + value); + } + + /** + * Normalizes comparisons with arithmetic on RAND() into a constant boolean + * when the comparison is decidable from the [0, 1) range. + * For example, RAND() * 3 < 3 → RAND() < 1.0 → true. + * + * @param e The comparison expression + * @return Constant boolean literal, or null if the comparison is undecidable + */ + private @Nullable RexNode normalizeRandComparison(RexCall e) { + final RexNode o0 = e.operands.get(0); + final RexNode o1 = e.operands.get(1); + + // Try: randExpr literal + RexNode result = tryNormalizeRandExpr(o0, o1, e.getKind()); + if (result != null) { + return result; + } + // Try: literal randExpr → flip comparison and normalize + return tryNormalizeRandExpr(o1, o0, e.getKind().reverse()); + } + + /** + * Counts the number of RAND() calls in an expression tree. + */ + private static int countRandCalls(RexNode expr) { + if (isRandCall(expr)) { + return 1; + } + if (expr instanceof RexCall) { + int count = 0; + for (RexNode operand : ((RexCall) expr).getOperands()) { + count += countRandCalls(operand); + } + return count; + } + return 0; + } + + /** + * Finds the first RAND() call in an expression tree. + */ + private static @Nullable RexNode findRandCall(RexNode expr) { + if (isRandCall(expr)) { + return expr; + } + if (expr instanceof RexCall) { + for (RexNode operand : ((RexCall) expr).getOperands()) { + RexNode rand = findRandCall(operand); + if (rand != null) { + return rand; + } + } + } + return null; + } + + /** + * Extracts the linear coefficient and offset of RAND() from an expression. + * For a linear expression coef * RAND() + offset, returns Pair.of(coef, offset). + * Returns null if the expression is not linear in RAND(). + * + *

Uses {@link BigDecimal} arithmetic so that the normalized bound is exact; + * a non-terminating division yields null rather than a rounded approximation. + */ + private @Nullable Pair extractLinearRand(RexNode expr) { + if (isRandCall(expr)) { + return Pair.of(BigDecimal.ONE, BigDecimal.ZERO); + } + if (isNumericLiteral(expr)) { + return Pair.of(BigDecimal.ZERO, getLiteralBigDecimal(expr)); + } + if (!(expr instanceof RexCall)) { + return null; + } + final RexCall call = (RexCall) expr; + if (call.getOperands().size() != 2) { + return null; + } + final Pair l = extractLinearRand(call.getOperands().get(0)); + final Pair r = extractLinearRand(call.getOperands().get(1)); + if (l == null || r == null) { + return null; + } + + switch (call.getKind()) { + case PLUS: + case CHECKED_PLUS: + return Pair.of(l.left.add(r.left), l.right.add(r.right)); + case MINUS: + case CHECKED_MINUS: + return Pair.of(l.left.subtract(r.left), l.right.subtract(r.right)); + case TIMES: + case CHECKED_TIMES: + // Non-linear: both sides contain RAND() → RAND() * RAND() + if (l.left.signum() != 0 && r.left.signum() != 0) { + return null; + } + // (c1*RAND + o1) * (c2*RAND + o2) where c1=0 or c2=0 + // Result: (c1*o2 + c2*o1)*RAND + o1*o2 + return Pair.of(l.left.multiply(r.right).add(l.right.multiply(r.left)), + l.right.multiply(r.right)); + case DIVIDE: + case CHECKED_DIVIDE: + // Right side cannot contain RAND() + if (r.left.signum() != 0) { + return null; + } + if (r.right.signum() == 0) { + return null; // divide by zero + } + try { + return Pair.of(l.left.divide(r.right), l.right.divide(r.right)); + } catch (ArithmeticException e) { + return null; // non-terminating decimal expansion; cannot represent exactly + } + default: + return null; + } + } + + /** + * Tries to normalize {@code randExpr literal} by rewriting the linear + * arithmetic on RAND() into a comparison of RAND() against a constant bound, + * then evaluating it against the [0, 1) range. + * For example, {@code 2*RAND()+1 < 3 → RAND() < 1.0 → true}. + * + * @param randExpr Expression that may contain RAND() + * @param literal The other operand (must be a literal) + * @param kind Comparison kind + * @return Constant boolean literal if the comparison is decidable, else null + */ + private @Nullable RexNode tryNormalizeRandExpr(RexNode randExpr, RexNode literal, + SqlKind kind) { + if (!isNumericLiteral(literal)) { + return null; + } + // Already pure RAND(), no arithmetic to normalize + if (isRandCall(randExpr)) { + return null; + } + // Must contain exactly one RAND() call + if (countRandCalls(randExpr) != 1) { + return null; + } + Pair linear = extractLinearRand(randExpr); + if (linear == null) { + return null; + } + final BigDecimal coef = linear.left; + final BigDecimal offset = linear.right; + if (coef.signum() == 0) { + return null; // no RAND() in expression + } + + // coef * RAND() + offset d + // → RAND() (d - offset) / coef (flip op when coef < 0) + // Avoid the division: the bound is (d - offset) / coef, so its position + // relative to an endpoint e is sign((d - offset) - e * coef) * sign(coef). + final BigDecimal d = getLiteralBigDecimal(literal); + final BigDecimal num = d.subtract(offset); + final int coefSign = coef.signum(); + final SqlKind newKind = coefSign > 0 ? kind : kind.reverse(); + final int cmp0 = num.signum() * coefSign; // bound vs 0 + final int cmp1 = num.subtract(coef).signum() * coefSign; // bound vs 1 + final Boolean result = evalRandComparison(newKind, cmp0, cmp1); + return result == null ? null : rexBuilder.makeLiteral(result); + } + + /** Checks if a RexNode is a RAND() function call. + * Note: RAND(seed) with seed is also handled. + */ + private static boolean isRandCall(RexNode node) { + if (node instanceof RexCall) { + RexCall call = (RexCall) node; + return call.getOperator() == SqlStdOperatorTable.RAND; + } + return false; + } + + /** Simplifies comparisons involving RAND() function. + * RAND() returns values in [0, 1), so certain comparisons can be reduced. + * For example, RAND() > 1.0 is always false, RAND() < 0.0 is always false. + * + * @param kind The comparison kind (GREATER_THAN, LESS_THAN, etc.) + * @param o0 The first operand + * @param o1 The second operand + * @return The simplified RexNode, or null if no simplification is possible + */ + private @Nullable RexNode simplifyRandComparison(SqlKind kind, RexNode o0, RexNode o1) { + // Check if one operand is RAND() and the other is a literal (possibly wrapped in CAST) + RexNode strippedO0 = RexUtil.removeCast(o0); + RexNode strippedO1 = RexUtil.removeCast(o1); + if (isRandCall(strippedO0) && strippedO1.isA(SqlKind.LITERAL)) { + return simplifyRandComparison0(kind, (RexLiteral) strippedO1, true); + } + if (strippedO0.isA(SqlKind.LITERAL) && isRandCall(strippedO1)) { + return simplifyRandComparison0(kind, (RexLiteral) strippedO0, false); + } + return null; + } + + /** Helper method to simplify RAND() comparison. + * + * @param kind The comparison kind + * @param literal The literal value + * @param randIsLeft Whether RAND() is on the left side of the comparison + * @return The simplified RexNode, or null if no simplification is possible + */ + private @Nullable RexNode simplifyRandComparison0(SqlKind kind, RexLiteral literal, + boolean randIsLeft) { + // For DOUBLE type, getValue() returns Double; for DECIMAL, it returns BigDecimal + final Comparable value = literal.getValue(); + final BigDecimal d; + if (value instanceof BigDecimal) { + d = (BigDecimal) value; + } else if (value instanceof Double) { + d = BigDecimal.valueOf((Double) value); + } else { + return null; + } + // Compare the bound d against the range endpoints 0 and 1. + final SqlKind op = randIsLeft ? kind : kind.reverse(); + final Boolean result = evalRandComparison(op, d.signum(), d.compareTo(BigDecimal.ONE)); + return result == null ? null : rexBuilder.makeLiteral(result); + } + + /** + * Evaluates {@code RAND() bound} using the knowledge that RAND() lies in + * the half-open range [0, 1), given only the sign of {@code bound} and the + * sign of {@code bound - 1}. Taking the two comparisons as inputs lets callers + * decide them with exact arithmetic (e.g. via multiplication instead of + * division) rather than materializing the bound. + * + * @param op The comparison kind, oriented as RAND() on the left + * @param cmp0 The sign of {@code bound} (i.e. {@code bound} compared with 0) + * @param cmp1 The sign of {@code bound - 1} (i.e. {@code bound} compared with 1) + * @return TRUE or FALSE if the comparison is decidable, else null + */ + private static @Nullable Boolean evalRandComparison(SqlKind op, int cmp0, int cmp1) { + switch (op) { + case GREATER_THAN: + // RAND() > d: true if d < 0, false if d >= 1 + return cmp0 < 0 ? Boolean.TRUE : cmp1 >= 0 ? Boolean.FALSE : null; + case GREATER_THAN_OR_EQUAL: + // RAND() >= d: true if d <= 0, false if d >= 1 + return cmp0 <= 0 ? Boolean.TRUE : cmp1 >= 0 ? Boolean.FALSE : null; + case LESS_THAN: + // RAND() < d: true if d >= 1, false if d <= 0 + return cmp1 >= 0 ? Boolean.TRUE : cmp0 <= 0 ? Boolean.FALSE : null; + case LESS_THAN_OR_EQUAL: + // RAND() <= d: true if d >= 1, false if d < 0 + return cmp1 >= 0 ? Boolean.TRUE : cmp0 < 0 ? Boolean.FALSE : null; + case EQUALS: + // RAND() = d: always false when d is outside [0, 1); undecidable otherwise + return cmp0 < 0 || cmp1 >= 0 ? Boolean.FALSE : null; + case NOT_EQUALS: + // RAND() <> d: always true when d is outside [0, 1); undecidable otherwise + return cmp0 < 0 || cmp1 >= 0 ? Boolean.TRUE : null; + default: + return null; + } + } } diff --git a/core/src/test/java/org/apache/calcite/rex/RexProgramBuilderBase.java b/core/src/test/java/org/apache/calcite/rex/RexProgramBuilderBase.java index 4b873fce5c8..2cebbdecbdf 100644 --- a/core/src/test/java/org/apache/calcite/rex/RexProgramBuilderBase.java +++ b/core/src/test/java/org/apache/calcite/rex/RexProgramBuilderBase.java @@ -351,6 +351,14 @@ protected RexNode least(RexNode... nodes) { return rexBuilder.makeCall(SqlLibraryOperators.LEAST, nodes); } + protected RexNode rand() { + return rexBuilder.makeCall(SqlStdOperatorTable.RAND); + } + + protected RexNode rand(RexNode seed) { + return rexBuilder.makeCall(SqlStdOperatorTable.RAND, seed); + } + protected RexNode m2v(RexNode n) { return rexBuilder.makeCall(SqlInternalOperators.M2V, n); } diff --git a/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java b/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java index 5f8b8edfb1d..16a6d9f3e0e 100644 --- a/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java +++ b/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java @@ -1135,6 +1135,72 @@ private void checkExponentialCnf(int n) { checkSimplify(div(vInt(), nullInt), "null:INTEGER"); } + /** Test case for + * [CALCITE-7694] + * RexSimplify should simplify comparisons involving RAND() using its [0, 1) range. */ + @Test void simplifyRand() { + // RAND() >= 1 is always false; RAND() < 1 (equivalently) covers the bound + checkSimplify(gt(rand(), literal(1.0)), "false"); + checkSimplify(ge(rand(), literal(1.0)), "false"); + checkSimplify(lt(rand(), literal(0.0)), "false"); + checkSimplify(le(rand(), literal(-0.1)), "false"); + + // RAND() >= 0 > -0.5 is always true; RAND() < 1 is always true + checkSimplify(gt(rand(), literal(-0.5)), "true"); + checkSimplify(lt(rand(), literal(1.0)), "true"); + + // literal on the left is normalized by flipping the comparison + checkSimplify(gt(literal(1.0), rand()), "true"); + + // arithmetic on RAND() is normalized before the range check + // RAND() * 3 < 3 -> RAND() < 1.0 -> true + checkSimplify(lt(mul(rand(), literal(3)), literal(3)), "true"); + // RAND() + 1 < 2 -> RAND() < 1.0 -> true + checkSimplify(lt(add(rand(), literal(1)), literal(2)), "true"); + // RAND() - 1 > 0 -> RAND() > 1.0 -> false + checkSimplify(gt(sub(rand(), literal(1)), literal(0)), "false"); + // RAND() / 2 > 1 -> RAND() > 2.0 -> false + checkSimplify(gt(div(rand(), literal(2)), literal(1)), "false"); + // negative coefficient flips the comparison: 1 - RAND() > 1 -> RAND() < 0.0 -> false + checkSimplify(gt(sub(literal(1), rand()), literal(1)), "false"); + + // RAND() = d / RAND() <> d for d outside [0, 1) + checkSimplify(eq(rand(), literal(5.0)), "false"); + checkSimplify(ne(rand(), literal(5.0)), "true"); + // arithmetic normalized before the equality check: RAND() * 2 = 4 -> RAND() = 2.0 -> false + checkSimplify(eq(mul(rand(), literal(2)), literal(4)), "false"); + + // RAND(seed) has the same [0, 1) range, so the same simplifications apply + checkSimplify(gt(rand(literal(1)), literal(1.0)), "false"); + checkSimplify(lt(rand(literal(1)), literal(1.0)), "true"); + + // exact BigDecimal arithmetic: RAND() * 0.1 < 0.3 -> RAND() < 3 -> true + checkSimplify( + lt(mul(rand(), literal(new BigDecimal("0.1"))), + literal(new BigDecimal("0.3"))), "true"); + // bound 5/3 is not exactly representable, but the endpoint comparison is done + // by multiplication (no division), so 3*RAND() < 5 -> RAND() < 5/3 -> true + checkSimplify(lt(mul(literal(3), rand()), literal(5)), "true"); + } + + /** Test case for + * [CALCITE-7694] + * RexSimplify should simplify comparisons involving RAND() using its [0, 1) range. */ + @Test void simplifyRandUnchanged() { + // RAND() in [0, 1): value inside the range is undecidable + checkSimplifyUnchanged(gt(rand(), literal(0.5))); + checkSimplifyUnchanged(lt(rand(), literal(0.5))); + // equality against a value inside [0, 1) is undecidable + checkSimplifyUnchanged(eq(rand(), literal(0.5))); + // RAND() * (-1) < 0 -> RAND() > 0.0, which is undecidable (RAND() may be 0) + checkSimplifyUnchanged(lt(mul(rand(), literal(-1)), literal(0))); + // non-linear: two RAND() calls are not normalized + checkSimplifyUnchanged(lt(mul(rand(), rand()), literal(1))); + // dividing RAND() by 3 gives a coefficient of 1/3 with no exact decimal + // expansion; extractLinearRand bails out rather than rounding the coefficient + checkSimplifyUnchanged(gt(div(rand(), literal(3)), literal(5))); + } + @Test void testSimplifyFilter() { final RelDataType booleanType = typeFactory.createSqlType(SqlTypeName.BOOLEAN); diff --git a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java index bad13247c3e..f9481202cde 100644 --- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java +++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java @@ -3758,6 +3758,31 @@ private void checkPushJoinThroughUnionOnRightDoesNotMatchSemiOrAntiJoin(JoinRelT sql(sql).withRule(CoreRules.PROJECT_REDUCE_EXPRESSIONS).check(); } + /** Tests that a RAND() predicate which is always false collapses the + * relation to empty via FILTER_REDUCE_EXPRESSIONS. Detailed simplification + * cases live in {@code RexProgramTest.simplifyRand}. */ + @Test void testRandComparisonSimplificationAlwaysFalse() { + HepProgramBuilder builder = new HepProgramBuilder(); + builder.addRuleClass(ReduceExpressionsRule.class); + HepPlanner hepPlanner = new HepPlanner(builder.build()); + hepPlanner.addRule(CoreRules.FILTER_REDUCE_EXPRESSIONS); + // RAND() > 1.0 is always false (RAND() in [0, 1)) + final String sql = "SELECT * FROM emp WHERE RAND() > 1.0"; + sql(sql).withPlanner(hepPlanner).check(); + } + + /** Tests that arithmetic on RAND() is normalized before the range check, so + * an always-true predicate removes the filter. */ + @Test void testRandComparisonSimplificationAlwaysTrue() { + HepProgramBuilder builder = new HepProgramBuilder(); + builder.addRuleClass(ReduceExpressionsRule.class); + HepPlanner hepPlanner = new HepPlanner(builder.build()); + hepPlanner.addRule(CoreRules.FILTER_REDUCE_EXPRESSIONS); + // RAND() * 3 < 3 → RAND() < 1.0 → always true + final String sql = "SELECT * FROM emp WHERE RAND() * 3 < 3"; + sql(sql).withPlanner(hepPlanner).check(); + } + /** Test case for * [CALCITE-6481] * Optimize 'VALUES...UNION...VALUES' to a single 'VALUES' the IN-list contains CAST diff --git a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml index c623e63038b..6c75872d3e5 100644 --- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml +++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml @@ -16497,6 +16497,42 @@ LogicalProject(EXPR$0=[1], A=[$1]) LogicalFilter(condition=[OR(AND(>=($1, CAST(1):DOUBLE NOT NULL), <=($1, CAST(10):DOUBLE NOT NULL)), =($1, CAST(1):DOUBLE NOT NULL), =($1, CAST(2):DOUBLE NOT NULL), =($1, CAST(3):DOUBLE NOT NULL), =($1, CAST(4):DOUBLE NOT NULL), =($1, CAST(5):DOUBLE NOT NULL), =($1, CAST(6):DOUBLE NOT NULL), =($1, CAST(7):DOUBLE NOT NULL), =($1, CAST(8):DOUBLE NOT NULL), =($1, CAST(9):DOUBLE NOT NULL), =($1, CAST(10):DOUBLE NOT NULL))]) LogicalProject(EXPR$0=[1], A=[ROUND(RAND())]) LogicalValues(tuples=[[{ 0 }]]) +]]> + + + + + 1.0]]> + + + (RAND(), CAST(1.0:DECIMAL(2, 1)):DOUBLE NOT NULL)]) + LogicalTableScan(table=[[CATALOG, SALES, EMP]]) +]]> + + + + + + + + + + + + + + diff --git a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml index a8da739aa94..b639549c7e1 100644 --- a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml +++ b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml @@ -8659,8 +8659,7 @@ LogicalProject(EXPR$0=[AND(=(1, CAST('y'):INTEGER NOT NULL), =(CAST('x'):INTEGER