blob: 216c394c58f6ce0bdb231170c67cd58cf14047d0 [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.calcite.test;
import org.apache.calcite.avatica.util.Casing;
import org.apache.calcite.avatica.util.Quoting;
import org.apache.calcite.config.Lex;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.sql.SqlCollation;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlOperatorTable;
import org.apache.calcite.sql.parser.SqlParseException;
import org.apache.calcite.sql.parser.StringAndPos;
import org.apache.calcite.sql.test.AbstractSqlTester;
import org.apache.calcite.sql.test.SqlTestFactory;
import org.apache.calcite.sql.test.SqlTester;
import org.apache.calcite.sql.test.SqlValidatorTester;
import org.apache.calcite.sql.validate.SqlConformance;
import org.apache.calcite.sql.validate.SqlConformanceEnum;
import org.apache.calcite.sql.validate.SqlMonotonicity;
import org.apache.calcite.sql.validate.SqlValidator;
import org.apache.calcite.test.catalog.MockCatalogReaderExtended;
import org.apache.calcite.testlib.annotations.WithLex;
import com.google.common.base.Preconditions;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.platform.commons.support.AnnotationSupport;
import java.nio.charset.Charset;
import java.util.Objects;
import java.util.function.UnaryOperator;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
/**
* An abstract base class for implementing tests against {@link SqlValidator}.
*
* <p>A derived class can refine this test in two ways. First, it can add <code>
* testXxx()</code> methods, to test more functionality.
*
* <p>Second, it can override the {@link #getTester} method to return a
* different implementation of the {@link Tester} object. This encapsulates the
* differences between test environments, for example, which SQL parser or
* validator to use.</p>
*/
public class SqlValidatorTestCase {
private static final SqlTestFactory EXTENDED_TEST_FACTORY =
SqlTestFactory.INSTANCE.withCatalogReader(MockCatalogReaderExtended::new);
static final SqlTester EXTENDED_CATALOG_TESTER =
new SqlValidatorTester(EXTENDED_TEST_FACTORY);
static final SqlTester EXTENDED_CATALOG_TESTER_2003 =
new SqlValidatorTester(EXTENDED_TEST_FACTORY)
.withConformance(SqlConformanceEnum.PRAGMATIC_2003);
static final SqlTester EXTENDED_CATALOG_TESTER_LENIENT =
new SqlValidatorTester(EXTENDED_TEST_FACTORY)
.withConformance(SqlConformanceEnum.LENIENT);
protected SqlTester tester;
/**
* Creates a test case.
*/
public SqlValidatorTestCase() {
this.tester = getTester();
}
//~ Methods ----------------------------------------------------------------
/**
* Returns a tester. Derived classes should override this method to run the
* same set of tests in a different testing environment.
*/
public SqlTester getTester() {
return new SqlValidatorTester(SqlTestFactory.INSTANCE);
}
/** Creates a test context with a SQL query. */
public final Sql sql(String sql) {
return new Sql(tester, StringAndPos.of(sql), true, false);
}
/** Creates a test context with a SQL expression. */
public final Sql expr(String sql) {
return new Sql(tester, StringAndPos.of(sql), false, false);
}
/** Creates a test context with a SQL expression.
* If an error occurs, the error is expected to span the entire expression. */
public final Sql wholeExpr(String sql) {
return expr(sql).withWhole(true);
}
public final Sql winSql(String sql) {
return sql(sql);
}
public final Sql win(String sql) {
return sql("select * from emp " + sql);
}
public Sql winExp(String sql) {
return winSql("select " + sql + " from emp window w as (order by deptno)");
}
public Sql winExp2(String sql) {
return winSql("select " + sql + " from emp");
}
/**
* Encapsulates differences between test environments, for example, which
* SQL parser or validator to use.
*
* <p>It contains a mock schema with <code>EMP</code> and <code>DEPT</code>
* tables, which can run without having to start up Farrago.
*/
public interface Tester {
SqlNode parseQuery(String sql) throws SqlParseException;
SqlNode parseAndValidate(SqlValidator validator, String sql);
SqlValidator getValidator();
/**
* Checks that a query is valid, or, if invalid, throws the right
* message at the right location.
*
* <p>If <code>expectedMsgPattern</code> is null, the query must
* succeed.
*
* <p>If <code>expectedMsgPattern</code> is not null, the query must
* fail, and give an error location of (expectedLine, expectedColumn)
* through (expectedEndLine, expectedEndColumn).
* @param sap SQL statement
* @param expectedMsgPattern If this parameter is null the query must be
* valid for the test to pass; If this parameter
* is not null the query must be malformed and the
*/
void assertExceptionIsThrown(
StringAndPos sap,
String expectedMsgPattern);
/**
* Returns the data type of the sole column of a SQL query.
*
* <p>For example, <code>getResultType("VALUES (1")</code> returns
* <code>INTEGER</code>.
*
* <p>Fails if query returns more than one column.
*
* @see #getResultType(String)
*/
RelDataType getColumnType(String sql);
/**
* Returns the data type of the row returned by a SQL query.
*
* <p>For example, <code>getResultType("VALUES (1, 'foo')")</code>
* returns <code>RecordType(INTEGER EXPR$0, CHAR(3) EXPR#1)</code>.
*/
RelDataType getResultType(String sql);
void checkCollation(
String sql,
String expectedCollationName,
SqlCollation.Coercibility expectedCoercibility);
void checkCharset(
String sql,
Charset expectedCharset);
/**
* Checks that a query returns one column of an expected type. For
* example, <code>checkType("VALUES (1 + 2)", "INTEGER NOT
* NULL")</code>.
*/
void checkColumnType(
String sql,
String expected);
/**
* Given a SQL query, returns a list of the origins of each result
* field.
*
* @param sql SQL query
* @param fieldOriginList Field origin list, e.g.
* "{(CATALOG.SALES.EMP.EMPNO, null)}"
*/
void checkFieldOrigin(String sql, String fieldOriginList);
/**
* Checks that a query gets rewritten to an expected form.
*
* @param query query to test
* @param expectedRewrite expected SQL text after rewrite and unparse
*/
void checkRewrite(String query, String expectedRewrite);
/**
* Checks that a query returns one column of an expected type. For
* example, <code>checkType("select empno, name from emp""{EMPNO INTEGER
* NOT NULL, NAME VARCHAR(10) NOT NULL}")</code>.
*/
void checkResultType(
String sql,
String expected);
/**
* Checks if the interval value conversion to milliseconds is valid. For
* example, <code>checkIntervalConv(VALUES (INTERVAL '1' Minute),
* "60000")</code>.
*/
void checkIntervalConv(
String sql,
String expected);
/**
* Given a SQL query, returns the monotonicity of the first item in the
* SELECT clause.
*
* @param sql SQL query
* @return Monotonicity
*/
SqlMonotonicity getMonotonicity(String sql);
SqlConformance getConformance();
}
/** Fluent testing API. */
static class Sql {
private final SqlTester tester;
private final StringAndPos sap;
private final boolean query;
private final boolean whole;
/** Creates a Sql.
*
* @param tester Tester
* @param sap SQL query or expression
* @param query True if {@code sql} is a query, false if it is an expression
* @param whole Whether the failure location is the whole query or
* expression
*/
Sql(SqlTester tester, StringAndPos sap, boolean query,
boolean whole) {
this.tester = tester;
this.query = query;
this.sap = sap;
this.whole = whole;
}
Sql withTester(UnaryOperator<SqlTester> transform) {
return new Sql(transform.apply(tester), sap, query, whole);
}
public Sql sql(String sql) {
return new Sql(tester, StringAndPos.of(sql), true, false);
}
public Sql expr(String sql) {
return new Sql(tester, StringAndPos.of(sql), false, false);
}
public StringAndPos toSql(boolean withCaret) {
final String sql2 = withCaret && sap.cursor >= 0
? sap.sql.substring(0, sap.cursor)
+ "^" + sap.sql.substring(sap.cursor)
: sap.sql;
return query ? sap
: StringAndPos.of(AbstractSqlTester.buildQuery(sap.addCarets()));
}
Sql withExtendedCatalog() {
return withTester(tester -> EXTENDED_CATALOG_TESTER);
}
public Sql withQuoting(Quoting quoting) {
return withTester(tester -> tester.withQuoting(quoting));
}
Sql withLex(Lex lex) {
return withTester(tester -> tester.withLex(lex));
}
Sql withConformance(SqlConformance conformance) {
return withTester(tester -> tester.withConformance(conformance));
}
Sql withTypeCoercion(boolean typeCoercion) {
return withTester(tester -> tester.enableTypeCoercion(typeCoercion));
}
Sql withWhole(boolean whole) {
Preconditions.checkArgument(sap.cursor < 0);
return new Sql(tester, StringAndPos.of("^" + sap.sql + "^"),
query, whole);
}
Sql ok() {
tester.assertExceptionIsThrown(toSql(false), null);
return this;
}
/**
* Checks that a SQL expression gives a particular error.
*/
Sql fails(String expected) {
Objects.requireNonNull(expected, "expected");
tester.assertExceptionIsThrown(toSql(true), expected);
return this;
}
/**
* Checks that a SQL expression fails, giving an {@code expected} error,
* if {@code b} is true, otherwise succeeds.
*/
Sql failsIf(boolean b, String expected) {
if (b) {
fails(expected);
} else {
ok();
}
return this;
}
/**
* Checks that a query returns a row of the expected type. For example,
*
* <blockquote>
* <code>sql("select empno, name from emp")<br>
* .type("{EMPNO INTEGER NOT NULL, NAME VARCHAR(10) NOT NULL}");</code>
* </blockquote>
*
* @param expectedType Expected row type
*/
public Sql type(String expectedType) {
tester.checkResultType(sap.sql, expectedType);
return this;
}
/**
* Checks that a query returns a single column, and that the column has the
* expected type. For example,
*
* <blockquote>
* <code>sql("SELECT empno FROM Emp").columnType("INTEGER NOT NULL");</code>
* </blockquote>
*
* @param expectedType Expected type, including nullability
*/
public Sql columnType(String expectedType) {
tester.checkColumnType(toSql(false).sql, expectedType);
return this;
}
public Sql monotonic(SqlMonotonicity expectedMonotonicity) {
tester.checkMonotonic(toSql(false).sql, expectedMonotonicity);
return this;
}
public Sql bindType(final String bindType) {
tester.check(sap.sql, null, parameterRowType ->
assertThat(parameterRowType.toString(), is(bindType)),
result -> { });
return this;
}
public void charset(Charset expectedCharset) {
tester.checkCharset(sap.sql, expectedCharset);
}
public void collation(String expectedCollationName,
SqlCollation.Coercibility expectedCoercibility) {
tester.checkCollation(sap.sql, expectedCollationName, expectedCoercibility);
}
/**
* Checks if the interval value conversion to milliseconds is valid. For
* example,
*
* <blockquote>
* <code>sql("VALUES (INTERVAL '1' Minute)").intervalConv("60000");</code>
* </blockquote>
*/
public void intervalConv(String expected) {
tester.checkIntervalConv(toSql(false).sql, expected);
}
public Sql withCaseSensitive(boolean caseSensitive) {
return withTester(tester -> tester.withCaseSensitive(caseSensitive));
}
public Sql withOperatorTable(SqlOperatorTable operatorTable) {
return withTester(tester -> tester.withOperatorTable(operatorTable));
}
public Sql withUnquotedCasing(Casing casing) {
return withTester(tester -> tester.withUnquotedCasing(casing));
}
private SqlTester addTransform(SqlTester tester, UnaryOperator<SqlValidator> after) {
return this.tester.withValidatorTransform(transform ->
validator -> after.apply(transform.apply(validator)));
}
public Sql withValidatorIdentifierExpansion(boolean expansion) {
final UnaryOperator<SqlValidator> after = sqlValidator ->
sqlValidator.transform(config -> config.withIdentifierExpansion(expansion));
return withTester(tester -> addTransform(tester, after));
}
public Sql withValidatorCallRewrite(boolean rewrite) {
final UnaryOperator<SqlValidator> after = sqlValidator ->
sqlValidator.transform(config -> config.withCallRewrite(rewrite));
return withTester(tester -> addTransform(tester, after));
}
public Sql withValidatorColumnReferenceExpansion(boolean expansion) {
final UnaryOperator<SqlValidator> after = sqlValidator ->
sqlValidator.transform(config -> config.withColumnReferenceExpansion(expansion));
return withTester(tester -> addTransform(tester, after));
}
public Sql rewritesTo(String expected) {
tester.checkRewrite(toSql(false).sql, expected);
return this;
}
}
/**
* Enables to configure {@link #tester} behavior on a per-test basis.
* {@code tester} object is created in the test object constructor, and
* there's no trivial way to override its features.
*
* <p>This JUnit rule enables post-process test object on a per test method
* basis.
*/
public static class LexConfiguration implements BeforeEachCallback {
@Override public void beforeEach(ExtensionContext context) {
context.getElement()
.flatMap(element -> AnnotationSupport.findAnnotation(element, WithLex.class))
.ifPresent(lex -> {
SqlValidatorTestCase tc = (SqlValidatorTestCase) context.getTestInstance().get();
SqlTester tester = tc.tester;
tester = tester.withLex(lex.value());
tc.tester = tester;
});
}
}
}