blob: 853354d341419bbb3f743f3e67d8ba34519c9b2c [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 java.lang.String.format;
import static org.apache.calcite.sql.type.SqlTypeName.ALL_TYPES;
import static org.apache.calcite.sql.type.SqlTypeName.BINARY_TYPES;
import static org.apache.calcite.sql.type.SqlTypeName.BOOLEAN_TYPES;
import static org.apache.calcite.sql.type.SqlTypeName.CHAR_TYPES;
import static org.apache.calcite.sql.type.SqlTypeName.DATETIME_TYPES;
import static org.apache.calcite.sql.type.SqlTypeName.EXACT_TYPES;
import static org.apache.calcite.sql.type.SqlTypeName.FRACTIONAL_TYPES;
import static org.apache.calcite.sql.type.SqlTypeName.INTERVAL_DAY;
import static org.apache.calcite.sql.type.SqlTypeName.INTERVAL_DAY_MINUTE;
import static org.apache.calcite.sql.type.SqlTypeName.INTERVAL_DAY_SECOND;
import static org.apache.calcite.sql.type.SqlTypeName.INTERVAL_HOUR;
import static org.apache.calcite.sql.type.SqlTypeName.INTERVAL_HOUR_MINUTE;
import static org.apache.calcite.sql.type.SqlTypeName.INTERVAL_HOUR_SECOND;
import static org.apache.calcite.sql.type.SqlTypeName.INTERVAL_MINUTE;
import static org.apache.calcite.sql.type.SqlTypeName.INTERVAL_MINUTE_SECOND;
import static org.apache.calcite.sql.type.SqlTypeName.INTERVAL_MONTH;
import static org.apache.calcite.sql.type.SqlTypeName.INTERVAL_SECOND;
import static org.apache.calcite.sql.type.SqlTypeName.INTERVAL_YEAR;
import static org.apache.calcite.sql.type.SqlTypeName.INTERVAL_YEAR_MONTH;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.calcite.sql.type.SqlTypeCoercionRule;
import org.apache.calcite.sql.type.SqlTypeMappingRule;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.ignite.internal.sql.engine.type.UuidType;
import org.apache.ignite.internal.sql.engine.util.IgniteCustomAssignmentsRules;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
/** Test CAST type to type possibilities. */
public class CastResolutionTest extends AbstractPlannerTest {
private static final String CAST_ERROR_MESSAGE = "Cast function cannot convert value of type";
private static final Set<String> NUMERIC_NAMES = new HashSet<>();
private static final Set<String> FRACTIONAL_NAMES = FRACTIONAL_TYPES.stream().map(SqlTypeName::getName).collect(Collectors.toSet());
private static final Set<String> EXACT_NUMERIC = EXACT_TYPES.stream().map(SqlTypeName::getName).collect(Collectors.toSet());
private static final Set<String> CHAR_NAMES = CHAR_TYPES.stream().map(SqlTypeName::getName).collect(Collectors.toSet());
private static final Set<String> BINARY_NAMES = BINARY_TYPES.stream().map(SqlTypeName::getName).collect(Collectors.toSet());
private static final Set<String> DT_NAMES = DATETIME_TYPES.stream().map(SqlTypeName::getName).collect(Collectors.toSet());
private static final Set<String> YM_INTERVAL = Set.of(INTERVAL_YEAR.getName(), INTERVAL_MONTH.getName());
private static final Set<String> DAY_INTERVAL = Set.of(INTERVAL_HOUR.getName(), INTERVAL_MINUTE.getName(), INTERVAL_SECOND.getName());
private static final Set<String> ALL_BESIDES_BINARY_NAMES =
BOOLEAN_TYPES.stream().map(SqlTypeName::getName).collect(Collectors.toSet());
private static final Set<String> CHAR_NUMERIC_AND_INTERVAL_NAMES = new HashSet<>();
private static final Set<String> CHAR_AND_NUMERIC_NAMES = new HashSet<>();
private static final Set<String> CHAR_AND_TS = new HashSet<>();
private static final Set<String> CHAR_AND_DT = new HashSet<>();
private static final Set<String> CHAR_EXACT_AND_YM_INTERVAL = new HashSet<>();
private static final Set<String> CHAR_EXACT_AND_DAY_INTERVAL = new HashSet<>();
private static final String COMMON_TEMPLATE = "SELECT CAST('1'::%s AS %s)";
private static final String INTERVAL_TEMPLATE = "SELECT CAST(%s AS %s)";
private static final String BINARY_TEMPLATE = "SELECT CAST(X'01'::%s AS %s)";
static {
NUMERIC_NAMES.add("NUMERIC");
NUMERIC_NAMES.addAll(EXACT_NUMERIC);
NUMERIC_NAMES.addAll(FRACTIONAL_NAMES);
ALL_BESIDES_BINARY_NAMES.addAll(NUMERIC_NAMES);
ALL_BESIDES_BINARY_NAMES.addAll(FRACTIONAL_NAMES);
ALL_BESIDES_BINARY_NAMES.addAll(CHAR_NAMES);
ALL_BESIDES_BINARY_NAMES.addAll(DT_NAMES);
ALL_BESIDES_BINARY_NAMES.addAll(YM_INTERVAL);
ALL_BESIDES_BINARY_NAMES.addAll(DAY_INTERVAL);
ALL_BESIDES_BINARY_NAMES.add("NUMERIC");
ALL_BESIDES_BINARY_NAMES.add(UuidType.NAME);
CHAR_NUMERIC_AND_INTERVAL_NAMES.addAll(NUMERIC_NAMES);
CHAR_NUMERIC_AND_INTERVAL_NAMES.addAll(CHAR_NAMES);
CHAR_NUMERIC_AND_INTERVAL_NAMES.addAll(YM_INTERVAL);
CHAR_NUMERIC_AND_INTERVAL_NAMES.addAll(DAY_INTERVAL);
CHAR_AND_NUMERIC_NAMES.addAll(NUMERIC_NAMES);
CHAR_AND_NUMERIC_NAMES.addAll(CHAR_NAMES);
CHAR_AND_TS.addAll(CHAR_NAMES);
CHAR_AND_TS.add(SqlTypeName.TIMESTAMP.getName());
CHAR_AND_TS.add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE.getName());
CHAR_AND_DT.addAll(DT_NAMES);
CHAR_AND_DT.addAll(CHAR_NAMES);
CHAR_EXACT_AND_YM_INTERVAL.addAll(List.of(INTERVAL_YEAR.getName(), INTERVAL_MONTH.getName()));
CHAR_EXACT_AND_YM_INTERVAL.addAll(CHAR_NAMES);
CHAR_EXACT_AND_YM_INTERVAL.addAll(EXACT_NUMERIC);
CHAR_EXACT_AND_YM_INTERVAL.add("NUMERIC");
CHAR_EXACT_AND_DAY_INTERVAL.addAll(List.of(INTERVAL_HOUR.getName(), INTERVAL_MINUTE.getName()));
CHAR_EXACT_AND_DAY_INTERVAL.addAll(CHAR_NAMES);
CHAR_EXACT_AND_DAY_INTERVAL.addAll(EXACT_NUMERIC);
CHAR_EXACT_AND_DAY_INTERVAL.add("NUMERIC");
}
/** Test CAST possibility for different supported types. */
@TestFactory
public Stream<DynamicTest> allowedCasts() {
List<DynamicTest> testItems = new ArrayList<>();
Set<String> allTypes = Arrays.stream(CastMatrix.values()).map(v -> v.from).collect(Collectors.toSet());
for (CastMatrix types : CastMatrix.values()) {
String fromInitial = types.from;
Set<String> toTypes = types.toTypes;
String template = template(fromInitial);
String from = makeUsableIntervalFromType(fromInitial);
for (String toType : toTypes) {
// TODO: https://issues.apache.org/jira/browse/IGNITE-21555
if (toType.equals(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE.getName())) {
continue;
}
toType = makeSpaceName(toType);
testItems.add(checkStatement().sql(format(template, from, toType)).ok(false));
}
testItems.add(checkStatement().sql(format(template, from, makeSpaceName(fromInitial))).ok());
testItems.add(checkStatement().sql(format(INTERVAL_TEMPLATE, "NULL", makeSpaceName(fromInitial))).ok());
String finalFrom = from;
Set<String> deprecatedCastTypes = allTypes.stream().filter(t -> !toTypes.contains(t) && !t.equals(finalFrom))
.collect(Collectors.toSet());
for (String toType : deprecatedCastTypes) {
testItems.add(
checkStatement().sql(format(template, from, makeSpaceName(toType))).fails(CAST_ERROR_MESSAGE)
);
}
}
return testItems.stream();
}
/* Check that casts between intervals and exact numerics can involve only intervals with a single datetime field. */
@TestFactory
public Stream<DynamicTest> testMultiIntervals() {
List<DynamicTest> testItems = new ArrayList<>();
testItems.add(checkStatement().sql("SELECT CAST(1 AS interval year to month)").fails(CAST_ERROR_MESSAGE));
testItems.add(checkStatement().sql("SELECT CAST(1 AS interval day to hour)").fails(CAST_ERROR_MESSAGE));
testItems.add(checkStatement().sql("SELECT CAST(1 AS interval day to minute)").fails(CAST_ERROR_MESSAGE));
testItems.add(checkStatement().sql("SELECT CAST(1 AS interval day to second)").fails(CAST_ERROR_MESSAGE));
testItems.add(checkStatement().sql("SELECT CAST(1 AS interval hour to minute)").fails(CAST_ERROR_MESSAGE));
testItems.add(checkStatement().sql("SELECT CAST(1 AS interval hour to second)").fails(CAST_ERROR_MESSAGE));
testItems.add(checkStatement().sql("SELECT CAST('1' AS interval hour to second)").ok());
testItems.add(checkStatement().sql("SELECT CAST('1' AS interval day to hour)").ok());
testItems.add(checkStatement().sql("SELECT CAST('1' AS interval day to minute)").ok());
testItems.add(checkStatement().sql("SELECT CAST('1' AS interval day to second)").ok());
testItems.add(checkStatement().sql("SELECT CAST('1' AS interval hour to minute)").ok());
testItems.add(checkStatement().sql("SELECT CAST('1' AS interval hour to second)").ok());
testItems.add(checkStatement().sql("SELECT CAST('1'::VARCHAR AS interval hour to second)").ok());
testItems.add(checkStatement().sql("SELECT CAST('1'::VARCHAR AS interval day to hour)").ok());
testItems.add(checkStatement().sql("SELECT CAST('1'::VARCHAR AS interval day to minute)").ok());
testItems.add(checkStatement().sql("SELECT CAST('1'::VARCHAR AS interval day to second)").ok());
testItems.add(checkStatement().sql("SELECT CAST('1'::VARCHAR AS interval hour to minute)").ok());
testItems.add(checkStatement().sql("SELECT CAST('1'::VARCHAR AS interval hour to second)").ok());
return testItems.stream();
}
@TestFactory
public Stream<DynamicTest> allowedCastsFromNull() {
List<DynamicTest> testItems = new ArrayList<>();
SqlTypeMappingRule rules = SqlTypeCoercionRule.instance(IgniteCustomAssignmentsRules.instance().getTypeMapping());
for (SqlTypeName type : ALL_TYPES) {
if (type == SqlTypeName.NULL) {
continue;
}
testItems.add(DynamicTest.dynamicTest(format("ALLOW: from: %s to: %s", SqlTypeName.NULL.getName(), type),
() -> assertTrue(rules.canApplyFrom(type, SqlTypeName.NULL))));
}
return testItems.stream();
}
@TestFactory
public Stream<DynamicTest> testRulesContainsIntervalWithExactTypesRules() {
List<DynamicTest> testItems = new ArrayList<>();
List<SqlTypeName> singleIntervals = List.of(INTERVAL_YEAR, INTERVAL_MONTH, INTERVAL_DAY, INTERVAL_HOUR, INTERVAL_MINUTE,
INTERVAL_SECOND);
List<SqlTypeName> nonSingleIntervals = List.of(INTERVAL_YEAR_MONTH, INTERVAL_DAY_MINUTE, INTERVAL_DAY_SECOND,
INTERVAL_HOUR_MINUTE, INTERVAL_HOUR_SECOND, INTERVAL_MINUTE_SECOND);
List<SqlTypeName> singleYearIntervals = List.of(INTERVAL_YEAR, INTERVAL_MONTH);
List<SqlTypeName> singleDayIntervals = List.of(INTERVAL_DAY, INTERVAL_HOUR, INTERVAL_MINUTE,
INTERVAL_SECOND);
SqlTypeMappingRule rules = SqlTypeCoercionRule.instance(IgniteCustomAssignmentsRules.instance().getTypeMapping());
for (SqlTypeName toType : singleIntervals) {
for (SqlTypeName fromType : EXACT_TYPES) {
testItems.add(DynamicTest.dynamicTest(format("ALLOW: from: %s to: %s", fromType, toType),
() -> assertTrue(rules.canApplyFrom(toType, fromType))));
}
}
for (SqlTypeName toType : nonSingleIntervals) {
for (SqlTypeName fromType : EXACT_TYPES) {
testItems.add(DynamicTest.dynamicTest(format("ALLOW: from: %s to: %s", fromType, toType),
() -> assertFalse(rules.canApplyFrom(toType, fromType))));
}
}
for (SqlTypeName toType : singleYearIntervals) {
for (SqlTypeName fromType : singleYearIntervals) {
testItems.add(DynamicTest.dynamicTest(format("ALLOW: from: %s to: %s", fromType, toType),
() -> assertTrue(rules.canApplyFrom(toType, fromType))));
}
}
for (SqlTypeName toType : singleDayIntervals) {
for (SqlTypeName fromType : singleDayIntervals) {
testItems.add(DynamicTest.dynamicTest(format("ALLOW: from: %s to: %s", fromType, toType),
() -> assertTrue(rules.canApplyFrom(toType, fromType))));
}
}
for (SqlTypeName toType : singleYearIntervals) {
for (SqlTypeName fromType : singleDayIntervals) {
testItems.add(DynamicTest.dynamicTest(format("FORBID: from: %s to: %s", fromType, toType),
() -> assertFalse(rules.canApplyFrom(toType, fromType))));
}
}
for (SqlTypeName toType : singleDayIntervals) {
for (SqlTypeName fromType : singleYearIntervals) {
testItems.add(DynamicTest.dynamicTest(format("FORBID: from: %s to: %s", fromType, toType),
() -> assertFalse(rules.canApplyFrom(toType, fromType))));
}
}
return testItems.stream();
}
private static String template(String typeName) {
if (typeName.toLowerCase().contains("interval")) {
return INTERVAL_TEMPLATE;
} else if (typeName.toLowerCase().contains("binary")) {
return BINARY_TEMPLATE;
} else {
return COMMON_TEMPLATE;
}
}
private static String makeSpaceName(String typeName) {
return typeName.replace("_", " ");
}
private static String makeUsableIntervalFromType(String typeName) {
if (typeName.toLowerCase().contains("interval")) {
return typeName.replace("_", " 1 ");
}
return makeSpaceName(typeName);
}
private enum CastMatrix {
BOOLEAN(SqlTypeName.BOOLEAN.getName(), CHAR_NAMES),
INT8(SqlTypeName.TINYINT.getName(), CHAR_NUMERIC_AND_INTERVAL_NAMES),
INT16(SqlTypeName.SMALLINT.getName(), CHAR_NUMERIC_AND_INTERVAL_NAMES),
INT32(SqlTypeName.INTEGER.getName(), CHAR_NUMERIC_AND_INTERVAL_NAMES),
INT64(SqlTypeName.BIGINT.getName(), CHAR_NUMERIC_AND_INTERVAL_NAMES),
DECIMAL(SqlTypeName.DECIMAL.getName(), CHAR_NUMERIC_AND_INTERVAL_NAMES),
REAL(SqlTypeName.REAL.getName(), CHAR_AND_NUMERIC_NAMES),
DOUBLE(SqlTypeName.DOUBLE.getName(), CHAR_AND_NUMERIC_NAMES),
FLOAT(SqlTypeName.FLOAT.getName(), CHAR_AND_NUMERIC_NAMES),
NUMERIC("NUMERIC", CHAR_NUMERIC_AND_INTERVAL_NAMES),
UUID(UuidType.NAME, new HashSet<>(CHAR_NAMES)),
VARCHAR(SqlTypeName.VARCHAR.getName(), ALL_BESIDES_BINARY_NAMES),
CHAR(SqlTypeName.CHAR.getName(), ALL_BESIDES_BINARY_NAMES),
VARBINARY(SqlTypeName.VARBINARY.getName(), BINARY_NAMES),
BINARY(SqlTypeName.BINARY.getName(), BINARY_NAMES),
DATE(SqlTypeName.DATE.getName(), CHAR_AND_TS),
TIME(SqlTypeName.TIME.getName(), CHAR_AND_TS),
// TODO: https://issues.apache.org/jira/browse/IGNITE-21555
// TIME_TZ(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE.getName(), CHAR_AND_DT);
TIMESTAMP(SqlTypeName.TIMESTAMP.getName(), CHAR_AND_DT),
TIMESTAMP_TZ(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE.getName(), CHAR_AND_DT),
INTERVAL_YEAR(SqlTypeName.INTERVAL_YEAR.getName(), CHAR_EXACT_AND_YM_INTERVAL),
INTERVAL_HOUR(SqlTypeName.INTERVAL_HOUR.getName(), CHAR_EXACT_AND_DAY_INTERVAL);
private String from;
private Set<String> toTypes;
CastMatrix(String from, Set<String> toTypes) {
this.from = from;
this.toTypes = toTypes;
}
}
}