This is an automated email from the ASF dual-hosted git repository. jhyde pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/calcite.git
commit 39e2b22d298237f1f38051d20108bcea6f6f9dc5 Author: Julian Hyde <[email protected]> AuthorDate: Wed Feb 15 17:11:39 2023 -0800 [CALCITE-5522] Babel parser cannot handle some overloads of the DATE_TRUNC function This is a regression caused by [CALCITE-5447]. Adding support for `DATE_TRUNC(expr, timeUnitName)` caused the parser to no longer support `DATE_TRUNC(expr, expr)` such as `DATE_TRUNC('month', orders.order_date)`. The parser saw an identifier and wrongly concluded that it must be the name of a custom time frame. The solution is for the parser to treat `timeUnitName` as just an expression (that happens to be a `SqlIdentifier`). The validator can decide that it is a custom time frame and convert it to a `SqlIntervalQualifier`. Add interface SqlOperandHandler to allow SqlBasicFunction to customize how it handles particular arguments, without needing a new subclass of SqlFunction. The SqlOperandHandler interface is similar to other strategies (e.g. SqlReturnTypeInference, SqlOperandTypeChecker) and we think that it will be useful in solving other problems in future. In future work, we should remove from the parser specific treatment of functions, such as TIMESTAMP_TRUNC, that have one time frame argument. --- babel/src/test/resources/sql/redshift.iq | 11 +++ core/src/main/codegen/templates/Parser.jj | 86 ++++++++++------- .../org/apache/calcite/sql/SqlBasicFunction.java | 41 +++++++-- .../java/org/apache/calcite/sql/SqlOperator.java | 12 ++- .../calcite/sql/fun/SqlLibraryOperators.java | 4 +- .../apache/calcite/sql/type/OperandHandlers.java | 102 +++++++++++++++++++++ .../apache/calcite/sql/type/SqlOperandHandler.java | 32 +++++++ .../org/apache/calcite/test/SqlValidatorTest.java | 2 +- .../org/apache/calcite/test/SqlOperatorTest.java | 2 +- 9 files changed, 241 insertions(+), 51 deletions(-) diff --git a/babel/src/test/resources/sql/redshift.iq b/babel/src/test/resources/sql/redshift.iq index 86bb34111d..4917e3124a 100755 --- a/babel/src/test/resources/sql/redshift.iq +++ b/babel/src/test/resources/sql/redshift.iq @@ -1261,6 +1261,17 @@ select date_trunc('week', date '2008-09-09'); SELECT "DATE_TRUNC"('week', DATE '2008-09-09') !explain-validated-on calcite +with orders (id, order_date) as + (values (1, date '2023-02-15'), (2, date '2023-01-01')) +select DATE_TRUNC('month', orders.order_date) +from orders +order by id; +WITH "ORDERS" ("ID", "ORDER_DATE") AS (VALUES ROW(1, DATE '2023-02-15'), + ROW(2, DATE '2023-01-01')) (SELECT "DATE_TRUNC"('month', "ORDERS"."ORDER_DATE") + FROM "ORDERS" AS "ORDERS" + ORDER BY "ORDERS"."ID") +!explain-validated-on calcite + # EXTRACT (datepart FROM {TIMESTAMP 'literal' | timestamp}) returns DOUBLE # Extracts a date part from a timestamp or literal. -- returns 8 diff --git a/core/src/main/codegen/templates/Parser.jj b/core/src/main/codegen/templates/Parser.jj index eb0b408f5c..0b9dec5216 100644 --- a/core/src/main/codegen/templates/Parser.jj +++ b/core/src/main/codegen/templates/Parser.jj @@ -5083,10 +5083,8 @@ SqlIntervalQualifier IntervalQualifierStart() : * registered as abbreviations in your time frame set. */ SqlIntervalQualifier TimeUnitOrName() : { - final Span span; - final String w; - final TimeUnit unit; final SqlIdentifier unitName; + final SqlIntervalQualifier intervalQualifier; } { // When we see a time unit that is also a non-reserved keyword, such as @@ -5097,43 +5095,61 @@ SqlIntervalQualifier TimeUnitOrName() : { // Reserved keywords, such as SECOND, cannot be identifiers, and are // therefore not ambiguous. LOOKAHEAD(2) - ( - <NANOSECOND> { return new SqlIntervalQualifier(TimeUnit.NANOSECOND, null, getPos()); } - | <MICROSECOND> { return new SqlIntervalQualifier(TimeUnit.MICROSECOND, null, getPos()); } - | <MILLISECOND> { return new SqlIntervalQualifier(TimeUnit.MILLISECOND, null, getPos()); } - | <SECOND> { return new SqlIntervalQualifier(TimeUnit.SECOND, null, getPos()); } - | <MINUTE> { return new SqlIntervalQualifier(TimeUnit.MINUTE, null, getPos()); } - | <HOUR> { return new SqlIntervalQualifier(TimeUnit.HOUR, null, getPos()); } - | <DAY> { return new SqlIntervalQualifier(TimeUnit.DAY, null, getPos()); } - | <DOW> { return new SqlIntervalQualifier(TimeUnit.DOW, null, getPos()); } - | <DOY> { return new SqlIntervalQualifier(TimeUnit.DOY, null, getPos()); } - | <ISODOW> { return new SqlIntervalQualifier(TimeUnit.ISODOW, null, getPos()); } - | <ISOYEAR> { return new SqlIntervalQualifier(TimeUnit.ISOYEAR, null, getPos()); } - | <WEEK> { span = span(); } - ( - // There is a choice between "WEEK(weekday)" and "WEEK". We prefer - // the former, and the parser will look ahead for '('. - LOOKAHEAD(2) - <LPAREN> w = weekdayName() <RPAREN> { - return new SqlIntervalQualifier(w, span.end(this)); - } - | - { return new SqlIntervalQualifier(TimeUnit.WEEK, null, getPos()); } - ) - | <MONTH> { return new SqlIntervalQualifier(TimeUnit.MONTH, null, getPos()); } - | <QUARTER> { return new SqlIntervalQualifier(TimeUnit.QUARTER, null, getPos()); } - | <YEAR> { return new SqlIntervalQualifier(TimeUnit.YEAR, null, getPos()); } - | <EPOCH> { return new SqlIntervalQualifier(TimeUnit.EPOCH, null, getPos()); } - | <DECADE> { return new SqlIntervalQualifier(TimeUnit.DECADE, null, getPos()); } - | <CENTURY> { return new SqlIntervalQualifier(TimeUnit.CENTURY, null, getPos()); } - | <MILLENNIUM> { return new SqlIntervalQualifier(TimeUnit.MILLENNIUM, null, getPos()); } - ) + intervalQualifier = TimeUnit() { + return intervalQualifier; + } | unitName = SimpleIdentifier() { return new SqlIntervalQualifier(unitName.getSimple(), unitName.getParserPosition()); } } +/** Parses a built-in time unit (e.g. "YEAR") + * and returns a {@link SqlIntervalQualifier}. + * + * <p>Includes {@code WEEK} and {@code WEEK(SUNDAY)} through + {@code WEEK(SATURDAY)}. + * + * <p>Does not include SQL_TSI_DAY, SQL_TSI_FRAC_SECOND etc. These will be + * parsed as identifiers and can be resolved in the validator if they are + * registered as abbreviations in your time frame set. + */ +SqlIntervalQualifier TimeUnit() : { + final Span span; + final String w; +} +{ + <NANOSECOND> { return new SqlIntervalQualifier(TimeUnit.NANOSECOND, null, getPos()); } +| <MICROSECOND> { return new SqlIntervalQualifier(TimeUnit.MICROSECOND, null, getPos()); } +| <MILLISECOND> { return new SqlIntervalQualifier(TimeUnit.MILLISECOND, null, getPos()); } +| <SECOND> { return new SqlIntervalQualifier(TimeUnit.SECOND, null, getPos()); } +| <MINUTE> { return new SqlIntervalQualifier(TimeUnit.MINUTE, null, getPos()); } +| <HOUR> { return new SqlIntervalQualifier(TimeUnit.HOUR, null, getPos()); } +| <DAY> { return new SqlIntervalQualifier(TimeUnit.DAY, null, getPos()); } +| <DOW> { return new SqlIntervalQualifier(TimeUnit.DOW, null, getPos()); } +| <DOY> { return new SqlIntervalQualifier(TimeUnit.DOY, null, getPos()); } +| <ISODOW> { return new SqlIntervalQualifier(TimeUnit.ISODOW, null, getPos()); } +| <ISOYEAR> { return new SqlIntervalQualifier(TimeUnit.ISOYEAR, null, getPos()); } +| <WEEK> { span = span(); } + ( + // There is a choice between "WEEK(weekday)" and "WEEK". We prefer + // the former, and the parser will look ahead for '('. + LOOKAHEAD(2) + <LPAREN> w = weekdayName() <RPAREN> { + return new SqlIntervalQualifier(w, span.end(this)); + } + | + { return new SqlIntervalQualifier(TimeUnit.WEEK, null, getPos()); } + ) +| <MONTH> { return new SqlIntervalQualifier(TimeUnit.MONTH, null, getPos()); } +| <QUARTER> { return new SqlIntervalQualifier(TimeUnit.QUARTER, null, getPos()); } +| <YEAR> { return new SqlIntervalQualifier(TimeUnit.YEAR, null, getPos()); } +| <EPOCH> { return new SqlIntervalQualifier(TimeUnit.EPOCH, null, getPos()); } +| <DECADE> { return new SqlIntervalQualifier(TimeUnit.DECADE, null, getPos()); } +| <CENTURY> { return new SqlIntervalQualifier(TimeUnit.CENTURY, null, getPos()); } +| <MILLENNIUM> { return new SqlIntervalQualifier(TimeUnit.MILLENNIUM, null, getPos()); } +} + String weekdayName() : { } @@ -6739,7 +6755,7 @@ SqlCall DateTruncFunctionCall() : // and the Redshift variant, e.g. "DATE_TRUNC('year', DATE '2008-09-08')". ( LOOKAHEAD(2) - unit = TimeUnitOrName() { args.add(unit); } + unit = TimeUnit() { args.add(unit); } | AddExpression(args, ExprContext.ACCEPT_SUB_QUERY) ) diff --git a/core/src/main/java/org/apache/calcite/sql/SqlBasicFunction.java b/core/src/main/java/org/apache/calcite/sql/SqlBasicFunction.java index e4dfbaab16..5787938a44 100644 --- a/core/src/main/java/org/apache/calcite/sql/SqlBasicFunction.java +++ b/core/src/main/java/org/apache/calcite/sql/SqlBasicFunction.java @@ -16,10 +16,13 @@ */ package org.apache.calcite.sql; +import org.apache.calcite.sql.type.OperandHandlers; +import org.apache.calcite.sql.type.SqlOperandHandler; import org.apache.calcite.sql.type.SqlOperandTypeChecker; import org.apache.calcite.sql.type.SqlOperandTypeInference; import org.apache.calcite.sql.type.SqlReturnTypeInference; import org.apache.calcite.sql.validate.SqlMonotonicity; +import org.apache.calcite.sql.validate.SqlValidator; import org.checkerframework.checker.nullness.qual.Nullable; @@ -41,6 +44,7 @@ import static java.util.Objects.requireNonNull; public class SqlBasicFunction extends SqlFunction { private final SqlSyntax syntax; private final boolean deterministic; + private final SqlOperandHandler operandHandler; private final Function<SqlOperatorBinding, SqlMonotonicity> monotonicityInference; //~ Constructors ----------------------------------------------------------- @@ -54,6 +58,7 @@ public class SqlBasicFunction extends SqlFunction { * @param deterministic Whether the function is deterministic * @param returnTypeInference Strategy to use for return type inference * @param operandTypeInference Strategy to use for parameter type inference + * @param operandHandler Strategy to use for handling operands * @param operandTypeChecker Strategy to use for parameter type checking * @param category Categorization for function * @param monotonicityInference Strategy to infer monotonicity of a call @@ -61,6 +66,7 @@ public class SqlBasicFunction extends SqlFunction { private SqlBasicFunction(String name, SqlKind kind, SqlSyntax syntax, boolean deterministic, SqlReturnTypeInference returnTypeInference, @Nullable SqlOperandTypeInference operandTypeInference, + SqlOperandHandler operandHandler, SqlOperandTypeChecker operandTypeChecker, SqlFunctionCategory category, Function<SqlOperatorBinding, SqlMonotonicity> monotonicityInference) { @@ -70,6 +76,7 @@ public class SqlBasicFunction extends SqlFunction { requireNonNull(operandTypeChecker, "operandTypeChecker"), category); this.syntax = requireNonNull(syntax, "syntax"); this.deterministic = deterministic; + this.operandHandler = requireNonNull(operandHandler, "operandHandler"); this.monotonicityInference = requireNonNull(monotonicityInference, "monotonicityInference"); } @@ -80,7 +87,8 @@ public class SqlBasicFunction extends SqlFunction { SqlReturnTypeInference returnTypeInference, SqlOperandTypeChecker operandTypeChecker) { return new SqlBasicFunction(kind.name(), kind, - SqlSyntax.FUNCTION, true, returnTypeInference, null, operandTypeChecker, + SqlSyntax.FUNCTION, true, returnTypeInference, null, + OperandHandlers.DEFAULT, operandTypeChecker, SqlFunctionCategory.SYSTEM, call -> SqlMonotonicity.NOT_MONOTONIC); } @@ -91,7 +99,8 @@ public class SqlBasicFunction extends SqlFunction { SqlReturnTypeInference returnTypeInference, SqlOperandTypeChecker operandTypeChecker) { return new SqlBasicFunction(name, SqlKind.OTHER_FUNCTION, - SqlSyntax.FUNCTION, true, returnTypeInference, null, operandTypeChecker, + SqlSyntax.FUNCTION, true, returnTypeInference, null, + OperandHandlers.DEFAULT, operandTypeChecker, SqlFunctionCategory.NUMERIC, call -> SqlMonotonicity.NOT_MONOTONIC); } @@ -101,7 +110,8 @@ public class SqlBasicFunction extends SqlFunction { SqlReturnTypeInference returnTypeInference, SqlOperandTypeChecker operandTypeChecker, SqlFunctionCategory category) { return new SqlBasicFunction(name, SqlKind.OTHER_FUNCTION, - SqlSyntax.FUNCTION, true, returnTypeInference, null, operandTypeChecker, + SqlSyntax.FUNCTION, true, returnTypeInference, null, + OperandHandlers.DEFAULT, operandTypeChecker, category, call -> SqlMonotonicity.NOT_MONOTONIC); } @@ -127,31 +137,35 @@ public class SqlBasicFunction extends SqlFunction { return monotonicityInference.apply(call); } + @Override public SqlNode rewriteCall(SqlValidator validator, SqlCall call) { + return operandHandler.rewriteCall(validator, call); + } + /** Returns a copy of this function with a given name. */ public SqlBasicFunction withName(String name) { return new SqlBasicFunction(name, kind, syntax, deterministic, - getReturnTypeInference(), getOperandTypeInference(), + getReturnTypeInference(), getOperandTypeInference(), operandHandler, getOperandTypeChecker(), getFunctionType(), monotonicityInference); } /** Returns a copy of this function with a given kind. */ public SqlBasicFunction withKind(SqlKind kind) { return new SqlBasicFunction(getName(), kind, syntax, deterministic, - getReturnTypeInference(), getOperandTypeInference(), + getReturnTypeInference(), getOperandTypeInference(), operandHandler, getOperandTypeChecker(), getFunctionType(), monotonicityInference); } /** Returns a copy of this function with a given category. */ public SqlBasicFunction withFunctionType(SqlFunctionCategory category) { return new SqlBasicFunction(getName(), kind, syntax, deterministic, - getReturnTypeInference(), getOperandTypeInference(), + getReturnTypeInference(), getOperandTypeInference(), operandHandler, getOperandTypeChecker(), category, monotonicityInference); } /** Returns a copy of this function with a given syntax. */ public SqlBasicFunction withSyntax(SqlSyntax syntax) { return new SqlBasicFunction(getName(), kind, syntax, deterministic, - getReturnTypeInference(), getOperandTypeInference(), + getReturnTypeInference(), getOperandTypeInference(), operandHandler, getOperandTypeChecker(), getFunctionType(), monotonicityInference); } @@ -160,14 +174,21 @@ public class SqlBasicFunction extends SqlFunction { public SqlBasicFunction withOperandTypeInference( SqlOperandTypeInference operandTypeInference) { return new SqlBasicFunction(getName(), kind, syntax, deterministic, - getReturnTypeInference(), operandTypeInference, + getReturnTypeInference(), operandTypeInference, operandHandler, getOperandTypeChecker(), getFunctionType(), monotonicityInference); } + /** Returns a copy of this function with a given strategy for handling + * operands. */ + public SqlBasicFunction withOperandHandler(SqlOperandHandler operandHandler) { + return new SqlBasicFunction(getName(), kind, syntax, deterministic, + getReturnTypeInference(), getOperandTypeInference(), operandHandler, + getOperandTypeChecker(), getFunctionType(), monotonicityInference); + } /** Returns a copy of this function with a given determinism. */ public SqlBasicFunction withDeterministic(boolean deterministic) { return new SqlBasicFunction(getName(), kind, syntax, deterministic, - getReturnTypeInference(), getOperandTypeInference(), + getReturnTypeInference(), getOperandTypeInference(), operandHandler, getOperandTypeChecker(), getFunctionType(), monotonicityInference); } @@ -176,7 +197,7 @@ public class SqlBasicFunction extends SqlFunction { public SqlBasicFunction withMonotonicityInference( Function<SqlOperatorBinding, SqlMonotonicity> monotonicityInference) { return new SqlBasicFunction(getName(), kind, syntax, deterministic, - getReturnTypeInference(), getOperandTypeInference(), + getReturnTypeInference(), getOperandTypeInference(), operandHandler, getOperandTypeChecker(), getFunctionType(), monotonicityInference); } } diff --git a/core/src/main/java/org/apache/calcite/sql/SqlOperator.java b/core/src/main/java/org/apache/calcite/sql/SqlOperator.java index 466c1e74b8..8cce10506d 100644 --- a/core/src/main/java/org/apache/calcite/sql/SqlOperator.java +++ b/core/src/main/java/org/apache/calcite/sql/SqlOperator.java @@ -665,8 +665,9 @@ public abstract class SqlOperator { final SqlValidatorScope operandScope = scope.getOperandScope(call); final ImmutableList.Builder<RelDataType> argTypeBuilder = - ImmutableList.builder(); - for (SqlNode operand : args) { + ImmutableList.builder(); + for (int i = 0; i < args.size(); i++) { + SqlNode operand = args.get(i); RelDataType nodeType; // for row arguments that should be converted to ColumnList // types, set the nodeType to a ColumnList type but defer @@ -677,7 +678,7 @@ public abstract class SqlOperator { nodeType = typeFactory.createSqlType(SqlTypeName.COLUMN_LIST); validator.setValidatedNodeType(operand, nodeType); } else { - nodeType = validator.deriveType(operandScope, operand); + nodeType = deriveOperandType(validator, operandScope, i, operand); } argTypeBuilder.add(nodeType); } @@ -685,6 +686,11 @@ public abstract class SqlOperator { return argTypeBuilder.build(); } + protected RelDataType deriveOperandType(SqlValidator validator, + SqlValidatorScope scope, int i, SqlNode operand) { + return validator.deriveType(scope, operand); + } + /** * Returns whether this operator should be surrounded by space when * unparsed. diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java index 9efadfca79..6b91ffa1c0 100644 --- a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java +++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java @@ -32,6 +32,7 @@ import org.apache.calcite.sql.SqlSpecialOperator; import org.apache.calcite.sql.SqlSyntax; import org.apache.calcite.sql.SqlWriter; import org.apache.calcite.sql.type.InferTypes; +import org.apache.calcite.sql.type.OperandHandlers; import org.apache.calcite.sql.type.OperandTypes; import org.apache.calcite.sql.type.ReturnTypes; import org.apache.calcite.sql.type.SqlOperandCountRanges; @@ -837,7 +838,8 @@ public abstract class SqlLibraryOperators { ReturnTypes.DATE_NULLABLE, OperandTypes.sequence("'DATE_TRUNC(<DATE>, <DATETIME_INTERVAL>)'", OperandTypes.DATE, OperandTypes.dateInterval()), - SqlFunctionCategory.TIMEDATE); + SqlFunctionCategory.TIMEDATE) + .withOperandHandler(OperandHandlers.OPERAND_1_MIGHT_BE_TIME_FRAME); /** The "TIME_SUB(time, interval)" function (BigQuery); * subtracts an interval from a time, independent of any time zone. diff --git a/core/src/main/java/org/apache/calcite/sql/type/OperandHandlers.java b/core/src/main/java/org/apache/calcite/sql/type/OperandHandlers.java new file mode 100644 index 0000000000..596e26bad9 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/sql/type/OperandHandlers.java @@ -0,0 +1,102 @@ +/* + * 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.type; + +import org.apache.calcite.linq4j.Ord; +import org.apache.calcite.rel.type.TimeFrame; +import org.apache.calcite.sql.SqlCall; +import org.apache.calcite.sql.SqlIdentifier; +import org.apache.calcite.sql.SqlIntervalQualifier; +import org.apache.calcite.sql.SqlNode; +import org.apache.calcite.sql.validate.SqlValidator; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +/** + * Strategies for handling operands. + * + * <p>This class defines singleton instances of strategy objects for operand + * expansion. {@link OperandTypes}, {@link ReturnTypes} and {@link InferTypes} + * provide similar strategies for operand type checking and inference, + * and operator return type inference. + * + * @see SqlOperandTypeChecker + * @see ReturnTypes + * @see InferTypes + */ +public abstract class OperandHandlers { + /** The default operand handler visits all operands. */ + public static final SqlOperandHandler DEFAULT = + new SqlOperandHandler() { + }; + + /** An operand handler that tries to convert operand #1 (0-based) into a time + * frame. + * + * <p>For example, the {@code DATE_TRUNC} function uses this; the calls + * {@code DATE_TRUNC('month', orders.order_date)}, + * {@code DATE_TRUNC(orders.order_date, MONTH)}, + * {@code DATE_TRUNC(orders.order_date, MINUTE15)} + * are all valid. The last uses a user-defined time frame, which appears + * to the validator as a {@link SqlIdentifier} and is then converted to a + * {@link SqlIntervalQualifier} when it matches a defined time frame. */ + public static final SqlOperandHandler OPERAND_1_MIGHT_BE_TIME_FRAME = + new TimeFrameOperandHandler(1); + + /** Operand handler for a function whose {@code timeFrameOperand} operand + * (0-based) may be a time frame. If the operand is of type + * {@link SqlIdentifier}, looks up the custom time frame and converts it to a + * {@link SqlIntervalQualifier}. */ + private static class TimeFrameOperandHandler implements SqlOperandHandler { + private final int timeFrameOperand; + + TimeFrameOperandHandler(int timeFrameOperand) { + this.timeFrameOperand = timeFrameOperand; + } + + private SqlNode getOperand(int i, SqlNode operand, + Function<String, @Nullable TimeFrame> timeFrameResolver) { + if (i == timeFrameOperand + && operand instanceof SqlIdentifier + && ((SqlIdentifier) operand).isSimple()) { + final String name = ((SqlIdentifier) operand).getSimple(); + final TimeFrame timeFrame = timeFrameResolver.apply(name); + if (timeFrame != null) { + return new SqlIntervalQualifier(name, operand.getParserPosition()); + } + } + return operand; + } + + @Override public SqlNode rewriteCall(SqlValidator validator, SqlCall call) { + final List<SqlNode> newOperandList = new ArrayList<>(); + Ord.forEach(call.getOperandList(), (operand, i) -> + newOperandList.add( + getOperand(i, operand, name -> + validator.getTimeFrameSet().getOpt(name)))); + if (newOperandList.equals(call.getOperandList())) { + return call; + } + return call.getOperator().createCall(call.getFunctionQuantifier(), + call.getParserPosition(), newOperandList); + } + } +} diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlOperandHandler.java b/core/src/main/java/org/apache/calcite/sql/type/SqlOperandHandler.java new file mode 100644 index 0000000000..7ed3aee6d2 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/sql/type/SqlOperandHandler.java @@ -0,0 +1,32 @@ +/* + * 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.type; + +import org.apache.calcite.sql.SqlCall; +import org.apache.calcite.sql.SqlNode; +import org.apache.calcite.sql.validate.SqlValidator; + +/** + * Strategy interface to process operands of an operator call. + * + * @see OperandHandlers + */ +public interface SqlOperandHandler { + default SqlNode rewriteCall(SqlValidator validator, SqlCall call) { + return call; + } +} 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 605bcc3f60..f8599f1b69 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java @@ -2758,7 +2758,7 @@ public class SqlValidatorTest extends SqlValidatorTestCase { // Check that each invalid code fails each query that it should. Consumer<String> invalidConsumer = weekday -> { - String errorMessage = "'" + weekday + "' is not a valid time frame"; + String errorMessage = "Column '" + weekday + "' not found in any table"; f.withSql("select date_trunc(" + ds + ", ^" + weekday + "^)") .fails(errorMessage); }; diff --git a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java index 9d6a171d38..45e2a65916 100644 --- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java +++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java @@ -8537,7 +8537,7 @@ public class SqlOperatorTest { .withLibrary(SqlLibrary.BIG_QUERY) .setFor(SqlLibraryOperators.DATE_TRUNC); f.checkFails("date_trunc(date '2015-02-19', ^foo^)", - "'FOO' is not a valid time frame", false); + "Column 'FOO' not found in any table", false); f.checkScalar("date_trunc(date '2015-02-19', day)", "2015-02-19", "DATE NOT NULL"); f.checkScalar("date_trunc(date '2015-02-19', week)",
