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 49a0d5e9c1ef65618425189b22f2a2b5ed51f6d8 Author: TJ Banghart <[email protected]> AuthorDate: Wed Jan 18 10:10:51 2023 -0800 [CALCITE-5357] Add FORMAT_TIME, FORMAT_DATE, FORMAT_DATETIME, FORMAT_TIMESTAMP functions (enabled in BigQuery library) Add interface FormatModel, class FormatModels, interface FormatElement to support other formatting and parsing functions in future. Co-authored-by: TJ Banghart <[email protected]> Co-authored-by: Julian Hyde <[email protected]> Close apache/calcite#3034 --- babel/src/test/resources/sql/big-query.iq | 86 +++---- .../calcite/adapter/enumerable/RexImpTable.java | 42 ++++ .../org/apache/calcite/runtime/SqlFunctions.java | 23 ++ .../java/org/apache/calcite/sql/SqlDialect.java | 14 ++ .../calcite/sql/dialect/BigQuerySqlDialect.java | 11 + .../calcite/sql/fun/SqlLibraryOperators.java | 48 ++++ .../org/apache/calcite/sql/type/OperandTypes.java | 6 + .../org/apache/calcite/util/BuiltInMethod.java | 6 + .../apache/calcite/util/format/FormatElement.java | 58 +++++ .../calcite/util/format/FormatElementEnum.java | 234 +++++++++++++++++ .../apache/calcite/util/format/FormatModel.java | 37 +++ .../apache/calcite/util/format/FormatModels.java | 278 +++++++++++++++++++++ .../apache/calcite/util/format/package-info.java | 21 ++ .../calcite/rel/rel2sql/RelToSqlConverterTest.java | 35 +++ .../org/apache/calcite/util/FormatModelTest.java | 67 +++++ site/_docs/reference.md | 4 + .../org/apache/calcite/test/SqlOperatorTest.java | 89 +++++++ 17 files changed, 1016 insertions(+), 43 deletions(-) diff --git a/babel/src/test/resources/sql/big-query.iq b/babel/src/test/resources/sql/big-query.iq index c31141d2db..528ac8797b 100755 --- a/babel/src/test/resources/sql/big-query.iq +++ b/babel/src/test/resources/sql/big-query.iq @@ -2445,35 +2445,35 @@ SELECT * FROM items; # # Returns STRING -!if (false) { SELECT FORMAT_DATE("%x", DATE "2008-12-25") AS US_format; -+------------+ -| US_format | -+------------+ -| 12/25/08 | -+------------+ ++-----------+ +| US_format | ++-----------+ +| 12/25/08 | ++-----------+ +(1 row) + !ok -!} -!if (false) { SELECT FORMAT_DATE("%b-%d-%Y", DATE "2008-12-25") AS formatted; +-------------+ | formatted | +-------------+ | Dec-25-2008 | +-------------+ +(1 row) + !ok -!} -!if (false) { SELECT FORMAT_DATE("%b %Y", DATE "2008-12-25") AS formatted; -+-------------+ -| formatted | -+-------------+ -| Dec 2008 | -+-------------+ ++-----------+ +| formatted | ++-----------+ +| Dec 2008 | ++-----------+ +(1 row) + !ok -!} ##################################################################### # FORMAT_DATETIME @@ -2485,7 +2485,6 @@ SELECT FORMAT_DATE("%b %Y", DATE "2008-12-25") AS formatted; # # Returns STRING -!if (false) { SELECT FORMAT_DATETIME("%c", DATETIME "2008-12-25 15:30:00") AS formatted; @@ -2494,10 +2493,10 @@ SELECT +--------------------------+ | Thu Dec 25 15:30:00 2008 | +--------------------------+ +(1 row) + !ok -!} -!if (false) { SELECT FORMAT_DATETIME("%b-%d-%Y", DATETIME "2008-12-25 15:30:00") AS formatted; @@ -2506,20 +2505,21 @@ SELECT +-------------+ | Dec-25-2008 | +-------------+ +(1 row) + !ok -!} -!if (false) { SELECT FORMAT_DATETIME("%b %Y", DATETIME "2008-12-25 15:30:00") AS formatted; -+-------------+ -| formatted | -+-------------+ -| Dec 2008 | -+-------------+ ++-----------+ +| formatted | ++-----------+ +| Dec 2008 | ++-----------+ +(1 row) + !ok -!} ##################################################################### # FORMAT_TIME @@ -2532,15 +2532,15 @@ SELECT # # Returns STRING -!if (false) { SELECT FORMAT_TIME("%R", TIME "15:30:00") as formatted_time; +----------------+ | formatted_time | +----------------+ | 15:30 | +----------------+ +(1 row) + !ok -!} ##################################################################### # FORMAT_TIMESTAMP @@ -2553,36 +2553,36 @@ SELECT FORMAT_TIME("%R", TIME "15:30:00") as formatted_time; # See Supported Format Elements For TIMESTAMP for a list of format # elements that this function supports. -!if (false) { -SELECT FORMAT_TIMESTAMP("%c", TIMESTAMP "2008-12-25 15:30:00+00", "UTC") AS formatted; +SELECT FORMAT_TIMESTAMP("%c", TIMESTAMP "2008-12-25 15:30:00" /* TODO should be "2008-12-25 15:30:00+00" */, "UTC") AS formatted; +--------------------------+ | formatted | +--------------------------+ | Thu Dec 25 15:30:00 2008 | +--------------------------+ +(1 row) + !ok -!} -!if (false) { -SELECT FORMAT_TIMESTAMP("%b-%d-%Y", TIMESTAMP "2008-12-25 15:30:00+00") AS formatted; +SELECT FORMAT_TIMESTAMP("%b-%d-%Y", TIMESTAMP "2008-12-25 15:30:00" /* TODO should be "2008-12-25 15:30:00+00" */) AS formatted; +-------------+ | formatted | +-------------+ | Dec-25-2008 | +-------------+ +(1 row) + !ok -!} -!if (false) { -SELECT FORMAT_TIMESTAMP("%b %Y", TIMESTAMP "2008-12-25 15:30:00+00") +SELECT FORMAT_TIMESTAMP("%b %Y", TIMESTAMP "2008-12-25 15:30:00" /* TODO should be "2008-12-25 15:30:00+00" */) AS formatted; -+-------------+ -| formatted | -+-------------+ -| Dec 2008 | -+-------------+ ++-----------+ +| formatted | ++-----------+ +| Dec 2008 | ++-----------+ +(1 row) + !ok -!} ##################################################################### # PARSE_DATE diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java index 3e3eeb621f..013f748b61 100644 --- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java +++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java @@ -133,6 +133,10 @@ import static org.apache.calcite.sql.fun.SqlLibraryOperators.ENDS_WITH; import static org.apache.calcite.sql.fun.SqlLibraryOperators.EXISTS_NODE; import static org.apache.calcite.sql.fun.SqlLibraryOperators.EXTRACT_VALUE; import static org.apache.calcite.sql.fun.SqlLibraryOperators.EXTRACT_XML; +import static org.apache.calcite.sql.fun.SqlLibraryOperators.FORMAT_DATE; +import static org.apache.calcite.sql.fun.SqlLibraryOperators.FORMAT_DATETIME; +import static org.apache.calcite.sql.fun.SqlLibraryOperators.FORMAT_TIME; +import static org.apache.calcite.sql.fun.SqlLibraryOperators.FORMAT_TIMESTAMP; import static org.apache.calcite.sql.fun.SqlLibraryOperators.FROM_BASE64; import static org.apache.calcite.sql.fun.SqlLibraryOperators.ILIKE; import static org.apache.calcite.sql.fun.SqlLibraryOperators.JSON_DEPTH; @@ -553,11 +557,20 @@ public class RexImpTable { defineMethod(DATE_FROM_UNIX_DATE, "dateFromUnixDate", NullPolicy.STRICT); defineMethod(UNIX_DATE, "unixDate", NullPolicy.STRICT); + // Datetime constructors defineMethod(DATE, "date", NullPolicy.STRICT); defineMethod(DATETIME, "datetime", NullPolicy.STRICT); defineMethod(TIMESTAMP, "timestamp", NullPolicy.STRICT); defineMethod(TIME, "time", NullPolicy.STRICT); + // Datetime formatting methods + final FormatDatetimeImplementor datetimeFormatImpl = new FormatDatetimeImplementor(); + map.put(FORMAT_TIMESTAMP, datetimeFormatImpl); + map.put(FORMAT_DATE, datetimeFormatImpl); + map.put(FORMAT_TIME, datetimeFormatImpl); + map.put(FORMAT_DATETIME, datetimeFormatImpl); + + // Boolean operators map.put(IS_NULL, new IsNullImplementor()); map.put(IS_NOT_NULL, new IsNotNullImplementor()); map.put(IS_TRUE, new IsTrueImplementor()); @@ -2349,6 +2362,35 @@ public class RexImpTable { } } + /** + * Implementor for the {@code FORMAT_TIMESTAMP, FORMAT_DATE, FORMAT_TIME} and + * {@code FORMAT_DATETIME} functions. + */ + private static class FormatDatetimeImplementor extends MethodNameImplementor { + + FormatDatetimeImplementor() { + super("formatDatetime", NullPolicy.STRICT, false); + } + + @Override Expression implementSafe(final RexToLixTranslator translator, + final RexCall call, final List<Expression> argValueList) { + final Expression operand0 = argValueList.get(0); + final Expression operand1 = argValueList.get(1); + Method method; + switch (call.operands.get(1).getType().getSqlTypeName()) { + case TIME: + method = BuiltInMethod.FORMAT_TIME.method; + break; + case DATE: + method = BuiltInMethod.FORMAT_DATE.method; + break; + default: + method = BuiltInMethod.FORMAT_TIMESTAMP.method; + } + return Expressions.call(method, translator.getRoot(), operand0, operand1); + } + } + /** Implementor for a function that generates calls to a given method. */ private static class MethodImplementor extends AbstractRexCallImplementor { protected final Method method; diff --git a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java index a44c6390f0..49592abdad 100644 --- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java +++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java @@ -41,6 +41,8 @@ import org.apache.calcite.util.TimeWithTimeZoneString; import org.apache.calcite.util.TimestampWithTimeZoneString; import org.apache.calcite.util.Unsafe; import org.apache.calcite.util.Util; +import org.apache.calcite.util.format.FormatElement; +import org.apache.calcite.util.format.FormatModels; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.language.Soundex; @@ -88,6 +90,7 @@ import java.util.TimeZone; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BinaryOperator; import java.util.regex.Pattern; +import java.util.stream.Collectors; import static org.apache.calcite.linq4j.Nullness.castNonNull; import static org.apache.calcite.util.Static.RESOURCE; @@ -2571,6 +2574,26 @@ public class SqlFunctions { .toString(); } + private static String internalFormatDatetime(String fmtString, java.util.Date date) { + List<FormatElement> elements = + FormatModels.BIG_QUERY.parse(fmtString); + return elements.stream() + .map(ele -> ele.format(date)) + .collect(Collectors.joining()); + } + + public static String formatTimestamp(DataContext ctx, String fmtString, long timestamp) { + return internalFormatDatetime(fmtString, internalToTimestamp(timestamp)); + } + + public static String formatDate(DataContext ctx, String fmtString, int date) { + return internalFormatDatetime(fmtString, internalToDate(date)); + } + + public static String formatTime(DataContext ctx, String fmtString, int time) { + return internalFormatDatetime(fmtString, internalToTime(time)); + } + /** * Converts a SQL TIMESTAMP value from the internal representation type * (number of milliseconds since January 1st, 1970) to the Java Type diff --git a/core/src/main/java/org/apache/calcite/sql/SqlDialect.java b/core/src/main/java/org/apache/calcite/sql/SqlDialect.java index 4654f8fea1..ffb12c3629 100644 --- a/core/src/main/java/org/apache/calcite/sql/SqlDialect.java +++ b/core/src/main/java/org/apache/calcite/sql/SqlDialect.java @@ -38,6 +38,8 @@ import org.apache.calcite.sql.type.AbstractSqlType; import org.apache.calcite.sql.type.SqlTypeUtil; import org.apache.calcite.sql.validate.SqlConformance; import org.apache.calcite.sql.validate.SqlConformanceEnum; +import org.apache.calcite.util.format.FormatModel; +import org.apache.calcite.util.format.FormatModels; import com.google.common.base.Preconditions; import com.google.common.base.Suppliers; @@ -1002,6 +1004,18 @@ public class SqlDialect { } } + /** + * Returns a description of the format string used by functions in this + * dialect. + * + * <p>Dialects may need to override this element mapping if they differ from + * <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/Format-Models.html"> + * Oracle's format elements</a>. By default, this returns {@link FormatModels#DEFAULT}. + */ + public FormatModel getFormatModel() { + return FormatModels.DEFAULT; + } + /** * Returns whether the dialect supports nested aggregations, for instance * {@code SELECT SUM(SUM(1)) }. diff --git a/core/src/main/java/org/apache/calcite/sql/dialect/BigQuerySqlDialect.java b/core/src/main/java/org/apache/calcite/sql/dialect/BigQuerySqlDialect.java index df38d9c3a9..4fd8b70a32 100644 --- a/core/src/main/java/org/apache/calcite/sql/dialect/BigQuerySqlDialect.java +++ b/core/src/main/java/org/apache/calcite/sql/dialect/BigQuerySqlDialect.java @@ -43,6 +43,8 @@ import org.apache.calcite.sql.parser.SqlParserPos; import org.apache.calcite.sql.type.BasicSqlType; import org.apache.calcite.sql.type.SqlTypeName; import org.apache.calcite.sql.type.SqlTypeUtil; +import org.apache.calcite.util.format.FormatModel; +import org.apache.calcite.util.format.FormatModels; import com.google.common.collect.ImmutableList; @@ -271,6 +273,15 @@ public class BigQuerySqlDialect extends SqlDialect { } } + /** + * {@inheritDoc} + * + * @see FormatModels#BIG_QUERY + */ + @Override public FormatModel getFormatModel() { + return FormatModels.BIG_QUERY; + } + /** {@inheritDoc} * * <p>BigQuery data type reference: 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 6b91ffa1c0..936194d4e1 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 @@ -789,6 +789,54 @@ public abstract class SqlLibraryOperators { OperandTypes.STRING_STRING, SqlFunctionCategory.TIMEDATE); + /** The "FORMAT_TIME(string, time)" function (BigQuery); + * Formats a time object according to the specified string. */ + @LibraryOperator(libraries = {BIG_QUERY}) + public static final SqlFunction FORMAT_TIME = + SqlBasicFunction.create("FORMAT_TIME", ReturnTypes.VARCHAR_2000_NULLABLE, + OperandTypes.CHARACTER_TIME, SqlFunctionCategory.STRING); + + /** The "FORMAT_DATE(string, date)" function (BigQuery); + * Formats a date object according to the specified string. */ + @LibraryOperator(libraries = {BIG_QUERY}) + public static final SqlFunction FORMAT_DATE = + SqlBasicFunction.create("FORMAT_DATE", ReturnTypes.VARCHAR_2000_NULLABLE, + OperandTypes.CHARACTER_DATE, SqlFunctionCategory.STRING); + + /** The "FORMAT_TIMESTAMP(string, timestamp)" function (BigQuery); + * Formats a timestamp object according to the specified string. + * + * <p>In BigQuery, the "TIMESTAMP" datatype maps to Calcite's + * TIMESTAMP_WITH_LOCAL_TIME_ZONE */ + @LibraryOperator(libraries = {BIG_QUERY}) + public static final SqlFunction FORMAT_TIMESTAMP = + SqlBasicFunction.create("FORMAT_TIMESTAMP", + ReturnTypes.VARCHAR_2000_NULLABLE, + OperandTypes.sequence("FORMAT_TIMESTAMP(<CHARACTER>, " + + "<TIMESTAMP WITH LOCAL TIME ZONE>)", + OperandTypes.CHARACTER, OperandTypes.TIMESTAMP_LTZ) + .or( + OperandTypes.sequence("FORMAT_TIMESTAMP(<CHARACTER>, " + + "<TIMESTAMP WITH LOCAL TIME ZONE>, <CHARACTER>)", + OperandTypes.CHARACTER, OperandTypes.TIMESTAMP_LTZ, + OperandTypes.CHARACTER)), + SqlFunctionCategory.STRING); + + /** The "FORMAT_DATETIME(string, timestamp)" function (BigQuery); + * Formats a timestamp object according to the specified string. */ + @LibraryOperator(libraries = {BIG_QUERY}) + public static final SqlFunction FORMAT_DATETIME = + SqlBasicFunction.create("FORMAT_DATETIME", + ReturnTypes.VARCHAR_2000_NULLABLE, + OperandTypes.sequence("FORMAT_DATETIME(<CHARACTER>, <TIMESTAMP>)", + OperandTypes.CHARACTER, OperandTypes.TIMESTAMP_NTZ) + .or( + OperandTypes.sequence("FORMAT_DATETIME(<CHARACTER>, " + + "<TIMESTAMP>, <CHARACTER>)", + OperandTypes.CHARACTER, OperandTypes.TIMESTAMP_NTZ, + OperandTypes.CHARACTER)), + SqlFunctionCategory.STRING); + /** The "TIMESTAMP_ADD(timestamp, interval)" function (BigQuery), the * two-argument variant of the built-in * {@link SqlStdOperatorTable#TIMESTAMP_ADD TIMESTAMPADD} function, which has diff --git a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java index cd927392aa..1f23a81707 100644 --- a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java +++ b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java @@ -468,6 +468,12 @@ public abstract class OperandTypes { public static final SqlSingleOperandTypeChecker CHARACTER_CHARACTER_DATETIME = family(SqlTypeFamily.CHARACTER, SqlTypeFamily.CHARACTER, SqlTypeFamily.DATETIME); + public static final SqlSingleOperandTypeChecker CHARACTER_DATE = + family(SqlTypeFamily.CHARACTER, SqlTypeFamily.DATE); + + public static final SqlSingleOperandTypeChecker CHARACTER_TIME = + family(SqlTypeFamily.CHARACTER, SqlTypeFamily.TIME); + public static final SqlSingleOperandTypeChecker PERIOD = new PeriodOperandTypeChecker(); diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java index 52ff2bf1ec..2b3eaaf085 100644 --- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java +++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java @@ -500,6 +500,12 @@ public enum BuiltInMethod { "timestampWithLocalTimeZoneToTimestamp", long.class, TimeZone.class), TIMESTAMP_WITH_LOCAL_TIME_ZONE_TO_STRING(SqlFunctions.class, "timestampWithLocalTimeZoneToString", long.class, TimeZone.class), + FORMAT_TIMESTAMP(SqlFunctions.class, "formatTimestamp", DataContext.class, + String.class, long.class), + FORMAT_DATE(SqlFunctions.class, "formatDate", DataContext.class, String.class, + int.class), + FORMAT_TIME(SqlFunctions.class, "formatTime", DataContext.class, String.class, + int.class), UNIX_DATE_TO_STRING(DateTimeUtils.class, "unixDateToString", int.class), UNIX_TIME_TO_STRING(DateTimeUtils.class, "unixTimeToString", int.class), UNIX_TIMESTAMP_TO_STRING(DateTimeUtils.class, "unixTimestampToString", diff --git a/core/src/main/java/org/apache/calcite/util/format/FormatElement.java b/core/src/main/java/org/apache/calcite/util/format/FormatElement.java new file mode 100644 index 0000000000..6670e23423 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/FormatElement.java @@ -0,0 +1,58 @@ +/* + * 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.util.format; + +import org.apache.calcite.linq4j.function.Experimental; + +import java.util.function.Consumer; + +/** + * A format element in a format string. Knows how to parse and unparse itself. + */ +@Experimental +public interface FormatElement { + + /** + * Formats a date to its appropriate string representation for the element. + * + * <p>This API is subject to change. It might be more efficient if the + * signature was one of the following: + * + * <pre> + * void format(StringBuilder, java.util.Date) + * void format(StringBuilder, long) + * </pre> + */ + String format(java.util.Date date); + + /** + * Returns the description of an element. + * + * <p>For example, {@code %H} in MySQL represents the hour in 24-hour format + * (e.g., 00..23). This method returns the string "The hour (24-hour clock) as + * a decimal number (00-23)", which is the description of + * {@link FormatElementEnum#HH24}. + */ + String getDescription(); + + /** + * Applies a consumer to a format element. + */ + default void flatten(Consumer<FormatElement> consumer) { + consumer.accept(this); + } +} diff --git a/core/src/main/java/org/apache/calcite/util/format/FormatElementEnum.java b/core/src/main/java/org/apache/calcite/util/format/FormatElementEnum.java new file mode 100644 index 0000000000..7aed1fc486 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/FormatElementEnum.java @@ -0,0 +1,234 @@ +/* + * 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.util.format; + +import org.apache.calcite.avatica.util.DateTimeUtils; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; + +import static org.apache.calcite.linq4j.Nullness.castNonNull; + +/** + * Implementation of {@link FormatElement} containing the standard format + * elements. These are based on Oracle's format model documentation. + * + * <p>See + * <a href="https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlqr/Format-Models.html"> + * Oracle format model reference.</a> + * + * @see FormatModels#DEFAULT + */ +public enum FormatElementEnum implements FormatElement { + D("The weekday (Monday as the first day of the week) as a decimal number (1-7)") { + @Override public String format(Date date) { + final Calendar calendar = Work.get().calendar; + calendar.setTime(date); + return String.format(Locale.ROOT, "%d", calendar.get(Calendar.DAY_OF_WEEK)); + } + }, + DAY("The full weekday name") { + @Override public String format(Date date) { + final Work work = Work.get(); + return work.eeeeFormat.format(date); + } + }, + DD("The day of the month as a decimal number (01-31)") { + @Override public String format(Date date) { + final Calendar calendar = Work.get().calendar; + calendar.setTime(date); + return String.format(Locale.ROOT, "%02d", calendar.get(Calendar.DAY_OF_MONTH)); + } + }, + DDD("The day of the year as a decimal number (001-366)") { + @Override public String format(Date date) { + final Calendar calendar = Work.get().calendar; + calendar.setTime(date); + return String.format(Locale.ROOT, "%03d", calendar.get(Calendar.DAY_OF_YEAR)); + } + }, + DY("The abbreviated weekday name") { + @Override public String format(Date date) { + final Work work = Work.get(); + return work.eeeFormat.format(date); + } + }, + FF1("Fractional seconds to 1 digit") { + @Override public String format(Date date) { + final Work work = Work.get(); + return work.sFormat.format(date); + } + }, + FF2("Fractional seconds to 2 digits") { + @Override public String format(Date date) { + final Work work = Work.get(); + return work.ssFormat.format(date); + } + }, + FF3("Fractional seconds to 3 digits") { + @Override public String format(Date date) { + final Work work = Work.get(); + return work.sssFormat.format(date); + } + }, + FF4("Fractional seconds to 4 digits") { + @Override public String format(Date date) { + final Work work = Work.get(); + return work.ssssFormat.format(date); + } + }, + FF5("Fractional seconds to 5 digits") { + @Override public String format(Date date) { + final Work work = Work.get(); + return work.sssssFormat.format(date); + } + }, + FF6("Fractional seconds to 6 digits") { + @Override public String format(Date date) { + final Work work = Work.get(); + return work.ssssssFormat.format(date); + } + }, + HH24("The hour (24-hour clock) as a decimal number (00-23)") { + @Override public String format(Date date) { + final Calendar calendar = Work.get().calendar; + calendar.setTime(date); + return String.format(Locale.ROOT, "%02d", calendar.get(Calendar.HOUR_OF_DAY)); + } + }, + IW("The ISO 8601 week number of the year (Monday as the first day of the week) " + + "as a decimal number (01-53)") { + @Override public String format(Date date) { + // TODO: ensure this is isoweek + final Calendar calendar = Work.get().calendar; + calendar.setTime(date); + calendar.setFirstDayOfWeek(Calendar.MONDAY); + return String.format(Locale.ROOT, "%02d", calendar.get(Calendar.WEEK_OF_YEAR)); + } + }, + MI("The minute as a decimal number (00-59)") { + @Override public String format(Date date) { + final Calendar calendar = Work.get().calendar; + calendar.setTime(date); + return String.format(Locale.ROOT, "%02d", calendar.get(Calendar.MINUTE)); + } + }, + MM("The month as a decimal number (01-12)") { + @Override public String format(Date date) { + final Calendar calendar = Work.get().calendar; + calendar.setTime(date); + return String.format(Locale.ROOT, "%02d", calendar.get(Calendar.MONTH) + 1); + } + }, + MON("The abbreviated month name") { + @Override public String format(Date date) { + final Work work = Work.get(); + return work.mmmFormat.format(date); + } + }, + MONTH("The full month name (English)") { + @Override public String format(Date date) { + final Work work = Work.get(); + return work.mmmmFormat.format(date); + } + }, + Q("The quarter as a decimal number (1-4)") { + @Override public String format(Date date) { + final Calendar calendar = Work.get().calendar; + calendar.setTime(date); + return String.format(Locale.ROOT, "%d", (calendar.get(Calendar.MONTH) / 3) + 1); + } + }, + SS("The second as a decimal number (00-60)") { + @Override public String format(Date date) { + final Calendar calendar = Work.get().calendar; + calendar.setTime(date); + return String.format(Locale.ROOT, "%02d", calendar.get(Calendar.SECOND)); + } + }, + TZR("The time zone name") { + @Override public String format(Date date) { + // TODO: how to support timezones? + throw new UnsupportedOperationException(); + } + }, + WW("The week number of the year (Sunday as the first day of the week) as a decimal " + + "number (00-53)") { + @Override public String format(Date date) { + final Calendar calendar = Work.get().calendar; + calendar.setTime(date); + calendar.setFirstDayOfWeek(Calendar.SUNDAY); + return String.format(Locale.ROOT, "%02d", calendar.get(Calendar.WEEK_OF_YEAR)); + } + }, + YY("Last 2 digits of year") { + @Override public String format(Date date) { + final Work work = Work.get(); + return work.yyFormat.format(date); + } + }, + YYYY("The year with century as a decimal number") { + @Override public String format(Date date) { + final Calendar calendar = Work.get().calendar; + calendar.setTime(date); + return String.format(Locale.ROOT, "%d", calendar.get(Calendar.YEAR)); + } + }; + + private final String description; + + // TODO: be sure to deal with TZ + + FormatElementEnum(String description) { + this.description = description; + } + + @Override public String getDescription() { + return description; + } + + /** Work space. Provides a value for each mutable data structure that might + * be needed by a format element. Ensures thread-safety. */ + static class Work { + private static final ThreadLocal<@Nullable Work> THREAD_WORK = + ThreadLocal.withInitial(Work::new); + + /** Returns an instance of Work for this thread. */ + static Work get() { + return castNonNull(THREAD_WORK.get()); + } + + final Calendar calendar = + Calendar.getInstance(DateTimeUtils.DEFAULT_ZONE, Locale.ROOT); + final DateFormat eeeeFormat = new SimpleDateFormat("EEEE", Locale.ROOT); + final DateFormat eeeFormat = new SimpleDateFormat("EEE", Locale.ROOT); + final DateFormat mmmFormat = new SimpleDateFormat("MMM", Locale.ROOT); + final DateFormat mmmmFormat = new SimpleDateFormat("MMMM", Locale.ROOT); + final DateFormat sFormat = new SimpleDateFormat("S", Locale.ROOT); + final DateFormat ssFormat = new SimpleDateFormat("SS", Locale.ROOT); + final DateFormat sssFormat = new SimpleDateFormat("SSS", Locale.ROOT); + final DateFormat ssssFormat = new SimpleDateFormat("SSSS", Locale.ROOT); + final DateFormat sssssFormat = new SimpleDateFormat("SSSSS", Locale.ROOT); + final DateFormat ssssssFormat = new SimpleDateFormat("SSSSSS", Locale.ROOT); + final DateFormat yyFormat = new SimpleDateFormat("yy", Locale.ROOT); + } +} diff --git a/core/src/main/java/org/apache/calcite/util/format/FormatModel.java b/core/src/main/java/org/apache/calcite/util/format/FormatModel.java new file mode 100644 index 0000000000..39bac2f053 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/FormatModel.java @@ -0,0 +1,37 @@ +/* + * 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.util.format; + +import java.util.List; +import java.util.Map; + +/** + * Describes the format strings used by a formatting function such as + * {@code FORMAT_TIMESTAMP} or {@code CAST(string AS DATE FORMAT formatString)}. + * + * <p>Can parse a format string to a list of format elements. + * + * @see FormatModels + */ +public interface FormatModel { + /** Returns the map used to create the {@link FormatModel} instance. */ + Map<String, FormatElement> getElementMap(); + + /** Parses a format string using element identifiers supplied by + * {@code format}. */ + List<FormatElement> parse(String format); +} diff --git a/core/src/main/java/org/apache/calcite/util/format/FormatModels.java b/core/src/main/java/org/apache/calcite/util/format/FormatModels.java new file mode 100644 index 0000000000..be7a144b3a --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/FormatModels.java @@ -0,0 +1,278 @@ +/* + * 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.util.format; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.apache.calcite.util.format.FormatElementEnum.D; +import static org.apache.calcite.util.format.FormatElementEnum.DAY; +import static org.apache.calcite.util.format.FormatElementEnum.DD; +import static org.apache.calcite.util.format.FormatElementEnum.DDD; +import static org.apache.calcite.util.format.FormatElementEnum.DY; +import static org.apache.calcite.util.format.FormatElementEnum.FF1; +import static org.apache.calcite.util.format.FormatElementEnum.FF2; +import static org.apache.calcite.util.format.FormatElementEnum.FF3; +import static org.apache.calcite.util.format.FormatElementEnum.FF4; +import static org.apache.calcite.util.format.FormatElementEnum.FF5; +import static org.apache.calcite.util.format.FormatElementEnum.FF6; +import static org.apache.calcite.util.format.FormatElementEnum.HH24; +import static org.apache.calcite.util.format.FormatElementEnum.IW; +import static org.apache.calcite.util.format.FormatElementEnum.MI; +import static org.apache.calcite.util.format.FormatElementEnum.MM; +import static org.apache.calcite.util.format.FormatElementEnum.MON; +import static org.apache.calcite.util.format.FormatElementEnum.MONTH; +import static org.apache.calcite.util.format.FormatElementEnum.Q; +import static org.apache.calcite.util.format.FormatElementEnum.SS; +import static org.apache.calcite.util.format.FormatElementEnum.TZR; +import static org.apache.calcite.util.format.FormatElementEnum.WW; +import static org.apache.calcite.util.format.FormatElementEnum.YY; +import static org.apache.calcite.util.format.FormatElementEnum.YYYY; + +import static java.util.Objects.requireNonNull; + +/** + * Utilities for {@link FormatModel}. + */ +public class FormatModels { + private FormatModels() { + } + + /** The format model consisting of built-in format elements. + * + * <p>Due to the design of {@link FormatElementEnum}, it is similar to + * Oracle's format model. + */ + public static final FormatModel DEFAULT; + + /** Format model for BigQuery. + * + * <p>BigQuery format element reference: + * <a href="https://cloud.google.com/bigquery/docs/reference/standard-sql/format-elements"> + * BigQuery Standard SQL Format Elements</a>. + */ + public static final FormatModel BIG_QUERY; + + static { + final Map<String, FormatElement> map = new HashMap<>(); + for (FormatElementEnum fe : FormatElementEnum.values()) { + map.put(fe.toString(), fe); + } + DEFAULT = create(map); + + map.clear(); + map.put("%A", DAY); + map.put("%a", DY); + map.put("%B", MONTH); + map.put("%b", MON); + map.put("%c", + compositeElement("The date and time representation (English);", + DY, literalElement(" "), MON, literalElement(" "), + DD, literalElement(" "), HH24, literalElement(":"), + MI, literalElement(":"), SS, literalElement(" "), + YYYY)); + map.put("%d", DD); + map.put("%E1S", FF1); + map.put("%E2S", FF2); + map.put("%E3S", FF3); + map.put("%E4S", FF4); + map.put("%E5S", FF5); + map.put("%E*S", FF6); + map.put("%H", HH24); + map.put("%j", DDD); + map.put("%M", MI); + map.put("%m", MM); + map.put("%Q", Q); + map.put("%R", + compositeElement("The time in the format %H:%M", + HH24, literalElement(":"), MI)); + map.put("%S", SS); + map.put("%u", D); + map.put("%V", IW); + map.put("%W", WW); + map.put("%x", + compositeElement("The date representation in MM/DD/YY format", + MM, literalElement("/"), DD, literalElement("/"), YY)); + map.put("%Y", YYYY); + map.put("%y", YY); + map.put("%Z", TZR); + BIG_QUERY = create(map); + } + + /** + * Generates a {@link Pattern} using the keys of a {@link FormatModel} element + * map. This pattern is used in {@link FormatModel#parse(String)} to help + * locate known format elements in a string. + */ + private static Pattern regexFromMap(Map<String, FormatElement> elementMap) { + StringBuilder regex = new StringBuilder(); + for (String key : elementMap.keySet()) { + regex.append("(").append(Pattern.quote(key)).append(")|"); + } + // remove the last '|' + regex.setLength(regex.length() - 1); + return Pattern.compile(regex.toString()); + } + + /** + * Creates a {@link FormatModel} that uses the provided map to identify + * {@link FormatElement}s while parsing a format string. + */ + public static FormatModel create(Map<String, FormatElement> elementMap) { + final Pattern pattern = regexFromMap(elementMap); + return new FormatModelImpl(pattern, elementMap); + } + + /** + * Creates a literal format element. + */ + public static FormatElement literalElement(String literal) { + return new FormatModelElementLiteral(literal); + } + + /** + * Creates a composite format element from the provided list of elements + * and description. + */ + public static FormatElement compositeElement(String description, + FormatElement... fmtElements) { + return new CompositeFormatElement(ImmutableList.copyOf(fmtElements), + description); + } + + + /** Implementation of {@link FormatModel} based on a list of format + * elements. */ + private static class FormatModelImpl implements FormatModel { + final Pattern pattern; + final Map<String, FormatElement> elementMap; + + /** Cache of parsed format strings. + * + * <p>NOTE: The current implementation could grow without bounds. + * A per-thread cache would be better, or a cache tied to a statement + * execution (e.g. in DataContext). Also limit to a say 100 entries. */ + final Map<String, List<FormatElement>> memoizedElements = + new ConcurrentHashMap<>(); + + FormatModelImpl(Pattern pattern, Map<String, FormatElement> elementMap) { + this.pattern = requireNonNull(pattern, "pattern"); + this.elementMap = ImmutableMap.copyOf(elementMap); + } + + @Override public Map<String, FormatElement> getElementMap() { + return elementMap; + } + + private List<FormatElement> internalParse(String format) { + final ImmutableList.Builder<FormatElement> elements = + ImmutableList.builder(); + final Matcher matcher = pattern.matcher(format); + int i = 0; + String literal; + while (matcher.find()) { + // Add any leading literal text before next element match + literal = format.substring(i, matcher.start()); + if (!literal.isEmpty()) { + elements.add(literalElement(literal)); + } + // add the element match - use literal as default to be safe. + String key = matcher.group(); + elements.add(getElementMap().getOrDefault(key, literalElement(key))); + i = matcher.end(); + } + // add any remaining literal text after last element match + literal = format.substring(i); + if (!literal.isEmpty()) { + elements.add(literalElement(literal)); + } + return elements.build(); + } + + @Override public List<FormatElement> parse(String format) { + return memoizedElements.computeIfAbsent(format, this::internalParse); + } + } + + /** + * A format element that is literal text. + */ + private static class FormatModelElementLiteral implements FormatElement { + private final String literal; + + FormatModelElementLiteral(String literal) { + this.literal = requireNonNull(literal, "literal"); + } + + @Override public String format(Date date) { + return literal; + } + + @Override public String getDescription() { + return "Represents literal text in a format string"; + } + + @Override public String toString() { + return this.literal; + } + } + + /** + * A format element comprised of one or more {@link FormatElement} entries. + */ + private static class CompositeFormatElement implements FormatElement { + private final String description; + private final List<FormatElement> formatElements; + + CompositeFormatElement(List<FormatElement> formatElements, + String description) { + this.formatElements = ImmutableList.copyOf(formatElements); + this.description = requireNonNull(description, "description"); + } + + @Override public String format(Date date) { + StringBuilder buf = new StringBuilder(); + flatten(ele -> buf.append(ele.format(date))); + return buf.toString(); + } + + /** + * Applies a consumer to each format element that make up the composite + * element. + * + * <p>For example, {@code %R} in Google SQL represents the hour in 24-hour + * format (e.g., 00..23) followed by the minute as a decimal number. + * {@code flatten(i -> println(i.toString())); } would print "HH24:MI". + */ + @Override public void flatten(Consumer<FormatElement> consumer) { + formatElements.forEach(consumer); + } + + @Override public String getDescription() { + return this.description; + } + } +} diff --git a/core/src/main/java/org/apache/calcite/util/format/package-info.java b/core/src/main/java/org/apache/calcite/util/format/package-info.java new file mode 100644 index 0000000000..88ed6b551c --- /dev/null +++ b/core/src/main/java/org/apache/calcite/util/format/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * Utility classes for handling format strings. + */ +package org.apache.calcite.util.format; diff --git a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java index 827592089a..83f1fd13d1 100644 --- a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java +++ b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java @@ -1891,6 +1891,41 @@ class RelToSqlConverterTest { sql(timeTrunc).withLibrary(SqlLibrary.BIG_QUERY).ok(expectedTimeTrunc); } + @Test void testBigQueryDatetimeFormatFunctions() { + final String formatTime = "select format_time('%H', time '12:45:30')\n" + + "from \"foodmart\".\"product\"\n"; + final String formatDate = "select format_date('%b-%d-%Y', date '2012-02-03')\n" + + "from \"foodmart\".\"product\"\n"; + final String formatTimestamp = "select format_timestamp('%b-%d-%Y',\n" + + " timestamp with local time zone '2012-02-03 12:30:40')\n" + + "from \"foodmart\".\"product\"\n"; + final String formatDatetime = "select format_datetime('%R',\n" + + " timestamp '2012-02-03 12:34:34')\n" + + "from \"foodmart\".\"product\"\n"; + + final String expectedBqFormatTime = + "SELECT FORMAT_TIME('%H', TIME '12:45:30')\n" + + "FROM foodmart.product"; + final String expectedBqFormatDate = + "SELECT FORMAT_DATE('%b-%d-%Y', DATE '2012-02-03')\n" + + "FROM foodmart.product"; + final String expectedBqFormatTimestamp = + "SELECT FORMAT_TIMESTAMP('%b-%d-%Y', TIMESTAMP_WITH_LOCAL_TIME_ZONE '2012-02-03 12:30:40')\n" + + "FROM foodmart.product"; + final String expectedBqFormatDatetime = + "SELECT FORMAT_DATETIME('%R', TIMESTAMP '2012-02-03 12:34:34')\n" + + "FROM foodmart.product"; + final Sql sql = fixture().withBigQuery().withLibrary(SqlLibrary.BIG_QUERY); + sql.withSql(formatTime) + .ok(expectedBqFormatTime); + sql.withSql(formatDate) + .ok(expectedBqFormatDate); + sql.withSql(formatTimestamp) + .ok(expectedBqFormatTimestamp); + sql.withSql(formatDatetime) + .ok(expectedBqFormatDatetime); + } + /** Test case for * <a href="https://issues.apache.org/jira/browse/CALCITE-3220">[CALCITE-3220] * HiveSqlDialect should transform the SQL-standard TRIM function to TRIM, diff --git a/core/src/test/java/org/apache/calcite/util/FormatModelTest.java b/core/src/test/java/org/apache/calcite/util/FormatModelTest.java new file mode 100644 index 0000000000..b313b652e0 --- /dev/null +++ b/core/src/test/java/org/apache/calcite/util/FormatModelTest.java @@ -0,0 +1,67 @@ +/* + * 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.util; + +import org.apache.calcite.util.format.FormatElement; +import org.apache.calcite.util.format.FormatModel; +import org.apache.calcite.util.format.FormatModels; + +import org.hamcrest.Matcher; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +/** + * Unit test for {@link FormatModel}. + */ +public class FormatModelTest { + + private void assertThatFormatElementParse(String formatString, + Matcher<List<String>> matcher) { + List<FormatElement> elements = FormatModels.BIG_QUERY.parse(formatString); + List<String> stringResults = new ArrayList<>(); + for (FormatElement element : elements) { + element.flatten(i -> stringResults.add(i.toString())); + } + assertThat(stringResults, matcher); + } + + @Test void testSingleElement() { + assertThatFormatElementParse("%j", is(Collections.singletonList("DDD"))); + } + + @Test void testMultipleElements() { + assertThatFormatElementParse("%b-%d-%Y", + is(Arrays.asList("MON", "-", "DD", "-", "YYYY"))); + } + + @Test void testArbitraryText() { + assertThatFormatElementParse("%jtext%b", + is(Arrays.asList("DDD", "text", "MON"))); + } + + @Test void testAliasText() { + assertThatFormatElementParse("%R", + is(Arrays.asList("HH24", ":", "MI"))); + } +} diff --git a/site/_docs/reference.md b/site/_docs/reference.md index cf30819fc2..9fef9858f1 100644 --- a/site/_docs/reference.md +++ b/site/_docs/reference.md @@ -2659,6 +2659,10 @@ BigQuery's type system uses confusingly different names for types and functions: | o | EXTRACT(xml, xpath, [, namespaces ]) | Returns the xml fragment of the element or elements matched by the XPath expression. The optional namespace value that specifies a default mapping or namespace mapping for prefixes, which is used when evaluating the XPath expression | o | EXISTSNODE(xml, xpath, [, namespaces ]) | Determines whether traversal of a XML document using a specified xpath results in any nodes. Returns 0 if no nodes remain after applying the XPath traversal on the document fragment of the element or elements matched by the XPath expression. Returns 1 if any nodes remain. The optional namespace value that specifies a default mapping or namespace mapping for prefixes, which is used when evaluating the XPath expression. | m | EXTRACTVALUE(xml, xpathExpr)) | Returns the text of the first text node which is a child of the element or elements matched by the XPath expression. +| b | FORMAT_DATE(string, date) | Formats *date* according to the specified format *string* +| b | FORMAT_DATETIME(string, timestamp) | Formats *timestamp* according to the specified format *string* +| b | FORMAT_TIME(string, time) | Formats *time* according to the specified format *string* +| b | FORMAT_TIMESTAMP(string timestamp) | Formats *timestamp* according to the specified format *string* | b o | GREATEST(expr [, expr ]*) | Returns the greatest of the expressions | b h s | IF(condition, value1, value2) | Returns *value1* if *condition* is TRUE, *value2* otherwise | b | IFNULL(value1, value2) | Equivalent to `NVL(value1, value2)` 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 45e2a65916..a51531284e 100644 --- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java +++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java @@ -8568,6 +8568,95 @@ public class SqlOperatorTest { "2014-12-29", "DATE NOT NULL"); } + @Test void testFormatTime() { + final SqlOperatorFixture f = fixture() + .withLibrary(SqlLibrary.BIG_QUERY) + .setFor(SqlLibraryOperators.FORMAT_TIME); + f.checkFails("^FORMAT_TIME('%x', timestamp '2008-12-25 15:30:00')^", + "Cannot apply 'FORMAT_TIME' to arguments of type " + + "'FORMAT_TIME\\(<CHAR\\(2\\)>, <TIMESTAMP\\(0\\)>\\)'\\. " + + "Supported form\\(s\\): " + + "'FORMAT_TIME\\(<CHARACTER>, <TIME>\\)'", + false); + f.checkScalar("FORMAT_TIME('%H', TIME '12:34:33')", + "12", + "VARCHAR(2000) NOT NULL"); + f.checkScalar("FORMAT_TIME('%R', TIME '12:34:33')", + "12:34", + "VARCHAR(2000) NOT NULL"); + f.checkScalar("FORMAT_TIME('The time is %M-%S', TIME '12:34:33')", + "The time is 34-33", + "VARCHAR(2000) NOT NULL"); + } + + @Test void testFormatDate() { + final SqlOperatorFixture f = fixture() + .withLibrary(SqlLibrary.BIG_QUERY) + .setFor(SqlLibraryOperators.FORMAT_DATE); + f.checkFails("^FORMAT_DATE('%x', 123)^", + "Cannot apply 'FORMAT_DATE' to arguments of type " + + "'FORMAT_DATE\\(<CHAR\\(2\\)>, <INTEGER>\\)'\\. " + + "Supported form\\(s\\): " + + "'FORMAT_DATE\\(<CHARACTER>, <DATE>\\)'", + false); + // Can implicitly cast TIMESTAMP to DATE + f.checkScalar("FORMAT_DATE('%x', timestamp '2008-12-25 15:30:00')", + "12/25/08", + "VARCHAR(2000) NOT NULL"); + f.checkScalar("FORMAT_DATE('%b-%d-%Y', DATE '2008-12-25')", + "Dec-25-2008", + "VARCHAR(2000) NOT NULL"); + f.checkScalar("FORMAT_DATE('%b %Y', DATE '2008-12-25')", + "Dec 2008", + "VARCHAR(2000) NOT NULL"); + f.checkScalar("FORMAT_DATE('%x', DATE '2008-12-25')", + "12/25/08", + "VARCHAR(2000) NOT NULL"); + f.checkScalar("FORMAT_DATE('The date is: %x', DATE '2008-12-25')", + "The date is: 12/25/08", + "VARCHAR(2000) NOT NULL"); + } + + @Test void testFormatTimestamp() { + final SqlOperatorFixture f = fixture() + .withLibrary(SqlLibrary.BIG_QUERY) + .setFor(SqlLibraryOperators.FORMAT_TIMESTAMP); + f.checkFails("^FORMAT_TIMESTAMP('%x', 123)^", + "Cannot apply 'FORMAT_TIMESTAMP' to arguments of type " + + "'FORMAT_TIMESTAMP\\(<CHAR\\(2\\)>, <INTEGER>\\)'\\. " + + "Supported form\\(s\\): " + + "FORMAT_TIMESTAMP\\(<CHARACTER>, " + + "<TIMESTAMP WITH LOCAL TIME ZONE>\\)\n" + + "FORMAT_TIMESTAMP\\(<CHARACTER>, " + + "<TIMESTAMP WITH LOCAL TIME ZONE>, <CHARACTER>\\)", + false); + f.checkScalar("FORMAT_TIMESTAMP('%c'," + + " TIMESTAMP WITH LOCAL TIME ZONE '2008-12-25 15:30:00')", + "Thu Dec 25 15:30:00 2008", + "VARCHAR(2000) NOT NULL"); + f.checkScalar("FORMAT_TIMESTAMP('%b-%d-%Y'," + + " TIMESTAMP WITH LOCAL TIME ZONE '2008-12-25 15:30:00')", + "Dec-25-2008", + "VARCHAR(2000) NOT NULL"); + f.checkScalar("FORMAT_TIMESTAMP('%b %Y'," + + " TIMESTAMP WITH LOCAL TIME ZONE '2008-12-25 15:30:00')", + "Dec 2008", + "VARCHAR(2000) NOT NULL"); + f.checkScalar("FORMAT_TIMESTAMP('%x'," + + " TIMESTAMP WITH LOCAL TIME ZONE '2008-12-25 15:30:00')", + "12/25/08", + "VARCHAR(2000) NOT NULL"); + f.checkScalar("FORMAT_TIMESTAMP('The time is: %R'," + + " TIMESTAMP WITH LOCAL TIME ZONE '2008-12-25 15:30:00')", + "The time is: 15:30", + "VARCHAR(2000) NOT NULL"); + f.checkScalar("FORMAT_TIMESTAMP('The time is: %R.%E2S'," + + " TIMESTAMP WITH LOCAL TIME ZONE '2008-12-25 15:30:00.1235456')", + "The time is: 15:30.123", + "VARCHAR(2000) NOT NULL"); + } + + @Test void testDenseRankFunc() { final SqlOperatorFixture f = fixture(); f.setFor(SqlStdOperatorTable.DENSE_RANK, VM_FENNEL, VM_JAVA);
