diff --git a/core/src/main/java/org/apache/calcite/sql/SqlValuesOperator.java b/core/src/main/java/org/apache/calcite/sql/SqlValuesOperator.java index 0175f89a316..966ddd3f618 100644 --- a/core/src/main/java/org/apache/calcite/sql/SqlValuesOperator.java +++ b/core/src/main/java/org/apache/calcite/sql/SqlValuesOperator.java @@ -16,6 +16,9 @@ */ package org.apache.calcite.sql; +import org.apache.calcite.sql.validate.SqlValidator; +import org.apache.calcite.sql.validate.SqlValidatorScope; + /** * The VALUES operator. */ @@ -28,6 +31,11 @@ public SqlValuesOperator() { //~ Methods ---------------------------------------------------------------- + @Override public void validateCall(SqlCall call, SqlValidator validator, + SqlValidatorScope scope, SqlValidatorScope operandScope) { + validator.validateQuery(call, scope, validator.getUnknownType()); + } + @Override public void unparse( SqlWriter writer, SqlCall call, diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java index 1d2c94d11a6..e21d62aecbf 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java @@ -2468,6 +2468,9 @@ protected void inferUnknownTypes( scope = getMeasureScope(((SelectScope) scope).getNode()); } inferUnknownTypes(inferredType, scope, ((SqlCall) node).operand(0)); + } else if (node.isA(SqlKind.QUERY)) { + // Do not descend into subqueries. Each query (SELECT, VALUES, + // etc.) calls inferUnknownTypes during its own validation. } else if (node instanceof SqlCall) { final SqlCall call = (SqlCall) node; final SqlOperandTypeInference operandTypeInference = diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java index 8a4a970e12a..7c08ef3925a 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java @@ -1322,13 +1322,20 @@ void testLikeAndSimilarFails() { expr("values 1.0 + ^NULL^") .withTypeCoercion(false) .fails("(?s).*Illegal use of .NULL.*"); + // SELECT produces DECIMAL(3, 1) because inferUnknownTypes infers + // NULL as DECIMAL(2, 1) via FIRST_KNOWN, and DECIMAL(2,1) + DECIMAL(2,1) + // = DECIMAL(3, 1) + sql("select 1.0 + NULL from (values (0)) as t(x)") + .columnType("DECIMAL(3, 1)"); expr("values 1.0 + NULL") - .columnType("DECIMAL(2, 1)"); + .columnType("DECIMAL(3, 1)"); expr("1.0 + ^NULL^") .withTypeCoercion(false) .fails("(?s).*Illegal use of .NULL.*"); + sql("select 1.0 + NULL from (values (0)) as t(x)") + .columnType("DECIMAL(3, 1)"); expr("1.0 + NULL") - .columnType("DECIMAL(2, 1)"); + .columnType("DECIMAL(3, 1)"); // FIXME: SQL:2003 does not allow raw NULL in IN clause expr("1 in (1, null, 2)").ok(); @@ -1602,9 +1609,14 @@ void testLikeAndSimilarFails() { expr("LOCALTIME").ok(); // fix sqlcontext later. wholeExpr("LOCALTIME(1+2)") .fails("Argument to function 'LOCALTIME' must be a literal"); - wholeExpr("LOCALTIME(NULL)") + // With type coercion disabled, inferUnknownTypes rejects NULL before + // LOCALTIME can validate. SELECT produces the same error. + sql("select LOCALTIME(^NULL^) from (values (0)) as t(x)") .withTypeCoercion(false) - .fails("Argument to function 'LOCALTIME' must not be NULL"); + .fails("(?s).*Illegal use of .NULL.*"); + expr("LOCALTIME(^NULL^)") + .withTypeCoercion(false) + .fails("(?s).*Illegal use of .NULL.*"); wholeExpr("LOCALTIME(NULL)") .fails("Argument to function 'LOCALTIME' must not be NULL"); wholeExpr("LOCALTIME(CAST(NULL AS INTEGER))") @@ -8225,6 +8237,78 @@ void testGroupExpressionEquivalenceParams() { .ok(); } + /** + * Test case for + * [CALCITE-7457] + * VALUES and SELECT produce different validation results for the same expression. + */ + @Test void testSelectVsValuesValidation() { + sql("select 1.0 + NULL from (values (0)) as t(x)") + .columnType("DECIMAL(3, 1)"); + expr("1.0 + NULL") + .columnType("DECIMAL(3, 1)"); + + sql("select 1 + ? from (values (0)) as t(x)").ok(); + expr("1 + ?").ok(); + + sql("select 1 + ^NULL^ from (values (0)) as t(x)") + .withTypeCoercion(false) + .fails("(?s).*Illegal use of .NULL.*"); + expr("1 + ^NULL^") + .withTypeCoercion(false) + .fails("(?s).*Illegal use of .NULL.*"); + + sql("select LOCALTIME(^NULL^) from (values (0)) as t(x)") + .withTypeCoercion(false) + .fails("(?s).*Illegal use of .NULL.*"); + expr("LOCALTIME(^NULL^)") + .withTypeCoercion(false) + .fails("(?s).*Illegal use of .NULL.*"); + } + + /** + * Verifies that {@code inferUnknownTypes} works for all subqueries + * from {@link org.apache.calcite.sql.SqlKind#QUERY}. + */ + @Test void testInferTypesForEveryQueryKindAsSubquery() { + // SELECT as scalar subquery + sql("select (select ? + 1 from (values (0)) as t(x))" + + " from (values (0)) as u(y)") + .assertBindType(is("RecordType(INTEGER ?0)")); + + // VALUES as scalar subquery + sql("select (values (? + 1)) from (values (0)) as u(y)") + .assertBindType(is("RecordType(INTEGER ?0)")); + + // UNION as subquery + sql("select * from (values (1)) as t(x)" + + " where x in (select ? + 1 from (values (0)) as u(y)" + + " union all select ? + 1 from (values (0)) as v(z))") + .assertBindType(is("RecordType(INTEGER ?0, INTEGER ?1)")); + + // INTERSECT as subquery + sql("select * from (values (1)) as t(x)" + + " where x in (select ? + 1 from (values (0)) as u(y)" + + " intersect select ? + 1 from (values (0)) as v(z))") + .assertBindType(is("RecordType(INTEGER ?0, INTEGER ?1)")); + + // EXCEPT as subquery + sql("select * from (values (1)) as t(x)" + + " where x in (select ? + 1 from (values (0)) as u(y)" + + " except select ? + 1 from (values (0)) as v(z))") + .assertBindType(is("RecordType(INTEGER ?0, INTEGER ?1)")); + + // WITH as scalar subquery + sql("select (with t(a) as (values (? + 1)) select a from t)" + + " from (values (0)) as u(y)") + .assertBindType(is("RecordType(INTEGER ?0)")); + + // ORDER_BY as scalar subquery + sql("select (select ? + 1 as c from (values (0)) as t(x) order by c)" + + " from (values (0)) as u(y)") + .assertBindType(is("RecordType(INTEGER ?0)")); + } + @Test void testPercentileFunctionsBigQuery() { final SqlOperatorTable opTable = operatorTableFor(SqlLibrary.BIG_QUERY); final String sql = "select\n" @@ -8405,15 +8489,28 @@ void testGroupExpressionEquivalenceParams() { + " overlaps (date '1-2-3', date '1-2-3')^\n" + "or false") .fails("(?s).*Cannot apply 'OVERLAPS' to arguments of type .*"); - // row with 3 arguments as right argument to overlaps + // row with 3 arguments as right argument to overlaps. + // validateValues checks ROW structure before OVERLAPS validates, + // producing "Unequal number of entries in ROW expressions". + // SELECT produces the same error. + sql("select true or" + + " (date '1-2-3', date '1-2-3')" + + " overlaps ^(date '1-2-3', date '1-2-3', date '1-2-3')^" + + " or false from (values (0)) as t(x)") + .fails("(?s).*Unequal number of entries in ROW expressions.*"); expr("true\n" - + "or ^(date '1-2-3', date '1-2-3')\n" - + " overlaps (date '1-2-3', date '1-2-3', date '1-2-3')^\n" + + "or (date '1-2-3', date '1-2-3')\n" + + " overlaps ^(date '1-2-3', date '1-2-3', date '1-2-3')^\n" + "or false") - .fails("(?s).*Cannot apply 'OVERLAPS' to arguments of type .*"); - expr("^period (date '1-2-3', date '1-2-3')\n" - + " overlaps (date '1-2-3', date '1-2-3', date '1-2-3')^") - .fails("(?s).*Cannot apply 'OVERLAPS' to arguments of type .*"); + .fails("(?s).*Unequal number of entries in ROW expressions.*"); + // SELECT produces the same error for mismatched ROW sizes + sql("select period (date '1-2-3', date '1-2-3')" + + " overlaps ^(date '1-2-3', date '1-2-3', date '1-2-3')^" + + " from (values (0)) as t(x)") + .fails("(?s).*Unequal number of entries in ROW expressions.*"); + expr("period (date '1-2-3', date '1-2-3')\n" + + " overlaps ^(date '1-2-3', date '1-2-3', date '1-2-3')^") + .fails("(?s).*Unequal number of entries in ROW expressions.*"); expr("true\n" + "or ^(1, 2) overlaps (2, 3)^\n" + "or false")