This is an automated email from the ASF dual-hosted git repository.
jhyde pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/main by this push:
new 8ea4160f10 [CALCITE-5722] Sarg.isComplementedPoints fails with
anti-points which are equal under `compareTo` but not `equals`
8ea4160f10 is described below
commit 8ea4160f10e95aca6c3b0029d505bbc56975a873
Author: ian.bertolacci <[email protected]>
AuthorDate: Wed May 24 12:49:40 2023 -0700
[CALCITE-5722] Sarg.isComplementedPoints fails with anti-points which are
equal under `compareTo` but not `equals`
BigDecimal is a type that implements Comparable but whose
natural order is not consistent with equals. For example, if
x = BigDecimal("1.0") and and y = BigDecimal("1"),
x.equals(y) returns false and x.compareTo(y) returns 0.
Guava RangeSet is OK with such types, and Sarg should be also.
Before this fix, Sarg could not deduce that
z < 1 or 1.0 < z
is equivalent to
z <> 1
Close apache/calcite#3224
---
.../java/org/apache/calcite/util/RangeSets.java | 10 +-
.../org/apache/calcite/rex/RexProgramTest.java | 54 +++++++++
.../java/org/apache/calcite/util/RangeSetTest.java | 125 ++++++++++++++++++++-
.../java/org/apache/calcite/test/Matchers.java | 14 ++-
4 files changed, 192 insertions(+), 11 deletions(-)
diff --git a/core/src/main/java/org/apache/calcite/util/RangeSets.java
b/core/src/main/java/org/apache/calcite/util/RangeSets.java
index a0967e2383..465ae83cc8 100644
--- a/core/src/main/java/org/apache/calcite/util/RangeSets.java
+++ b/core/src/main/java/org/apache/calcite/util/RangeSets.java
@@ -129,7 +129,7 @@ public class RangeSets {
public static <C extends Comparable<C>> boolean isPoint(Range<C> range) {
return range.hasLowerBound()
&& range.hasUpperBound()
- && range.lowerEndpoint().equals(range.upperEndpoint())
+ && range.lowerEndpoint().compareTo(range.upperEndpoint()) == 0
&& !range.isEmpty();
}
@@ -183,6 +183,10 @@ public class RangeSets {
if (range.upperBoundType() == BoundType.OPEN) {
return handler.closedOpen(lower, upper);
} else {
+ // We use .equals rather than .compareTo, because if the endpoints
+ // are not equal the range could not have been created using
+ // Range.singleton. (If they are equal, we cannot distinguish
+ // between closed(v, v) but assume singleton(v).)
if (lower.equals(upper)) {
return handler.singleton(lower);
} else {
@@ -249,6 +253,10 @@ public class RangeSets {
if (range.upperBoundType() == BoundType.OPEN) {
consumer.closedOpen(lower, upper);
} else {
+ // We use .equals rather than .compareTo, because if the endpoints
+ // are not equal the range could not have been created using
+ // Range.singleton. (If they are equal, we cannot distinguish
+ // between closed(v, v) but assume singleton(v).)
if (lower.equals(upper)) {
consumer.singleton(lower);
} else {
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 e7663ba51f..1a356e75a9 100644
--- a/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java
+++ b/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java
@@ -1713,6 +1713,60 @@ class RexProgramTest extends RexProgramTestBase {
.expandedSearch(expanded);
}
+ @Test void testSimplifyRange8() {
+ final RexNode aRef = input(tInt(true), 0);
+ // a not in (1.0, 2.0)
+ RexNode expr =
+ not(
+ in(aRef,
+ literal(new BigDecimal("1.0")),
+ literal(new BigDecimal("2.0"))));
+ final String simplified = "SEARCH($0, "
+ + "Sarg[(-\u221e..1.0:DECIMAL(2, 1)),"
+ + " (1.0:DECIMAL(2, 1)..2.0:DECIMAL(2, 1)),"
+ + " (2.0:DECIMAL(2, 1)..+\u221e)]:DECIMAL(2, 1))";
+ checkSimplify(expr, simplified);
+
+ // The following identical to previous, and simplifies to the same:
+ // a < 1.0 or (1.0 < a and a < 2.0) or 2.0 < a
+ RexNode expr2 =
+ or(lt(aRef, literal(new BigDecimal("1.0"))),
+ and(lt(literal(new BigDecimal("1.0")), aRef),
+ lt(aRef, literal(new BigDecimal("2.0")))),
+ lt(literal(new BigDecimal("2.0")), aRef));
+ checkSimplify(expr2, simplified);
+
+ // Identical to previous, except one "2.0" is "2":
+ // a < 1.0 or (1.0 < a and a < 2.0) or 2.00 < a
+ RexNode expr3 =
+ or(lt(aRef, literal(new BigDecimal("1.0"))),
+ and(lt(literal(new BigDecimal("1.0")), aRef),
+ lt(aRef, literal(new BigDecimal("2.0")))),
+ lt(literal(new BigDecimal("2")), aRef));
+ final String simplified3 = "SEARCH($0, "
+ + "Sarg[(-\u221e..1.0:DECIMAL(11, 1)),"
+ + " (1.0:DECIMAL(11, 1)..2.0:DECIMAL(11, 1)),"
+ + " (2:DECIMAL(11, 1)..+\u221e)]:DECIMAL(11, 1))";
+ checkSimplify(expr3, simplified3);
+ }
+
+ @Test void testSimplifyRange9() {
+ // Contiguous ranges can be merged:
+ // a > 1 and <= 2 or a > 2.0 and a < 3
+ // becomes
+ // a > 1 and a < 3
+ final RexNode aRef = input(tInt(true), 0);
+ RexNode expr =
+ or(
+ and(gt(aRef, literal(1)),
+ le(aRef, literal(2))),
+ and(gt(aRef, literal(new BigDecimal("2.0"))),
+ lt(aRef, literal(3))));
+ final String simplified = "SEARCH($0, "
+ + "Sarg[(1:DECIMAL(11, 1)..3:DECIMAL(11, 1))]:DECIMAL(11, 1))";
+ checkSimplify(expr, simplified);
+ }
+
/** Unit test for
* <a
href="https://issues.apache.org/jira/browse/CALCITE-4352">[CALCITE-4352]
* OR simplification incorrectly loses term</a>. */
diff --git a/core/src/test/java/org/apache/calcite/util/RangeSetTest.java
b/core/src/test/java/org/apache/calcite/util/RangeSetTest.java
index aa619ff3f1..76dab211a4 100644
--- a/core/src/test/java/org/apache/calcite/util/RangeSetTest.java
+++ b/core/src/test/java/org/apache/calcite/util/RangeSetTest.java
@@ -17,6 +17,7 @@
package org.apache.calcite.util;
import org.apache.calcite.linq4j.Ord;
import org.apache.calcite.rel.externalize.RelJson;
+import org.apache.calcite.test.Matchers;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableRangeSet;
@@ -29,16 +30,20 @@ import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
import java.util.function.BiConsumer;
+import java.util.function.Function;
import static org.apache.calcite.test.Matchers.isRangeSet;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.hasToString;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import static java.util.Arrays.asList;
/**
* Unit test for {@link RangeSets} and other utilities relating to Guava
@@ -155,6 +160,118 @@ class RangeSetTest {
assertThat(RangeSets.isPoint(Range.atMost(0)), is(false));
assertThat(RangeSets.isPoint(Range.greaterThan(0)), is(false));
assertThat(RangeSets.isPoint(Range.atLeast(0)), is(false));
+
+ // Test situation where endpoints of closed range are equal under
+ // Comparable.compareTo but not T.equals.
+ final BigDecimal one = new BigDecimal("1");
+ final BigDecimal onePoint = new BigDecimal("1.0");
+ assertThat(RangeSets.isPoint(Range.closed(one, onePoint)), is(true));
+ }
+
+ /** Tests ranges with a data type that implements {@link Comparable}
+ * but is not consistent with {@code equals}.
+ *
+ * <p>Per {@link Comparable}:
+ *
+ * <blockquote>
+ * Virtually all Java core classes that implement Comparable have natural
+ * orderings that are consistent with equals. One exception is
+ * {@link BigDecimal}, whose natural ordering equates {@code BigDecimal}
+ * objects with equal numerical values and different representations
+ * (such as 4.0 and 4.00). For {@link BigDecimal#equals} to return true,
+ * the representation and numerical value of the two {@code BigDecimal}
+ * objects must be the same.
+ * </blockquote>
+ */
+ @Test void testNotConsistentWithEquals() {
+ final BigDecimal one = new BigDecimal("1");
+ final BigDecimal onePoint = new BigDecimal("1.0");
+ final BigDecimal two = BigDecimal.valueOf(2);
+ assertThat(one.equals(onePoint), is(false));
+ assertThat(onePoint.equals(one), is(false));
+ assertThat(one.compareTo(onePoint), is(0));
+ assertThat(onePoint.equals(one), is(false));
+ assertThat(one.equals(BigDecimal.ONE), is(true));
+ assertThat(onePoint.equals(BigDecimal.ONE), is(false));
+ assertThat(RangeSets.isPoint(Range.closed(one, onePoint)), is(true));
+
+ // Ranges (0, 1], [1.0, 2) merges to [0, 2).
+ final Range<BigDecimal> range01 = Range.closed(BigDecimal.ZERO, one);
+ final Range<BigDecimal> range01point = Range.closed(BigDecimal.ZERO,
onePoint);
+ final Range<BigDecimal> range1point2 = Range.closedOpen(onePoint, two);
+ final Range<BigDecimal> range12 = Range.closedOpen(one, two);
+ assertThrows(IllegalArgumentException.class,
+ () -> ImmutableRangeSet.<BigDecimal>builder()
+ .add(range01point)
+ .add(range12)
+ .build());
+
+ assertThat(RangeSets.compare(range01, range12), is(-1));
+ assertThat(RangeSets.compare(range12, range01), is(1));
+ assertThat(RangeSets.compare(range01, range01point), is(0));
+ assertThat(RangeSets.compare(range12, range1point2), is(0));
+
+ // Ranges are merged correctly.
+ final ImmutableRangeSet<BigDecimal> rangeSet =
+ unionRangeSet(asList(range01point, range12));
+ final ImmutableRangeSet<BigDecimal> rangeSet2 =
+ unionRangeSet(asList(range01, range12));
+ final ImmutableRangeSet<BigDecimal> rangeSet3 =
+ unionRangeSet(asList(range01, range1point2));
+ assertThat(rangeSet.asRanges(), hasSize(1));
+ assertThat(rangeSet, is(rangeSet2));
+ assertThat(rangeSet, is(rangeSet3));
+
+ // Check the Consumer mechanism; RangeSets.printer is a Consumer,
+ // and it gives the Consumer mechanism a pretty good workout.
+ final Function<RangeSet<BigDecimal>, String> f =
+ rs -> {
+ final StringBuilder buf = new StringBuilder();
+ final RangeSets.Consumer<BigDecimal> printer =
+ RangeSets.printer(buf, StringBuilder::append);
+ RangeSets.forEach(rs, printer);
+ return Matchers.sanitizeRangeSet(buf.toString());
+ };
+ final Function<Range<BigDecimal>, String> f2 =
+ r -> f.apply(ImmutableRangeSet.of(r));
+ assertThat(f.apply(rangeSet), is("[0..2)"));
+
+ // If a closed range has bounds that are equal, Consumer should treat them
+ // as singletons of the lower bound; but not if the bounds compareTo 0.
+ assertThat(f2.apply(Range.singleton(onePoint)), is("1.0"));
+ assertThat(f2.apply(Range.closed(one, one)), is("1"));
+ assertThat(f2.apply(Range.closed(one, onePoint)), is("[1..1.0]"));
+ assertThat(f2.apply(Range.closed(onePoint, one)), is("[1.0..1]"));
+ assertThat(f2.apply(Range.closed(onePoint, onePoint)), is("1.0"));
+ assertThat(f2.apply(Range.closed(onePoint, two)), is("[1.0..2]"));
+
+ // As for Consumer, now for Handler.
+ // RangeSets.copy tests Handler pretty thoroughly.
+ final Function<RangeSet<BigDecimal>, RangeSet<BigDecimal>> g =
+ rs -> RangeSets.copy(rs, v -> v.multiply(two));
+ final Function<Range<BigDecimal>, RangeSet<BigDecimal>> g2 =
+ r -> g.apply(ImmutableRangeSet.of(r));
+ assertThat(g.apply(rangeSet), isRangeSet("[[0..4)]"));
+
+ assertThat(g2.apply(Range.singleton(onePoint)),
isRangeSet("[[2.0..2.0]]"));
+ assertThat(g2.apply(Range.closed(one, one)), isRangeSet("[[2..2]]"));
+ assertThat(g2.apply(Range.closed(one, onePoint)),
isRangeSet("[[2..2.0]]"));
+ assertThat(g2.apply(Range.closed(onePoint, one)),
isRangeSet("[[2.0..2]]"));
+ assertThat(g2.apply(Range.closed(onePoint, onePoint)),
+ isRangeSet("[[2.0..2.0]]"));
+ assertThat(g2.apply(Range.closed(onePoint, two)),
isRangeSet("[[2.0..4]]"));
+ }
+
+ /** Equivalent to {@link ImmutableRangeSet#unionOf(Iterable)}, which was only
+ * added in Guava 21. */
+ @SuppressWarnings("unchecked")
+ private static <C extends Comparable<C>> ImmutableRangeSet<C> unionRangeSet(
+ List<? extends Range<C>> ranges) {
+ final TreeRangeSet<C> treeRangeSet = TreeRangeSet.create();
+ for (Range<C> range : ranges) {
+ treeRangeSet.add(range);
+ }
+ return ImmutableRangeSet.copyOf(treeRangeSet);
}
/** Tests {@link RangeSets#isOpenInterval(RangeSet)}. */
@@ -420,7 +537,7 @@ class RangeSetTest {
final ImmutableRangeSet<Integer> empty = ImmutableRangeSet.of();
final List<Range<Integer>> ranges =
- Arrays.asList(Range.all(),
+ asList(Range.all(),
Range.atMost(3),
Range.atLeast(4),
Range.lessThan(5),
@@ -442,7 +559,7 @@ class RangeSetTest {
+ "closedOpen(14, 15)";
final List<Range<Integer>> sortedRanges =
- Arrays.asList(
+ asList(
Range.lessThan(3),
Range.atMost(3),
Range.lessThan(5),
@@ -460,7 +577,7 @@ class RangeSetTest {
Range.closedOpen(14, 15));
final List<Range<Integer>> disjointRanges =
- Arrays.asList(Range.lessThan(5),
+ asList(Range.lessThan(5),
Range.greaterThan(16),
Range.singleton(7),
Range.open(8, 9),
diff --git a/testkit/src/main/java/org/apache/calcite/test/Matchers.java
b/testkit/src/main/java/org/apache/calcite/test/Matchers.java
index 03553cecfe..3a3648be1a 100644
--- a/testkit/src/main/java/org/apache/calcite/test/Matchers.java
+++ b/testkit/src/main/java/org/apache/calcite/test/Matchers.java
@@ -287,13 +287,15 @@ public class Matchers {
* <p>This method is necessary because {@link RangeSet#toString()} changed
* behavior. Guava 19 - 28 used a unicode symbol; Guava 29 onwards uses "..".
*/
- @SuppressWarnings("BetaApi")
+ @SuppressWarnings({"BetaApi", "rawtypes"})
public static Matcher<RangeSet> isRangeSet(final String value) {
- return compose(Is.is(value), input -> {
- // Change all '\u2025' (a unicode symbol denoting a range) to '..',
- // consistent with Guava 29+.
- return input.toString().replace("\u2025", "..");
- });
+ return compose(Is.is(value), input -> sanitizeRangeSet(input.toString()));
+ }
+
+ /** Changes all '\u2025' (a unicode symbol denoting a range) to '..',
+ * consistent with Guava 29+. */
+ public static String sanitizeRangeSet(String string) {
+ return string.replace("\u2025", "..");
}
/**