/*
// Licensed to Julian Hyde under one or more contributor license
// agreements. See the NOTICE file distributed with this work for
// additional information regarding copyright ownership.
//
// Julian Hyde 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.eigenbase.test;
import java.nio.charset.*;
import java.util.regex.*;
import org.eigenbase.reltype.*;
import org.eigenbase.sql.*;
import org.eigenbase.sql.parser.*;
import org.eigenbase.sql.test.DefaultSqlTestFactory;
import org.eigenbase.sql.test.SqlTester;
import org.eigenbase.sql.test.SqlTesterImpl;
import org.eigenbase.sql.validate.*;
import org.eigenbase.util.*;
import static org.junit.Assert.*;
/**
* 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 {
//~ Static fields/initializers ---------------------------------------------
private static final Pattern LINE_COL_PATTERN =
Pattern.compile("At line ([0-9]+), column ([0-9]+)");
private static final Pattern LINE_COL_TWICE_PATTERN =
Pattern.compile(
"(?s)From line ([0-9]+), column ([0-9]+) to line ([0-9]+), column ([0-9]+): (.*)");
//~ Instance fields --------------------------------------------------------
protected final SqlTester tester;
//~ Constructors -----------------------------------------------------------
/**
* 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 SqlTesterImpl(DefaultSqlTestFactory.INSTANCE);
}
public void check(String sql) {
tester.assertExceptionIsThrown(sql, null);
}
public void checkExp(String sql) {
tester.assertExceptionIsThrown(
SqlTesterImpl.buildQuery(sql),
null);
}
/**
* Checks that a SQL query gives a particular error, or succeeds if {@code
* expected} is null.
*/
public final void checkFails(
String sql,
String expected) {
tester.assertExceptionIsThrown(sql, expected);
}
/**
* Checks that a SQL expression gives a particular error.
*/
public final void checkExpFails(
String sql,
String expected) {
tester.assertExceptionIsThrown(
SqlTesterImpl.buildQuery(sql),
expected);
}
/**
* Checks that a SQL expression gives a particular error, and that the
* location of the error is the whole expression.
*/
public final void checkWholeExpFails(
String sql,
String expected) {
assert sql.indexOf('^') < 0;
checkExpFails("^" + sql + "^", expected);
}
public final void checkExpType(
String sql,
String expected) {
checkColumnType(
SqlTesterImpl.buildQuery(sql),
expected);
}
/**
* Checks that a query returns a single column, and that the column has the
* expected type. For example,
*
* <blockquote><code>checkColumnType("SELECT empno FROM Emp", "INTEGER NOT
* NULL");</code></blockquote>
*
* @param sql Query
* @param expected Expected type, including nullability
*/
public final void checkColumnType(
String sql,
String expected) {
tester.checkColumnType(sql, expected);
}
/**
* Checks that a query returns a row of the expected type. For example,
*
* <blockquote><code>checkResultType("select empno, name from emp","{EMPNO
* INTEGER NOT NULL, NAME VARCHAR(10) NOT NULL}");</code></blockquote>
*
* @param sql Query
* @param expected Expected row type
*/
public final void checkResultType(
String sql,
String expected) {
tester.checkResultType(sql, expected);
}
/**
* Checks that the first column returned by a query has the expected type.
* For example,
*
* <blockquote><code>checkQueryType("SELECT empno FROM Emp", "INTEGER NOT
* NULL");</code></blockquote>
*
* @param sql Query
* @param expected Expected type, including nullability
*/
public final void checkIntervalConv(
String sql,
String expected) {
tester.checkIntervalConv(
SqlTesterImpl.buildQuery(sql),
expected);
}
protected final void assertExceptionIsThrown(
String sql,
String expectedMsgPattern) {
assert expectedMsgPattern != null;
tester.assertExceptionIsThrown(sql, expectedMsgPattern);
}
public void checkCharset(
String sql,
Charset expectedCharset) {
tester.checkCharset(sql, expectedCharset);
}
public void checkCollation(
String sql,
String expectedCollationName,
SqlCollation.Coercibility expectedCoercibility) {
tester.checkCollation(sql, expectedCollationName, expectedCoercibility);
}
/**
* Checks whether an exception matches the expected pattern. If <code>
* sap</code> contains an error location, checks this too.
*
* @param ex Exception thrown
* @param expectedMsgPattern Expected pattern
* @param sap Query and (optional) position in query
*/
public static void checkEx(
Throwable ex,
String expectedMsgPattern,
SqlParserUtil.StringAndPos sap) {
if (null == ex) {
if (expectedMsgPattern == null) {
// No error expected, and no error happened.
return;
} else {
throw new AssertionError(
"Expected query to throw exception, but it did not; "
+ "query [" + sap.sql
+ "]; expected [" + expectedMsgPattern + "]");
}
}
Throwable actualException = ex;
String actualMessage = actualException.getMessage();
int actualLine = -1;
int actualColumn = -1;
int actualEndLine = 100;
int actualEndColumn = 99;
// Search for an EigenbaseContextException somewhere in the stack.
EigenbaseContextException ece = null;
for (Throwable x = ex; x != null; x = x.getCause()) {
if (x instanceof EigenbaseContextException) {
ece = (EigenbaseContextException) x;
break;
}
if (x.getCause() == x) {
break;
}
}
// Search for a SqlParseException -- with its position set -- somewhere
// in the stack.
SqlParseException spe = null;
for (Throwable x = ex; x != null; x = x.getCause()) {
if ((x instanceof SqlParseException)
&& (((SqlParseException) x).getPos() != null)) {
spe = (SqlParseException) x;
break;
}
if (x.getCause() == x) {
break;
}
}
if (ece != null) {
actualLine = ece.getPosLine();
actualColumn = ece.getPosColumn();
actualEndLine = ece.getEndPosLine();
actualEndColumn = ece.getEndPosColumn();
if (ece.getCause() != null) {
actualException = ece.getCause();
actualMessage = actualException.getMessage();
}
} else if (spe != null) {
actualLine = spe.getPos().getLineNum();
actualColumn = spe.getPos().getColumnNum();
actualEndLine = spe.getPos().getEndLineNum();
actualEndColumn = spe.getPos().getEndColumnNum();
if (spe.getCause() != null) {
actualException = spe.getCause();
actualMessage = actualException.getMessage();
}
} else {
final String message = ex.getMessage();
if (message != null) {
Matcher matcher = LINE_COL_TWICE_PATTERN.matcher(message);
if (matcher.matches()) {
actualLine = Integer.parseInt(matcher.group(1));
actualColumn = Integer.parseInt(matcher.group(2));
actualEndLine = Integer.parseInt(matcher.group(3));
actualEndColumn = Integer.parseInt(matcher.group(4));
actualMessage = matcher.group(5);
} else {
matcher = LINE_COL_PATTERN.matcher(message);
if (matcher.matches()) {
actualLine = Integer.parseInt(matcher.group(1));
actualColumn = Integer.parseInt(matcher.group(2));
}
}
}
}
if (null == expectedMsgPattern) {
if (null != actualException) {
actualException.printStackTrace();
fail(
"Validator threw unexpected exception"
+ "; query [" + sap.sql
+ "]; exception [" + actualMessage
+ "]; class [" + actualException.getClass()
+ "]; pos [line " + actualLine
+ " col " + actualColumn
+ " thru line " + actualLine
+ " col " + actualColumn + "]");
}
} else {
if (null == actualException) {
fail(
"Expected validator to throw "
+ "exception, but it did not; query [" + sap.sql
+ "]; expected [" + expectedMsgPattern + "]");
} else {
String sqlWithCarets;
if ((actualColumn <= 0)
|| (actualLine <= 0)
|| (actualEndColumn <= 0)
|| (actualEndLine <= 0)) {
if (sap.pos != null) {
throw new AssertionError(
"Expected error to have position,"
+ " but actual error did not: "
+ " actual pos [line " + actualLine
+ " col " + actualColumn
+ " thru line " + actualEndLine
+ " col " + actualEndColumn + "]");
}
sqlWithCarets = sap.sql;
} else {
sqlWithCarets =
SqlParserUtil.addCarets(
sap.sql,
actualLine,
actualColumn,
actualEndLine,
actualEndColumn + 1);
if (sap.pos == null) {
throw new AssertionError(
"Actual error had a position, but expected error"
+ " did not. Add error position carets to sql:\n"
+ sqlWithCarets);
}
}
if (actualMessage != null) {
actualMessage = Util.toLinux(actualMessage);
}
if ((actualMessage == null)
|| !actualMessage.matches(expectedMsgPattern)) {
actualException.printStackTrace();
final String actualJavaRegexp =
(actualMessage == null) ? "null"
: TestUtil.quoteForJava(
TestUtil.quotePattern(actualMessage));
fail(
"Validator threw different "
+ "exception than expected; query [" + sap.sql
+ "];\n"
+ " expected pattern [" + expectedMsgPattern
+ "];\n"
+ " actual [" + actualMessage
+ "];\n"
+ " actual as java regexp [" + actualJavaRegexp
+ "]; pos [" + actualLine
+ " col " + actualColumn
+ " thru line " + actualEndLine
+ " col " + actualEndColumn
+ "]; sql [" + sqlWithCarets + "]");
} else if (
(sap.pos != null)
&& ((actualLine != sap.pos.getLineNum())
|| (actualColumn != sap.pos.getColumnNum())
|| (actualEndLine != sap.pos.getEndLineNum())
|| (actualEndColumn != sap.pos.getEndColumnNum()))) {
fail(
"Validator threw expected "
+ "exception [" + actualMessage
+ "];\nbut at pos [line " + actualLine
+ " col " + actualColumn
+ " thru line " + actualEndLine
+ " col " + actualEndColumn
+ "];\nsql [" + sqlWithCarets + "]");
}
}
}
}
//~ Inner Interfaces -------------------------------------------------------
/**
* 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 sql 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
* message given must match the pattern
*/
void assertExceptionIsThrown(
String sql,
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 validator validator to use; null for default
* @param query query to test
* @param expectedRewrite expected SQL text after rewrite and unparse
*/
void checkRewrite(
SqlValidator validator,
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();
}
}
// End SqlValidatorTestCase.java