org.apache.calcite.sql.test.SqlOperatorFixture Maven / Gradle / Ivy
/*
* 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.calcite.sql.test;
import org.apache.calcite.avatica.util.Casing;
import org.apache.calcite.avatica.util.Quoting;
import org.apache.calcite.config.CalciteConnectionProperty;
import org.apache.calcite.config.Lex;
import org.apache.calcite.sql.SqlOperator;
import org.apache.calcite.sql.SqlOperatorTable;
import org.apache.calcite.sql.fun.SqlLibrary;
import org.apache.calcite.sql.fun.SqlLibraryOperatorTableFactory;
import org.apache.calcite.sql.parser.SqlParser;
import org.apache.calcite.sql.parser.StringAndPos;
import org.apache.calcite.sql.test.SqlTester.ResultChecker;
import org.apache.calcite.sql.test.SqlTester.TypeChecker;
import org.apache.calcite.sql.validate.SqlConformance;
import org.apache.calcite.sql.validate.SqlConformanceEnum;
import org.apache.calcite.sql.validate.SqlValidator;
import org.apache.calcite.test.CalciteAssert;
import org.apache.calcite.test.ConnectionFactories;
import org.apache.calcite.test.ConnectionFactory;
import org.apache.calcite.test.Matchers;
import org.apache.calcite.util.Bug;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.UnaryOperator;
import static org.apache.calcite.rel.type.RelDataTypeImpl.NON_NULLABLE_SUFFIX;
import static org.apache.calcite.sql.test.ResultCheckers.isSingle;
/**
* A fixture for testing the SQL operators.
*
* It provides a fluent API so that you can write tests by chaining method
* calls.
*
*
It is immutable. If you have two test cases that require a similar set up
* (for example, the same SQL expression and parser configuration), it is safe
* to use the same fixture object as a starting point for both tests.
*
*
The idea is that when you define an operator (or another piece of SQL
* functionality), you can define the logical behavior of that operator once, as
* part of that operator. Later you can define one or more physical
* implementations of that operator, and test them all using the same set of
* tests.
*
*
Depending on the implementation of {@link SqlTester} used
* (see {@link #withTester(UnaryOperator)}), the fixture may or may not
* evaluate expressions and check their results.
*/
public interface SqlOperatorFixture extends AutoCloseable {
//~ Enums ------------------------------------------------------------------
// TODO: Change message
String INVALID_CHAR_MESSAGE = "(?s).*";
String OUT_OF_RANGE_MESSAGE = ".* out of range.*";
String WRONG_FORMAT_MESSAGE = "Number has wrong format.*";
// TODO: Change message
String DIVISION_BY_ZERO_MESSAGE = "(?s).*";
// TODO: Change message
String STRING_TRUNC_MESSAGE = "(?s).*";
// TODO: Change message
String BAD_DATETIME_MESSAGE = "(?s).*";
String LITERAL_OUT_OF_RANGE_MESSAGE =
"(?s).*Numeric literal.*out of range.*";
String INVALID_ARGUMENTS_NUMBER =
"Invalid number of arguments to function .* Was expecting .* arguments";
String INVALID_ARGUMENTS_TYPE_VALIDATION_ERROR =
"Cannot apply '.*' to arguments of type .*";
//~ Enums ------------------------------------------------------------------
/**
* Name of a virtual machine that can potentially implement an operator.
*/
enum VmName {
JAVA, EXPAND
}
//~ Methods ----------------------------------------------------------------
/** Returns the test factory. */
SqlTestFactory getFactory();
/** Creates a copy of this fixture with a new test factory. */
SqlOperatorFixture withFactory(UnaryOperator transform);
/** Returns the tester. */
SqlTester getTester();
/** Creates a copy of this fixture with a new tester. */
SqlOperatorFixture withTester(UnaryOperator transform);
/** Creates a copy of this fixture with a new parser configuration. */
default SqlOperatorFixture withParserConfig(
UnaryOperator transform) {
return withFactory(f -> f.withParserConfig(transform));
}
/** Returns a fixture that tests a given SQL quoting style. */
default SqlOperatorFixture withQuoting(Quoting quoting) {
return withParserConfig(c -> c.withQuoting(quoting));
}
/** Returns a fixture that applies a given casing policy to quoted
* identifiers. */
default SqlOperatorFixture withQuotedCasing(Casing casing) {
return withParserConfig(c -> c.withQuotedCasing(casing));
}
/** Returns a fixture that applies a given casing policy to unquoted
* identifiers. */
default SqlOperatorFixture withUnquotedCasing(Casing casing) {
return withParserConfig(c -> c.withUnquotedCasing(casing));
}
/** Returns a fixture that matches identifiers by case-sensitive or
* case-insensitive. */
default SqlOperatorFixture withCaseSensitive(boolean sensitive) {
return withParserConfig(c -> c.withCaseSensitive(sensitive));
}
/** Returns a fixture that follows a given lexical policy. */
default SqlOperatorFixture withLex(Lex lex) {
return withParserConfig(c -> c.withLex(lex));
}
/** Returns a fixture that tests conformance to a particular SQL language
* version. */
default SqlOperatorFixture withConformance(SqlConformance conformance) {
return withParserConfig(c -> c.withConformance(conformance))
.withValidatorConfig(c -> c.withConformance(conformance))
.withConnectionFactory(cf -> cf.with("conformance", conformance));
}
/** Returns the conformance. */
default SqlConformance conformance() {
return getFactory().parserConfig().conformance();
}
/** Returns a fixture with a given validator configuration. */
default SqlOperatorFixture withValidatorConfig(
UnaryOperator transform) {
return withFactory(f -> f.withValidatorConfig(transform));
}
/** Returns a fixture that tests with implicit type coercion on/off. */
default SqlOperatorFixture enableTypeCoercion(boolean enabled) {
return withValidatorConfig(c -> c.withTypeCoercionEnabled(enabled));
}
/** Returns a fixture that does not fail validation if it encounters an
* unknown function. */
default SqlOperatorFixture withLenientOperatorLookup(boolean lenient) {
return withValidatorConfig(c -> c.withLenientOperatorLookup(lenient));
}
/** Returns a fixture that gets connections from a given factory. */
default SqlOperatorFixture withConnectionFactory(
UnaryOperator transform) {
return withFactory(f -> f.withConnectionFactory(transform));
}
/** Returns a fixture that uses a given operator table. */
default SqlOperatorFixture withOperatorTable(
SqlOperatorTable operatorTable) {
return withFactory(f -> f.withOperatorTable(o -> operatorTable));
}
/** Returns whether to run tests that are considered 'broken'.
* Returns false by default, but it is useful to temporarily enable the
* 'broken' tests to see whether they are still broken. */
boolean brokenTestsEnabled();
/** Sets {@link #brokenTestsEnabled()}. */
SqlOperatorFixture withBrokenTestsEnabled(boolean enableBrokenTests);
void checkScalar(String expression,
TypeChecker typeChecker,
ResultChecker resultChecker);
/**
* Tests that a scalar SQL expression returns the expected result and the
* expected type. For example,
*
*
* checkScalar("1.1 + 2.9", "4.0", "DECIMAL(2, 1) NOT NULL");
*
*
* @param expression Scalar expression
* @param result Expected result
* @param resultType Expected result type
*/
default void checkScalar(
String expression,
Object result,
String resultType) {
checkType(expression, resultType);
checkScalar(expression, SqlTests.ANY_TYPE_CHECKER,
ResultCheckers.createChecker(result));
}
/**
* Tests that a scalar SQL expression returns the expected exact numeric
* result as an integer. For example,
*
*
* checkScalarExact("1 + 2", 3);
*
*
* @param expression Scalar expression
* @param result Expected result
*/
default void checkScalarExact(String expression, int result) {
checkScalar(expression, SqlTests.INTEGER_TYPE_CHECKER, isSingle(result));
}
/**
* Tests that a scalar SQL expression returns the expected exact numeric
* result. For example,
*
*
* checkScalarExact("1 + 2", "3");
*
*
* @param expression Scalar expression
* @param expectedType Type we expect the result to have, including
* nullability, precision and scale, for example
* DECIMAL(2, 1) NOT NULL
.
* @param result Expected result
*/
default void checkScalarExact(
String expression,
String expectedType,
String result) {
checkScalarExact(expression, expectedType, isSingle(result));
}
void checkScalarExact(
String expression,
String expectedType,
ResultChecker resultChecker);
/**
* Tests that a scalar SQL expression returns expected approximate numeric
* result. For example,
*
*
* checkScalarApprox("1.0 + 2.1", "3.1");
*
*
* @param expression Scalar expression
* @param expectedType Type we expect the result to have, including
* nullability, precision and scale, for example
* DECIMAL(2, 1) NOT NULL
.
* @param result Expected result, or a matcher
*
* @see Matchers#within(Number, double)
*/
void checkScalarApprox(
String expression,
String expectedType,
Object result);
/**
* Tests that a scalar SQL expression returns the expected boolean result.
* For example,
*
*
* checkScalarExact("TRUE AND FALSE", Boolean.TRUE);
*
*
* The expected result can be null:
*
*
* checkScalarExact("NOT UNKNOWN", null);
*
*
* @param expression Scalar expression
* @param result Expected result (null signifies NULL).
*/
void checkBoolean(
String expression,
@Nullable Boolean result);
/**
* Tests that a scalar SQL expression returns the expected string result.
* For example,
*
*
* checkScalarExact("'ab' || 'c'", "abc");
*
*
* @param expression Scalar expression
* @param result Expected result
* @param resultType Expected result type
*/
void checkString(
String expression,
String result,
String resultType);
/**
* Tests that a SQL expression returns the SQL NULL value. For example,
*
*
* checkNull("CHAR_LENGTH(CAST(NULL AS VARCHAR(3))");
*
*
* @param expression Scalar expression
*/
void checkNull(String expression);
/**
* Tests that a SQL expression has a given type. For example,
*
*
* checkType("SUBSTR('hello' FROM 1 FOR 3)",
* "VARCHAR(3) NOT NULL");
*
*
* This method checks length/precision, scale, and whether the type allows
* NULL values, so is more precise than the type-checking done by methods
* such as {@link #checkScalarExact}.
*
* @param expression Scalar expression
* @param type Type string
*/
void checkType(
String expression,
String type);
/** Very similar to {@link #checkType}, but generates inside a SELECT
* with a non-empty GROUP BY. Aggregate functions may be nullable if executed
* in a SELECT with an empty GROUP BY.
*
*
Viz: {@code SELECT sum(1) FROM emp} has type "INTEGER",
* {@code SELECT sum(1) FROM emp GROUP BY deptno} has type "INTEGER NOT NULL",
*/
default SqlOperatorFixture checkAggType(String expr, String type) {
checkColumnType(AbstractSqlTester.buildQueryAgg(expr), type);
return this;
}
/**
* Checks that a query returns one column of an expected type. For example,
* checkType("VALUES (1 + 2)", "INTEGER NOT NULL")
.
*
* @param sql Query expression
* @param type Type string
*/
void checkColumnType(
String sql,
String type);
/**
* Tests that a SQL query returns a single column with the given type. For
* example,
*
*
* check("VALUES (1 + 2)", "3", SqlTypeName.Integer);
*
*
* If result
is null, the expression must yield the SQL NULL
* value. If result
is a {@link java.util.regex.Pattern}, the
* result must match that pattern.
*
* @param query SQL query
* @param typeChecker Checks whether the result is the expected type; must
* not be null
* @param result Expected result, or matcher
*/
default void check(String query,
TypeChecker typeChecker,
Object result) {
check(query, typeChecker, SqlTests.ANY_PARAMETER_CHECKER,
ResultCheckers.createChecker(result));
}
default void check(String query, String expectedType, Object result) {
check(query, new SqlTests.StringTypeChecker(expectedType), result);
}
/**
* Tests that a SQL query returns a result of expected type and value.
* Checking of type and value are abstracted using {@link TypeChecker}
* and {@link ResultChecker} functors.
*
* @param query SQL query
* @param typeChecker Checks whether the result is the expected type
* @param parameterChecker Checks whether the parameters are of expected
* types
* @param resultChecker Checks whether the result has the expected value
*/
default void check(String query,
SqlTester.TypeChecker typeChecker,
SqlTester.ParameterChecker parameterChecker,
ResultChecker resultChecker) {
getTester()
.check(getFactory(), query, typeChecker, parameterChecker,
resultChecker);
}
/**
* Declares that this test is for a given operator. So we can check that all
* operators are tested.
*
* @param operator Operator
* @param unimplementedVmNames Names of virtual machines for which this
*/
SqlOperatorFixture setFor(
SqlOperator operator,
VmName... unimplementedVmNames);
/**
* Checks that an aggregate expression returns the expected result.
*
*
For example, checkAgg("AVG(DISTINCT x)", new String[] {"2", "3",
* null, "3" }, new Double(2.5), 0);
*
* @param expr Aggregate expression, e.g. SUM(DISTINCT x)
* @param inputValues Array of input values, e.g. ["1", null,
* "2"]
.
* @param checker Result checker
*/
void checkAgg(
String expr,
String[] inputValues,
ResultChecker checker);
/**
* Checks that an aggregate expression returns the expected result.
*
*
For example, checkAgg("AVG(DISTINCT x)", new String[] {"2", "3",
* null, "3" }, "INTEGER", isSingle([2, 3]));
*
* @param expr Aggregate expression, e.g. SUM(DISTINCT x)
* @param inputValues Array of input values, e.g. ["1", null,
* "2"]
.
* @param type Expected result type
* @param checker Result checker
*/
void checkAgg(
String expr,
String[] inputValues,
String type,
ResultChecker checker);
/**
* Checks that an aggregate expression with multiple args returns the expected
* result.
*
* @param expr Aggregate expression, e.g. AGG_FUNC(x, x2, x3)
* @param inputValues Nested array of input values, e.g. [
* ["1", null, "2"]
* ["3", "4", null]
* ]
* @param resultChecker Checks whether the result has the expected value
*/
void checkAggWithMultipleArgs(
String expr,
String[][] inputValues,
ResultChecker resultChecker);
/**
* Checks that a windowed aggregate expression returns the expected result.
*
*
For example, checkWinAgg("FIRST_VALUE(x)", new String[] {"2",
* "3", null, "3" }, "INTEGER NOT NULL", 2, 0d);
*
* @param expr Aggregate expression, e.g. {@code SUM(DISTINCT x)}
* @param inputValues Array of input values, e.g. {@code ["1", null, "2"]}
* @param type Expected result type
* @param resultChecker Checks whether the result has the expected value
*/
void checkWinAgg(
String expr,
String[] inputValues,
String windowSpec,
String type,
ResultChecker resultChecker);
/**
* Tests that an aggregate expression fails at run time.
*
* @param expr An aggregate expression
* @param inputValues Array of input values
* @param expectedError Pattern for expected error
* @param runtime If true, must fail at runtime; if false, must fail at
* validate time
*/
void checkAggFails(
String expr,
String[] inputValues,
String expectedError,
boolean runtime);
/**
* Tests that a scalar SQL expression fails at run time.
*
* @param expression SQL scalar expression
* @param expectedError Pattern for expected error. If !runtime, must
* include an error location.
* @param runtime If true, must fail at runtime; if false, must fail at
* validate time
*/
void checkFails(
StringAndPos expression,
String expectedError,
boolean runtime);
/** As {@link #checkFails(StringAndPos, String, boolean)}, but with a string
* that contains carets. */
default void checkFails(
String expression,
String expectedError,
boolean runtime) {
checkFails(StringAndPos.of(expression), expectedError, runtime);
}
/**
* Tests that a SQL query fails at prepare time.
*
* @param sap SQL query and error position
* @param expectedError Pattern for expected error. Must
* include an error location.
*/
void checkQueryFails(StringAndPos sap, String expectedError);
/**
* Tests that a SQL query succeeds at prepare time.
*
* @param sql SQL query
*/
void checkQuery(String sql);
default SqlOperatorFixture withLibrary(SqlLibrary library) {
return withOperatorTable(
SqlLibraryOperatorTableFactory.INSTANCE
.getOperatorTable(SqlLibrary.STANDARD, library))
.withConnectionFactory(cf ->
cf.with(ConnectionFactories.add(CalciteAssert.SchemaSpec.HR))
.with(CalciteConnectionProperty.FUN, library.fun));
}
default SqlOperatorFixture withLibraries(SqlLibrary... libraries) {
List names = new ArrayList<>();
for (SqlLibrary lib : libraries) {
names.add(lib.fun);
}
return withOperatorTable(
SqlLibraryOperatorTableFactory.INSTANCE
.getOperatorTable(libraries))
.withConnectionFactory(cf ->
cf.with(ConnectionFactories.add(CalciteAssert.SchemaSpec.HR))
.with(CalciteConnectionProperty.FUN, String.join(",", names)));
}
/** Applies this fixture to some code for each of the given libraries. */
default void forEachLibrary(Iterable extends SqlLibrary> libraries,
Consumer consumer) {
SqlLibrary.expand(libraries).forEach(library -> {
try {
consumer.accept(this.withLibrary(library));
} catch (Exception e) {
throw new RuntimeException("for library " + library, e);
}
});
}
/** Applies this fixture to some code for each of the given conformances. */
default void forEachConformance(Iterable extends SqlConformanceEnum> conformances,
Consumer consumer) {
conformances.forEach(conformance -> {
try {
consumer.accept(this.withConformance(conformance));
} catch (Exception e) {
throw new RuntimeException("for conformance " + conformance, e);
}
});
}
default SqlOperatorFixture forOracle(SqlConformance conformance) {
return withConformance(conformance)
.withOperatorTable(
SqlLibraryOperatorTableFactory.INSTANCE
.getOperatorTable(SqlLibrary.STANDARD, SqlLibrary.ORACLE))
.withConnectionFactory(cf ->
cf.with(ConnectionFactories.add(CalciteAssert.SchemaSpec.HR))
.with("fun", "oracle"));
}
/**
* Types for cast.
*/
enum CastType {
CAST("cast"),
SAFE_CAST("safe_cast"),
TRY_CAST("try_cast");
CastType(String name) {
this.name = name;
}
final String name;
}
default String getCastString(
String value,
String targetType,
boolean errorLoc,
CastType castType) {
if (errorLoc) {
value = "^" + value + "^";
}
String function = castType.name;
return function + "(" + value + " as " + targetType + ")";
}
default void checkCastToApproxOkay(String value, String targetType,
Object expected, CastType castType) {
checkScalarApprox(getCastString(value, targetType, false, castType),
getTargetType(targetType, castType), expected);
}
default void checkCastToStringOkay(String value, String targetType,
String expected, CastType castType) {
final String castString = getCastString(value, targetType, false, castType);
checkString(castString, expected, getTargetType(targetType, castType));
}
default void checkCastToScalarOkay(String value, String targetType,
String expected, CastType castType) {
final String castString = getCastString(value, targetType, false, castType);
checkScalarExact(castString, getTargetType(targetType, castType), expected);
}
default String getTargetType(String targetType, CastType castType) {
return castType == CastType.CAST ? targetType + NON_NULLABLE_SUFFIX : targetType;
}
default void checkCastToScalarOkay(String value, String targetType,
CastType castType) {
checkCastToScalarOkay(value, targetType, value, castType);
}
default void checkCastFails(String value, String targetType,
String expectedError, boolean runtime, CastType castType) {
// Safe casts should never fail
boolean shouldFail = castType == CastType.CAST;
final String castString = getCastString(value, targetType, shouldFail && !runtime, castType);
if (shouldFail) {
checkFails(castString, expectedError, runtime);
} else {
checkNull(castString);
}
}
default void checkCastToString(String value, @Nullable String type,
@Nullable String expected, CastType castType) {
String spaces = " ";
if (expected == null) {
expected = value.trim();
}
int len = expected.length();
if (type != null) {
value = getCastString(value, type, false, castType);
}
// currently no exception thrown for truncation
if (Bug.DT239_FIXED) {
checkCastFails(value,
"VARCHAR(" + (len - 1) + ")", STRING_TRUNC_MESSAGE,
true, castType);
}
checkCastToStringOkay(value, "VARCHAR(" + len + ")", expected, castType);
checkCastToStringOkay(value, "VARCHAR(" + (len + 5) + ")", expected, castType);
// currently no exception thrown for truncation
if (Bug.DT239_FIXED) {
checkCastFails(value,
"CHAR(" + (len - 1) + ")", STRING_TRUNC_MESSAGE,
true, castType);
}
checkCastToStringOkay(value, "CHAR(" + len + ")", expected, castType);
checkCastToStringOkay(value, "CHAR(" + (len + 5) + ")",
expected + spaces, castType);
}
}