blob: eb659bb65a1df4b7b56f78104a18270abd6a4de1 [file] [log] [blame]
/*
* 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.ignite.internal.sql.engine.planner;
import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rex.RexLiteral;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.sql.type.SqlTypeUtil;
import org.apache.ignite.internal.logger.IgniteLogger;
import org.apache.ignite.internal.logger.Loggers;
import org.apache.ignite.internal.sql.engine.framework.TestBuilders.TableBuilder;
import org.apache.ignite.internal.sql.engine.prepare.bounds.ExactBounds;
import org.apache.ignite.internal.sql.engine.prepare.bounds.MultiBounds;
import org.apache.ignite.internal.sql.engine.prepare.bounds.RangeBounds;
import org.apache.ignite.internal.sql.engine.prepare.bounds.SearchBounds;
import org.apache.ignite.internal.sql.engine.rel.IgniteIndexScan;
import org.apache.ignite.internal.sql.engine.schema.IgniteIndex.Collation;
import org.apache.ignite.internal.sql.engine.schema.IgniteIndex.Type;
import org.apache.ignite.internal.sql.engine.schema.IgniteSchema;
import org.apache.ignite.internal.sql.engine.trait.IgniteDistributions;
import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
import org.apache.ignite.internal.sql.engine.util.RexUtils;
import org.apache.ignite.internal.type.NativeTypes;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
/**
* Index bounds check tests.
*/
public class IndexSearchBoundsPlannerTest extends AbstractPlannerTest {
private static final IgniteLogger LOG = Loggers.forClass(IndexSearchBoundsPlannerTest.class);
private static List<String> NODES = new ArrayList<>(4);
private IgniteSchema publicSchema;
@BeforeAll
public static void init() {
IntStream.rangeClosed(0, 3).forEach(i -> NODES.add(UUID.randomUUID().toString()));
}
@BeforeEach
public void beforeEach() {
publicSchema = createSchemaFrom(tableA("TEST"));
}
/** Simple case on one field, without multi tuple SEARCH/SARG. */
@Test
public void testBoundsOneFieldSingleTuple() throws Exception {
assertBounds("SELECT * FROM TEST WHERE C1 = 1", exact(1));
assertBounds("SELECT * FROM TEST WHERE C1 = 1 AND C3 = 1", exact(1), empty(), empty());
assertBounds("SELECT * FROM TEST WHERE C1 > 1 AND C1 <= 3",
range(1, 3, false, true));
assertBounds("SELECT * FROM TEST WHERE C1 < 3 AND C1 IS NOT NULL",
range(null, 3, true, false));
// Redundant "IS NOT NULL condition".
assertBounds("SELECT * FROM TEST WHERE C1 > 3 AND C1 IS NOT NULL",
range(3, "null", false, false));
// C4 field not in collation.
assertBounds("SELECT * FROM TEST WHERE C1 > 1 AND C1 <= 3 AND C4 = 1",
range(1, 3, false, true),
empty(),
empty()
);
// Cannot proceed to C3 without C2.
assertBounds("SELECT * FROM TEST WHERE C1 = 1 AND C3 = 1",
exact(1),
empty(),
empty()
);
assertBounds("SELECT * FROM TEST WHERE C1 > 1 AND C1 <= 3 AND C3 = 1",
range(1, 3, false, true),
empty(),
empty()
);
}
/** Simple SEARCH/SARG. */
@Test
public void testBoundsOneFieldSearch() throws Exception {
assertBounds("SELECT * FROM TEST WHERE C1 IN (1, 2, 3)",
multi(exact(1), exact(2), exact(3)));
assertBounds("SELECT * FROM TEST WHERE C1 IN (1, 2, 3) AND C1 IS NOT NULL",
multi(exact(1), exact(2), exact(3)));
assertBounds("SELECT * FROM TEST WHERE (C1 > 1 AND C1 < 3) OR C1 IN (4, 5) OR C1 = 6 OR C1 > 7",
multi(
range(1, 3, false, false),
exact(4),
exact(5),
exact(6),
range(7, "null", false, false)));
}
/** Simple SEARCH/SARG, values deduplication. */
@Test
public void testBoundsOneFieldSearchDeduplication() throws Exception {
assertBounds("SELECT * FROM TEST WHERE C1 IN (1, 2, 3, 2, 1)",
multi(exact(1), exact(2), exact(3)));
}
/** Simple SEARCH/SARG, range optimization. */
@Test
public void testBoundsOneFieldSearchRangeOptimization() throws Exception {
assertBounds("SELECT * FROM TEST WHERE (C1 > 1 AND C1 < 4) OR (C1 > 3 AND C1 < 5) OR (C1 > 7) OR (C1 > 6)",
multi(
range(1, 5, false, false),
range(6, "null", false, false)));
assertBounds("SELECT * FROM TEST WHERE C1 > 1 AND C1 < 3 AND C1 <> 2",
multi(
range(1, 2, false, false),
range(2, 3, false, false)));
assertBounds("SELECT * FROM TEST WHERE C1 = 1 AND "
+ "((C2 > '1' AND C2 < '3') OR (C2 > '11' AND C2 < '33') OR C2 > '4')",
exact(1),
multi(
range("1", "33", false, false),
range("4", "null", false, false)));
assertBounds("SELECT * FROM TEST WHERE C1 = 1 AND (C2 > '1' OR C2 < '3')",
exact(1));
}
/** Simple SEARCH/SARG with "IS NULL" condition. */
@Test
public void testBoundsOneFieldSearchWithNull() throws Exception {
assertBounds("SELECT * FROM TEST WHERE C1 IN (1, 2, 3) OR C1 IS NULL",
multi(exact("null"), exact(1), exact(2), exact(3)),
empty(),
empty()
);
}
/** Tests bounds with DESC ordering. */
@Test
public void testBoundsDescOrdering() throws Exception {
publicSchema = createSchemaFrom(tableA("TEST")
.andThen(t -> t.sortedIndex()
.name("C4")
.addColumn("C4", Collation.DESC_NULLS_LAST)
.addColumn("C3", Collation.ASC_NULLS_FIRST)
.end())
);
assertBounds("SELECT * FROM TEST WHERE C4 > 1",
range(null, 1, true, false));
assertBounds("SELECT * FROM TEST WHERE C4 < 1",
range(1, "null", false, false));
assertBounds("SELECT * FROM TEST WHERE C4 IS NULL", exact("null"));
assertBounds("SELECT /*+ FORCE_INDEX(c4) */ * FROM TEST WHERE C4 IS NOT NULL",
range(null, "null", true, false));
assertBounds("SELECT * FROM TEST WHERE C4 IN (1, 2, 3) AND C3 > 1",
multi(exact(1), exact(2), exact(3)),
range(1, null, false, true)
);
assertBounds("SELECT * FROM TEST WHERE ((C4 > 1 AND C4 < 5) OR (C4 > 7 AND C4 < 9)) AND C3 = 1",
multi(
range(5, 1, false, false),
range(9, 7, false, false))
);
}
/** Tests bounds with conditions on several fields. */
@Test
public void testBoundsSeveralFieldsSearch() throws Exception {
assertBounds("SELECT * FROM TEST WHERE C1 = 1 AND C2 IN ('a', 'b')",
exact(1),
multi(exact("a"), exact("b"))
);
assertBounds("SELECT * FROM TEST WHERE C1 = 1 AND C2 > 'a'",
exact(1),
range("a", "null", false, false)
);
assertBounds("SELECT * FROM TEST WHERE C1 IN (1, 2, 3) AND C2 = 'a'",
multi(exact(1), exact(2), exact(3)),
exact("a")
);
assertBounds("SELECT * FROM TEST WHERE C1 IN (1, 2, 3) AND C2 IN ('a', 'b')",
multi(exact(1), exact(2), exact(3)),
multi(exact("a"), exact("b"))
);
assertBounds("SELECT * FROM TEST WHERE C1 IN (1, 2, 3) AND C2 IN ('a', 'b') AND C3 IN (4, 5) AND C4 = 1",
multi(exact(1), exact(2), exact(3)),
multi(exact("a"), exact("b")),
multi(exact(4), exact(5))
);
assertBounds("SELECT * FROM TEST WHERE C1 IN (1, 2, 3) AND C2 IN ('a', 'b') AND C3 > 4",
multi(exact(1), exact(2), exact(3)),
multi(exact("a"), exact("b")),
range(4, "null", false, false)
);
// Cannot proceed to the next field after the range condition.
assertBounds("SELECT * FROM TEST WHERE C1 > 1 AND C2 = 'a'",
range(1, "null", false, false),
empty(),
empty()
);
assertBounds("SELECT * FROM TEST WHERE C1 > 1 AND C2 > 'a'",
range(1, "null", false, false),
empty(),
empty()
);
// TODO https://issues.apache.org/jira/browse/IGNITE-13568 Fix to exact("a")
assertBounds("SELECT * FROM TEST WHERE C1 >= 1 AND C2 = 'a'",
range(1, "null", true, false),
empty()
);
// TODO https://issues.apache.org/jira/browse/IGNITE-13568 Fix to range("a", null, false, true)
assertBounds("SELECT * FROM TEST WHERE C1 >= 1 AND C2 > 'a'",
range(1, "null", true, false),
empty()
);
assertBounds("SELECT * FROM TEST WHERE C1 >= 1 AND C2 < 'a'",
range(1, "null", true, false),
empty()
);
assertBounds("SELECT * FROM TEST WHERE C1 >= 1 AND C2 IN ('a', 'b')",
range(1, "null", true, false),
empty()
);
// Cannot proceed to the next field after SEARCH/SARG with range condition.
assertBounds("SELECT * FROM TEST WHERE ((C1 > 1 AND C1 < 3) OR C1 > 5) AND C2 = 'a'",
multi(
range(1, 3, false, false),
range(5, "null", false, false)),
empty()
);
}
/** Tests max complexity of SEARCH/SARG to include into index scan. */
@Test
public void testBoundsMaxComplexity() throws Exception {
int limit = RexUtils.MAX_SEARCH_BOUNDS_COMPLEXITY;
String inVals = String.join(", ", IntStream.range(0, limit + 1).mapToObj(Integer::toString)
.toArray(String[]::new));
assertPlan("SELECT * FROM TEST WHERE C1 IN (" + inVals + ")", publicSchema, isTableScan("TEST"));
inVals = String.join(", ", IntStream.range(0, limit / 10 + 1).mapToObj(Integer::toString)
.toArray(String[]::new));
assertBounds("SELECT * FROM TEST WHERE C1 IN (1, 2, 3, 4, 5) AND C2 IN ('a', 'b') AND C3 IN (" + inVals + ")",
multi(exact(1), exact(2), exact(3), exact(4), exact(5)),
multi(exact("a"), exact("b")),
empty()
);
}
/** Tests bounds with wrong literal types. */
@Test
public void testBoundsTypeConversion() throws Exception {
// Implicit cast of all filter values to INTEGER.
assertBounds("SELECT * FROM TEST WHERE C1 IN ('1', '2', '3')",
multi(exact(1), exact(2), exact(3))
);
// Implicit cast of '1' to INTEGER.
assertBounds("SELECT * FROM TEST WHERE C1 IN ('1', 2, 3)",
multi(exact(1), exact(2), exact(3))
);
// Casted to INTEGER type C2 column cannot be used as index bound.
assertBounds("SELECT * FROM TEST WHERE C1 = 1 AND C2 > '1'",
exact(1),
range('1', "null", false, false)
);
// Casted to INTEGER type C2 column cannot be used as index bound.
assertBounds("SELECT * FROM TEST WHERE C1 = 1 AND C2 IN (2, 3)",
exact(1),
empty()
);
// Casted to INTEGER type C2 column cannot be used as index bound.
assertBounds("SELECT * FROM TEST WHERE CAST(CAST(C1 AS VARCHAR) AS INTEGER) = 1 AND C2 IN (2, 3)",
exact(1),
empty()
);
// Implicit cast of 2 to VARCHAR.
assertBounds("SELECT * FROM TEST WHERE C1 = 1 AND C2 IN (2, '3')",
exact(1),
multi(exact("2"), exact("3"))
);
}
/** Tests bounds with dynamic parameters. */
@Test
public void testBoundsDynamicParams() throws Exception {
assertBounds("SELECT * FROM TEST WHERE C1 IN (?, ?)",
multi(exact("?0"), exact("?1")));
assertBounds("SELECT * FROM TEST WHERE C1 = ? AND C2 IN ('a', 'b')", List.of(1), publicSchema,
exact("?0"),
multi(exact("a"), exact("b"))
);
assertBounds("SELECT * FROM TEST WHERE C1 = ? AND C2 > ? AND C2 < ?", List.of(1, 'a', 'w'), publicSchema,
exact("?0"),
range("?1", "?2", false, false)
);
}
/** Tests bounds with correlated value. */
@Test
public void testBoundsWithCorrelate() throws Exception {
assertBounds("SELECT (SELECT C1 FROM TEST t2 WHERE t2.C1 = t1.C1) FROM TEST t1",
exact("$cor0.C1")
);
assertBounds(
"SELECT (SELECT C1 FROM TEST t2 WHERE C1 = 1 AND C2 = 'a' AND C3 IN (t1.C3, 0, 1, 2)) FROM TEST t1",
exact(1),
exact("a"),
empty()
);
}
/** Tests bounds merge. */
@Test
public void testBoundsMerge() throws Exception {
IgniteSchema publicSchema = createSchemaFrom(tableA("TEST")
.andThen(t -> t.sortedIndex()
.name("C4")
.addColumn("C4", Collation.DESC_NULLS_LAST)
.addColumn("C3", Collation.ASC_NULLS_FIRST)
.end()
));
assertBounds("SELECT * FROM TEST WHERE C1 > ? AND C1 >= 1", List.of(10), publicSchema,
range("$GREATEST2(?0, 1)", "null", true, false)
);
assertBounds("SELECT * FROM TEST WHERE C1 > ? AND C1 >= ? AND C1 > ?", List.of(10, 10, 10), publicSchema,
range("$GREATEST2($GREATEST2(?0, ?1), ?2)", "null", true, false)
);
assertBounds("SELECT * FROM TEST WHERE C1 > ? AND C1 >= 1 AND C1 < ? AND C1 < ?", List.of(10, 10, 10), publicSchema,
range("$GREATEST2(?0, 1)", "$LEAST2(?1, ?2)", true, false)
);
assertBounds("SELECT * FROM TEST WHERE C1 < ? AND C1 BETWEEN 1 AND 10 ", List.of(10), publicSchema,
range(1, "$LEAST2(?0, 10)", true, true)
);
assertBounds("SELECT * FROM TEST WHERE C1 NOT IN (1, 2) AND C1 >= ?", List.of(10), publicSchema,
range("?0", "null", true, false)
);
assertBounds("SELECT * FROM TEST WHERE C4 > ? AND C4 >= 1 AND C4 < ? AND C4 < ?", List.of(10, 10, 10), publicSchema,
range("$LEAST2(?1, ?2)", "$GREATEST2(?0, 1)", false, true)
);
}
/** Tests complex bounds expressions. */
@Test
public void testBoundsComplex() throws Exception {
assertBounds("SELECT * FROM TEST WHERE C1 = ? + 10", List.of(1), publicSchema,
exact("+(?0, 10)")
);
assertBounds("SELECT * FROM TEST WHERE C1 = 1 AND C2 > SUBSTRING(?::VARCHAR, 1, 2) || '3'", List.of("1"), publicSchema,
exact(1),
range("||(SUBSTRING(?0, 1, 2), _UTF-8'3')", "null", false, false)
);
assertBounds("SELECT * FROM TEST WHERE C1 = 1 AND C2 > SUBSTRING(C3::VARCHAR, 1, 2) || '3'",
exact(1),
empty()
);
assertBounds("SELECT (SELECT C1 FROM TEST t2 WHERE t2.C1 = t1.C1 + t1.C3 * ?) FROM TEST t1", List.of(1), publicSchema,
exact("+($cor0.C1, *($cor0.C3, ?0))")
);
assertPlan("SELECT * FROM TEST WHERE C1 = ? + C3", publicSchema, isTableScan("TEST"), List.of(1));
assertPlan("SELECT (SELECT C1 FROM TEST t2 WHERE t2.C1 < t1.C1 + t2.C1) FROM TEST t1", publicSchema,
nodeOrAnyChild(isIndexScan("TEST", "C1C2C3")).negate());
// Here we have two OR sets in CNF, second set can't be used, since it contains condition on C1 and C2 columns,
// so use only first OR set as bounds.
assertBounds("SELECT * FROM TEST WHERE C1 in (?, 1, 2) or (C1 = ? and C2 > 'asd')",
multi(exact("?0"), exact(1), exact(2), exact("?1"))
);
assertBounds("SELECT * FROM TEST WHERE C1 in (?, ? + 1, ? * 2)",
multi(exact("?0"), exact("+(?1, 1)"), exact("*(?2, 2)"))
);
// Don't support expanding OR with correlate to bounds.
assertPlan("SELECT (SELECT C1 FROM TEST t2 WHERE C1 in (t1.C1, 1, ?)) FROM TEST t1", publicSchema,
nodeOrAnyChild(isIndexScan("TEST", "C1C2C3")).negate());
// Here "BETWEEN" generates AND condition, and we have two OR sets in CNF, so we can't correctly use range
// with both upper and lower bounds. So, we use only first OR set as bounds.
assertBounds("SELECT * FROM TEST WHERE C1 in (?, 1, 2) or C1 between ? and ?",
multi(exact("?0"), exact(1), exact(2), range("?1", "null", true, false))
);
// Check equality condition priority over SEARCH/SARG.
assertBounds("SELECT * FROM TEST WHERE (C1 BETWEEN 1 AND 10 OR C1 IN (20, 30)) AND C1 = ?",
exact("?0")
);
}
/**
* Index bound checks - search key lies out of value range.
*/
@ParameterizedTest
@MethodSource("boundsTypeLimits")
public void testBoundsTypeLimits(RelDataType type, Object value, Predicate<SearchBounds> bounds) throws Exception {
IgniteSchema schema = createSchemaFrom(
tableB("TEST2", "C2", type).andThen(addSortIndex("C2")));
assertBounds("SELECT * FROM test2 WHERE C2 = " + value, List.of(), schema, bounds);
}
@ParameterizedTest
@MethodSource("indexTypeAndNumericsInBounds")
public void testCorrectNumericIndexInBounds(
Type indexType,
RelDataType columnType,
String valueExpr,
String boundExpr,
RelDataType boundExprType) throws Exception {
UnaryOperator<TableBuilder> tableB = tableB("TEST2", "C2", columnType);
IgniteSchema schema;
if (indexType == Type.HASH) {
schema = createSchemaFrom(tableB.andThen(addHashIndex("C2")));
} else {
schema = createSchemaFrom(tableB.andThen(addSortIndex("C2")));
}
assertBounds("SELECT * FROM test2 WHERE C2 = " + valueExpr, List.of(), schema, searchBounds -> {
if (!(searchBounds instanceof ExactBounds)) {
log.info("SearchBound type does not match. Expected {} but got {}", ExactBounds.class, searchBounds);
return false;
}
ExactBounds exactBounds = (ExactBounds) searchBounds;
RexNode rexNode = exactBounds.bound();
if (!SqlTypeUtil.equalSansNullability(boundExprType, rexNode.getType())) {
log.info("Bound type does not match. Expected {} but got {}", boundExprType, rexNode.getType());
return false;
}
if (!boundExpr.equals(rexNode.toString())) {
log.info("Bound expr does not match. Expected {} but got {}", boundExpr, rexNode);
return false;
}
return true;
});
}
private static Stream<Arguments> indexTypeAndNumericsInBounds() {
Stream<Arguments> result = Stream.of();
for (Type type : Type.values()) {
Stream<Arguments> idx = numericsInBounds().map(a -> {
Object[] v = a.get();
Object[] newArgs = new Object[v.length + 1];
newArgs[0] = type;
System.arraycopy(v, 0, newArgs, 1, v.length);
return arguments(newArgs);
});
result = Stream.concat(result, idx);
}
return result;
}
private static Stream<Arguments> numericsInBounds() {
return Stream.of(
// Column type, expr to use in search condition, expected expression in search bounds as RexNode::toString, its type.
arguments(sqlType(SqlTypeName.TINYINT), "42", "42:TINYINT", sqlType(SqlTypeName.TINYINT)),
arguments(sqlType(SqlTypeName.TINYINT), "CAST(42 AS TINYINT)", "42:TINYINT", sqlType(SqlTypeName.TINYINT)),
arguments(sqlType(SqlTypeName.TINYINT), "CAST(42 AS SMALLINT)", "42:TINYINT", sqlType(SqlTypeName.TINYINT)),
arguments(sqlType(SqlTypeName.TINYINT), "CAST(42 AS INTEGER)", "42:TINYINT", sqlType(SqlTypeName.TINYINT)),
arguments(sqlType(SqlTypeName.TINYINT), "CAST(42 AS BIGINT)", "42:TINYINT", sqlType(SqlTypeName.TINYINT)),
arguments(sqlType(SqlTypeName.SMALLINT), "42", "42:SMALLINT", sqlType(SqlTypeName.SMALLINT)),
arguments(sqlType(SqlTypeName.SMALLINT), "CAST(42 AS TINYINT)", "42:SMALLINT", sqlType(SqlTypeName.SMALLINT)),
arguments(sqlType(SqlTypeName.SMALLINT), "CAST(42 AS SMALLINT)", "42:SMALLINT", sqlType(SqlTypeName.SMALLINT)),
arguments(sqlType(SqlTypeName.SMALLINT), "CAST(42 AS INTEGER)", "42:SMALLINT", sqlType(SqlTypeName.SMALLINT)),
arguments(sqlType(SqlTypeName.SMALLINT), "CAST(42 AS BIGINT)", "42:SMALLINT", sqlType(SqlTypeName.SMALLINT)),
arguments(sqlType(SqlTypeName.INTEGER), "42", "42", sqlType(SqlTypeName.INTEGER)),
arguments(sqlType(SqlTypeName.INTEGER), "CAST(42 AS TINYINT)", "42", sqlType(SqlTypeName.INTEGER)),
arguments(sqlType(SqlTypeName.INTEGER), "CAST(42 AS SMALLINT)", "42", sqlType(SqlTypeName.INTEGER)),
arguments(sqlType(SqlTypeName.INTEGER), "CAST(42 AS INTEGER)", "42", sqlType(SqlTypeName.INTEGER)),
arguments(sqlType(SqlTypeName.INTEGER), "CAST(42 AS BIGINT)", "42", sqlType(SqlTypeName.INTEGER)),
arguments(sqlType(SqlTypeName.BIGINT), "42", "42:BIGINT", sqlType(SqlTypeName.BIGINT)),
arguments(sqlType(SqlTypeName.BIGINT), "CAST(42 AS TINYINT)", "42:BIGINT", sqlType(SqlTypeName.BIGINT)),
arguments(sqlType(SqlTypeName.BIGINT), "CAST(42 AS SMALLINT)", "42:BIGINT", sqlType(SqlTypeName.BIGINT)),
arguments(sqlType(SqlTypeName.BIGINT), "CAST(42 AS INTEGER)", "42:BIGINT", sqlType(SqlTypeName.BIGINT)),
arguments(sqlType(SqlTypeName.BIGINT), "CAST(42 AS BIGINT)", "42:BIGINT", sqlType(SqlTypeName.BIGINT)),
arguments(sqlType(SqlTypeName.REAL), "42", "42:REAL", sqlType(SqlTypeName.REAL)),
arguments(sqlType(SqlTypeName.DOUBLE), "42", "42:DOUBLE", sqlType(SqlTypeName.DOUBLE))
// TODO https://issues.apache.org/jira/browse/IGNITE-19881 uncomment after this issue is fixed
// The optimizer selects TableScan instead of a IndexScan (Real/double columns)
// arguments(sqlType(SqlTypeName.REAL), "CAST(42 AS DOUBLE)", "42:REAL", sqlType(SqlTypeName.REAL)),
// arguments(sqlType(SqlTypeName.DOUBLE), "CAST(42 AS REAL)", "42:DOUBLE", sqlType(SqlTypeName.DOUBLE)),
// TODO https://issues.apache.org/jira/browse/IGNITE-19882 uncomment after this issue is fixed
// The optimizer selects TableScan instead of a IndexScan (Decimal columns)
// arguments(sqlType(SqlTypeName.DECIMAL, 5), "42", "42:DECIMAL(10, 0)", sqlType(SqlTypeName.DECIMAL, 5, 0)),
// arguments(sqlType(SqlTypeName.DECIMAL, 10, 2), "42", "42:DECIMAL(10, 2)", sqlType(SqlTypeName.DECIMAL, 10, 2)),
// arguments(sqlType(SqlTypeName.INTEGER), "CAST(42 AS DECIMAL(10))", "42", sqlType(SqlTypeName.INTEGER)),
);
}
private static Stream<Arguments> boundsTypeLimits() {
RelDataType tinyintType = sqlType(SqlTypeName.TINYINT);
byte[] tinyIntTypeLimits = {Byte.MIN_VALUE, Byte.MAX_VALUE};
List<Arguments> tinyInts = List.of(
arguments(tinyintType, -129, exact(tinyIntTypeLimits[0])),
arguments(tinyintType, -128, exact(tinyIntTypeLimits[0])),
arguments(tinyintType, 127, exact(tinyIntTypeLimits[1])),
arguments(tinyintType, 128, exact(tinyIntTypeLimits[1]))
);
RelDataType smallIntType = sqlType(SqlTypeName.SMALLINT);
short[] smallIntLimits = {Short.MIN_VALUE, Short.MAX_VALUE};
List<Arguments> smallInts = List.of(
arguments(smallIntType, (-(int) Math.pow(2, 15) - 1), exact(smallIntLimits[0])),
arguments(smallIntType, (-(int) Math.pow(2, 15)), exact(smallIntLimits[0])),
arguments(smallIntType, ((int) Math.pow(2, 15)), exact(smallIntLimits[1])),
arguments(smallIntType, ((int) Math.pow(2, 15) + 1), exact(smallIntLimits[1]))
);
RelDataType intType = sqlType(SqlTypeName.INTEGER);
int[] intLimits = {Integer.MIN_VALUE, Integer.MAX_VALUE};
List<Arguments> ints = List.of(
arguments(intType, (-(long) Math.pow(2, 31) - 1), exact(intLimits[0])),
arguments(intType, (-(long) Math.pow(2, 31)), exact(intLimits[0])),
arguments(intType, ((long) Math.pow(2, 31)), exact(intLimits[1])),
arguments(intType, ((long) Math.pow(2, 31) + 1), exact(intLimits[1]))
);
RelDataType bigIntType = sqlType(SqlTypeName.BIGINT);
BigDecimal[] bigIntTypeLimits = {BigDecimal.valueOf(Long.MIN_VALUE), BigDecimal.valueOf(Long.MAX_VALUE)};
List<Arguments> bigints = List.of(
arguments(bigIntType, BigInteger.TWO.pow(63).negate(), exact(bigIntTypeLimits[0]))
);
RelDataType decimal3Type = sqlType(SqlTypeName.DECIMAL, 3);
BigDecimal[] decimal3TypeLimits = {BigDecimal.valueOf(-999), BigDecimal.valueOf(999)};
List<Arguments> decimal3s = List.of(
arguments(decimal3Type, "(-1000)::DECIMAL(3)", exact(decimal3TypeLimits[0])),
arguments(decimal3Type, "(-999)::DECIMAL(3)", exact(decimal3TypeLimits[0])),
arguments(decimal3Type, "999::DECIMAL(3)", exact(decimal3TypeLimits[1])),
arguments(decimal3Type, "1000::DECIMAL(3)", exact(decimal3TypeLimits[1]))
);
RelDataType decimal53Type = sqlType(SqlTypeName.DECIMAL, 5, 3);
BigDecimal[] decimal53TypeLimits = {new BigDecimal("-99.999"), new BigDecimal("99.999")};
List<Arguments> decimal35s = List.of(
arguments(decimal53Type, "(-100.000)::DECIMAL(5, 3)", exact(decimal53TypeLimits[0])),
arguments(decimal53Type, "(100.000)::DECIMAL(5, 3)", exact(decimal53TypeLimits[1]))
);
// TODO https://issues.apache.org/jira/browse/IGNITE-19858
// Cause serialization/deserialization mismatch in AbstractPlannerTest::checkSplitAndSerialization
//
// RelDataType realType = TYPE_FACTORY.createSqlType(SqlTypeName.REAL);
// List<Arguments> reals = List.of(
// arguments(realType, BigDecimal.valueOf(Float.MAX_VALUE).add(BigDecimal.ONE) + "::REAL", exact(Float.MAX_VALUE)),
// arguments(realType, BigDecimal.valueOf(Double.MAX_VALUE).add(BigDecimal.ONE), exact(Double.MAX_VALUE))
// );
return Stream.of(
tinyInts,
smallInts,
ints,
bigints,
decimal3s,
decimal35s
).flatMap(Collection::stream);
}
private static Predicate<SearchBounds> exact(Object val) {
Predicate<SearchBounds> p = b -> b instanceof ExactBounds && matchValue(val, ((ExactBounds) b).bound());
return named(p, format("={}", val));
}
private static Predicate<SearchBounds> multi(Predicate<SearchBounds>... predicates) {
Predicate<SearchBounds> p = b -> b instanceof MultiBounds
&& ((MultiBounds) b).bounds().size() == predicates.length
&& matchBounds(((MultiBounds) b).bounds(), predicates);
return named(p, Arrays.toString(predicates));
}
private void assertBounds(String sql, Predicate<SearchBounds>... predicates) throws Exception {
assertPlan(sql, publicSchema, nodeOrAnyChild(isInstanceOf(IgniteIndexScan.class)
.and(scan -> matchBounds(scan.searchBounds(), predicates))), List.of());
}
private void assertBounds(
String sql,
List<Object> params,
IgniteSchema schema,
Predicate<SearchBounds>... predicates
) throws Exception {
assertPlan(sql, schema, nodeOrAnyChild(isInstanceOf(IgniteIndexScan.class)
.and(scan -> matchBounds(scan.searchBounds(), predicates))), params);
}
private static boolean matchBounds(List<SearchBounds> searchBounds, Predicate<SearchBounds>... predicates) {
for (int i = 0; i < predicates.length; i++) {
if (!predicates[i].test(searchBounds.get(i))) {
LOG.info("{} bounds do not not match: {}", searchBounds.get(i), predicates[i]);
return false;
}
}
return true;
}
private static Predicate<SearchBounds> range(
@Nullable Object lower,
@Nullable Object upper,
boolean lowerInclude,
boolean upperInclude
) {
Predicate<SearchBounds> range = b -> b instanceof RangeBounds
&& matchValue(lower, ((RangeBounds) b).lowerBound())
&& matchValue(upper, ((RangeBounds) b).upperBound())
&& lowerInclude == ((RangeBounds) b).lowerInclude()
&& upperInclude == ((RangeBounds) b).upperInclude();
String lc = lowerInclude ? "[" : "(";
String uc = upperInclude ? "]" : ")";
return named(range, format("{}{}, {}{}", lc, lower, upper, uc));
}
private static RelDataType sqlType(SqlTypeName typeName) {
return TYPE_FACTORY.createSqlType(typeName);
}
private static RelDataType sqlType(SqlTypeName typeName, int precision) {
return TYPE_FACTORY.createSqlType(typeName, precision);
}
private static RelDataType sqlType(SqlTypeName typeName, int precision, int scale) {
return TYPE_FACTORY.createSqlType(typeName, precision, scale);
}
private static boolean matchValue(@Nullable Object val, RexNode bound) {
if (val == null || bound == null) {
return val == bound;
}
String actual = Objects.toString(bound instanceof RexLiteral ? ((RexLiteral) bound).getValueAs(val.getClass()) : bound);
String expected = Objects.toString(val);
return expected.equals(actual);
}
private static Predicate<SearchBounds> named(Predicate<SearchBounds> p, String name) {
return new Predicate<>() {
@Override
public boolean test(SearchBounds bounds) {
return p.test(bounds);
}
@Override
public String toString() {
return name;
}
};
}
private static UnaryOperator<TableBuilder> tableA(String tableName) {
return tableBuilder -> tableBuilder
.name(tableName)
.name("TEST")
.addColumn("C1", NativeTypes.INT32)
.addColumn("C2", NativeTypes.STRING)
.addColumn("C3", NativeTypes.INT32)
.addColumn("C4", NativeTypes.INT32)
.addColumn("C5", NativeTypes.INT8)
.distribution(IgniteDistributions.single())
.size(100)
.sortedIndex()
.name("C1C2C3")
.addColumn("C1", Collation.ASC_NULLS_LAST)
.addColumn("C2", Collation.ASC_NULLS_LAST)
.addColumn("C3", Collation.ASC_NULLS_LAST)
.end();
}
private static UnaryOperator<TableBuilder> tableB(String tableName, String column, RelDataType type) {
return tableBuilder -> tableBuilder
.name(tableName)
.addColumn("C1", NativeTypes.INT32)
.addColumn(column, IgniteTypeFactory.relDataTypeToNative(type))
.size(400)
.distribution(IgniteDistributions.single());
}
}