Skip to content

Commit

Permalink
[CALCITE-5180] Implement BigQuery Date/Time Type Aliases and Construc…
Browse files Browse the repository at this point in the history
…tors

Restore "DATE(string)" function (desugared to CAST)

Combine parsing of DATE/TIME/TIMESTAMP/DATETIME constructor
functions.

Use SqlBasicFunction for all overloads of DATE.
(Add OperandTypes.typeName to support.)

In SQL reference, use Calcite rather than BigQuery type
names: switch DATETIME to TIMESTAMP, TIMESTAMP to TIMESTAMP
WITH LOCAL TIME ZONE.

Close apache#3023
  • Loading branch information
julianhyde committed Jan 29, 2023
1 parent 076e660 commit e509cd6
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 108 deletions.
31 changes: 29 additions & 2 deletions babel/src/test/resources/sql/big-query.iq
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,8 @@ SELECT LENGTH("hello") as length;
#####################################################################
# DATE
#
# 0. DATE(string)
# Shorthand for 'CAST(string AS DATE)'
# 1. DATE(year, month, day)
# Constructs a DATE from INT64 values representing the year, month,
# and day.
Expand Down Expand Up @@ -1146,18 +1148,43 @@ select unix_date(datetime '2008-12-25') as d;
#####################################################################
# DATE
#
# 0. DATE(string)
# 1. DATE(year, month, day)
# 2. DATE(timestamp_expression[, time_zone])
# 3. DATE(datetime_expression)
#
# 0.
# 1. Constructs a DATE from INT64 values representing the year, month, and day.
# 1. Extracts the DATE from a TIMESTAMP expression. It supports an optional
# 2. Extracts the DATE from a TIMESTAMP expression. It supports an optional
# parameter to specify a time zone. If no time zone is specified, the default
# time zone, UTC, is used.
# 1. Extracts the DATE from a DATETIME expression.
# 3. Extracts the DATE from a DATETIME expression.
#
# Return Data Type: DATE

# 'date(x) is shorthand for 'cast(x as date)'
select date('1970-01-01') as d;
+------------+
| d |
+------------+
| 1970-01-01 |
+------------+
(1 row)

!ok

!if (false) {
select date(cast(null as varchar(10))) as d;
+---+
| D |
+---+
| |
+---+
(1 row)

!ok
!}

select date(2022, 11, 15) as d1,
date(datetime "2008-01-01 01:03:05") as d2,
date(datetime(2008, 1, 1, 1, 3, 5)) as d3;
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/codegen/default_config.fmpp
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ parser: {

# List of methods for parsing builtin function calls.
# Return type of method implementation should be "SqlNode".
# Example: "DateFunctionCall()".
# Example: "DateTimeConstructorCall()".
builtinFunctionCallMethods: [
]

Expand Down
86 changes: 5 additions & 81 deletions core/src/main/codegen/templates/Parser.jj
Original file line number Diff line number Diff line change
Expand Up @@ -4631,56 +4631,9 @@ SqlLiteral DateTimeLiteral() :
}
}

/**
* Parses BigQuery's built-in DATE() function.
*/
SqlNode DateFunctionCall() :
{
final SqlFunctionCategory funcType = SqlFunctionCategory.TIMEDATE;
final SqlIdentifier qualifiedName;
final Span s;
final SqlLiteral quantifier;
final List<? extends SqlNode> args;
}
{
<DATE> {
s = span();
qualifiedName = new SqlIdentifier(unquotedIdentifier(), getPos());
}
args = FunctionParameterList(ExprContext.ACCEPT_SUB_QUERY) {
quantifier = (SqlLiteral) args.get(0);
args.remove(0);
return createCall(qualifiedName, s.end(this), funcType, quantifier, args);
}
}

/**
* Parses BigQuery's built-in DATETIME() function.
*/
SqlNode DatetimeFunctionCall() :
{
final SqlFunctionCategory funcType = SqlFunctionCategory.TIMEDATE;
final SqlIdentifier qualifiedName;
final Span s;
final SqlLiteral quantifier;
final List<? extends SqlNode> args;
}
{
<DATETIME> {
s = span();
qualifiedName = new SqlIdentifier(unquotedIdentifier(), getPos());
}
args = FunctionParameterList(ExprContext.ACCEPT_SUB_QUERY) {
quantifier = (SqlLiteral) args.get(0);
args.remove(0);
return createCall(qualifiedName, s.end(this), funcType, quantifier, args);
}
}

/**
* Parses BigQuery's built-in TIMESTAMP() function.
*/
SqlNode TimestampFunctionCall() :
/** Parses a Date/Time constructor function, for example "DATE(1969, 7, 21)"
* or "DATETIME(d, t)". Enabled in some libraries (e.g. BigQuery). */
SqlNode DateTimeConstructorCall() :
{
final SqlFunctionCategory funcType = SqlFunctionCategory.TIMEDATE;
final SqlIdentifier qualifiedName;
Expand All @@ -4689,30 +4642,7 @@ SqlNode TimestampFunctionCall() :
final List<? extends SqlNode> args;
}
{
<TIMESTAMP> {
s = span();
qualifiedName = new SqlIdentifier(unquotedIdentifier(), getPos());
}
args = FunctionParameterList(ExprContext.ACCEPT_SUB_QUERY) {
quantifier = (SqlLiteral) args.get(0);
args.remove(0);
return createCall(qualifiedName, s.end(this), funcType, quantifier, args);
}
}

/**
* Parses BigQuery's built-in TIME() function.
*/
SqlNode TimeFunctionCall() :
{
final SqlFunctionCategory funcType = SqlFunctionCategory.TIMEDATE;
final SqlIdentifier qualifiedName;
final Span s;
final SqlLiteral quantifier;
final List<? extends SqlNode> args;
}
{
<TIME> {
(<DATE> | <TIME> | <DATETIME> | <TIMESTAMP>) {
s = span();
qualifiedName = new SqlIdentifier(unquotedIdentifier(), getPos());
}
Expand Down Expand Up @@ -6180,13 +6110,9 @@ SqlNode BuiltinFunctionCall() :
return SqlStdOperatorTable.TRIM.createCall(s.end(this), args);
}
|
node = DateFunctionCall() { return node; }
node = DateTimeConstructorCall() { return node; }
|
node = DateTruncFunctionCall() { return node; }
|
node = DatetimeFunctionCall() { return node; }
|
node = TimestampFunctionCall() { return node; }
|
node = TimestampAddFunctionCall() { return node; }
|
Expand All @@ -6197,8 +6123,6 @@ SqlNode BuiltinFunctionCall() :
node = TimestampTruncFunctionCall() { return node; }
|
node = TimeDiffFunctionCall() { return node; }
|
node = TimeFunctionCall() { return node; }
|
node = TimeTruncFunctionCall() { return node; }
|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -482,18 +482,28 @@ private SqlLibraryOperators() {
public static final SqlAggFunction MIN_BY =
SqlStdOperatorTable.ARG_MIN.withName("MIN_BY");

/** The "DATE(string)" function, equivalent to "CAST(string AS DATE). */
/** The "DATE(string)" function, equivalent to "CAST(string AS DATE);
* also "DATE(year, month, date)",
* "DATE(timestampLtz [, timeZone])",
* "DATE(timestamp)". */
@LibraryOperator(libraries = {BIG_QUERY})
public static final SqlFunction DATE =
SqlBasicFunction.create("DATE", ReturnTypes.DATE_NULLABLE,
OperandTypes.STRING, SqlFunctionCategory.TIMEDATE);


/**
* BigQuery's {@code DATE} function can take various operands.
* See {@link SqlDateFunction}.
*/
@LibraryOperator(libraries = {BIG_QUERY})
public static final SqlFunction DATE2 = new SqlDateFunction();
OperandTypes.or(
// DATE(string)
OperandTypes.STRING,
// DATE(year, month, date)
OperandTypes.family(SqlTypeFamily.INTEGER, SqlTypeFamily.INTEGER,
SqlTypeFamily.INTEGER),
// DATE(timestamp)
OperandTypes.TIMESTAMP_NTZ,
// DATE(timestampLtz)
OperandTypes.TIMESTAMP_LTZ,
// DATE(timestampLtz, timeZone)
OperandTypes.sequence(null,
OperandTypes.TIMESTAMP_LTZ,
OperandTypes.CHARACTER)),
SqlFunctionCategory.TIMEDATE);

/**
* BigQuery's {@code DATETIME} function can take various operands.
Expand Down
53 changes: 53 additions & 0 deletions core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@

import static org.apache.calcite.util.Static.RESOURCE;

import static java.util.Objects.requireNonNull;

/**
* Strategies for checking operand types.
*
Expand Down Expand Up @@ -130,6 +132,14 @@ public static FamilyOperandTypeChecker family(List<SqlTypeFamily> families) {
return family(families, i -> false);
}

/**
* Creates a single-operand checker that passes if the operand's type has a
* particular {@link SqlTypeName}.
*/
public static SqlSingleOperandTypeChecker typeName(SqlTypeName typeName) {
return new TypeNameChecker(typeName);
}

/**
* Creates a checker that passes if the operand is an interval appropriate for
* a given date/time type. For example, the time frame HOUR is appropriate for
Expand Down Expand Up @@ -439,6 +449,16 @@ public static SqlOperandTypeChecker variadic(
public static final SqlSingleOperandTypeChecker TIMESTAMP =
family(SqlTypeFamily.TIMESTAMP);

/** Type-checker that matches "TIMESTAMP WITH LOCAL TIME ZONE" but not other
* members of the "TIMESTAMP" family (e.g. "TIMESTAMP"). */
public static final SqlSingleOperandTypeChecker TIMESTAMP_LTZ =
new TypeNameChecker(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE);

/** Type-checker that matches "TIMESTAMP" but not other members of the
* "TIMESTAMP" family (e.g. "TIMESTAMP WITH LOCAL TIME ZONE"). */
public static final SqlSingleOperandTypeChecker TIMESTAMP_NTZ =
new TypeNameChecker(SqlTypeName.TIMESTAMP);

public static final SqlSingleOperandTypeChecker INTERVAL =
family(SqlTypeFamily.DATETIME_INTERVAL);

Expand Down Expand Up @@ -1022,4 +1042,37 @@ private static class PeriodOperandTypeChecker
"PERIOD (DATETIME, DATETIME)"));
}
}

/** Checker that passes if the operand's type has a particular
* {@link SqlTypeName}. */
private static class TypeNameChecker implements SqlSingleOperandTypeChecker,
ImplicitCastOperandTypeChecker {
final SqlTypeName typeName;

TypeNameChecker(SqlTypeName typeName) {
this.typeName = requireNonNull(typeName, "typeName");
}

@Override public boolean checkSingleOperandType(SqlCallBinding callBinding,
SqlNode operand, int iFormalOperand, boolean throwOnFailure) {
final RelDataType operandType =
callBinding.getValidator().getValidatedNodeType(operand);
return operandType.getSqlTypeName() == typeName;
}

@Override public boolean checkOperandTypesWithoutTypeCoercion(
SqlCallBinding callBinding, boolean throwOnFailure) {
// FIXME we assume that there is exactly one operand
return checkSingleOperandType(callBinding, callBinding.operand(0), 0,
throwOnFailure);
}

@Override public SqlTypeFamily getOperandSqlTypeFamily(int iFormalOperand) {
return requireNonNull(typeName.getFamily(), "family");
}

@Override public String getAllowedSignatures(SqlOperator op, String opName) {
return opName + "(" + typeName.getSpaceName() + ")";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,19 @@ private StandardConvertletTable() {
}
});

// DATE(string) is equivalent to CAST(string AS DATE),
// but other DATE variants are treated as regular functions.
registerOp(SqlLibraryOperators.DATE,
(cx, call) -> {
final RexCall e =
(RexCall) StandardConvertletTable.this.convertCall(cx, call);
if (e.getOperands().size() == 1
&& SqlTypeUtil.isString(e.getOperands().get(0).getType())) {
return cx.getRexBuilder().makeCast(e.type, e.getOperands().get(0));
}
return e;
});

registerOp(SqlLibraryOperators.LTRIM,
new TrimConvertlet(SqlTrimFunction.Flag.LEADING));
registerOp(SqlLibraryOperators.RTRIM,
Expand Down Expand Up @@ -345,8 +358,7 @@ private static RexNode convertNvl(SqlRexContext cx, SqlCall call) {
rexBuilder.makeCast(
cx.getTypeFactory()
.createTypeWithNullability(type, operand1.getType().isNullable()),
operand1)
));
operand1)));
}

/** Converts a call to the DECODE function. */
Expand Down
19 changes: 10 additions & 9 deletions site/_docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2613,8 +2613,8 @@ BigQuery's type system uses confusingly different names for types and functions:
Calcite's `TIMESTAMP` type;
* BigQuery's `TIMESTAMP` type represents an instant, and corresponds to
Calcite's `TIMESTAMP WITH LOCAL TIME ZONE` type;
* The *instant* parameter, for instance in `DATE(instant)`, has Calcite type
`TIMESTAMP WITH LOCAL TIME ZONE`;
* The *timestampLtz* parameter, for instance in `DATE(timestampLtz)`, has
Calcite type `TIMESTAMP WITH LOCAL TIME ZONE`;
* The `TIMESTAMP(string)` function, designed to be compatible the BigQuery
function, return a Calcite `TIMESTAMP WITH LOCAL TIME ZONE`;
* Similarly, `DATETIME(string)` returns a Calcite `TIMESTAMP`.
Expand All @@ -2635,17 +2635,18 @@ BigQuery's type system uses confusingly different names for types and functions:
| p | CONVERT_TIMEZONE(tz1, tz2, datetime) | Converts the timezone of *datetime* from *tz1* to *tz2*
| b | CURRENT_DATETIME([ timeZone ]) | Returns the current time as a TIMESTAMP from *timezone*
| m | DAYNAME(datetime) | Returns the name, in the connection's locale, of the weekday in *datetime*; for example, it returns '星期日' for both DATE '2020-02-10' and TIMESTAMP '2020-02-10 10:10:10'
| b | DATE(year, month, day) | Returns a DATE value for *year*, *month*, and *day* (all of type INTEGER)
| b | DATE(instant) | Extracts the DATE from *instant*, assuming UTC
| b | DATE(instant, timeZone) | Extracts the DATE from *instant*, in *timeZone*
| b | DATE(timestamp) | Extracts the DATE from a *timestamp*
| b | DATE(timestampLtz) | Extracts the DATE from *timestampLtz* (an instant; BigQuery's TIMESTAMP type), assuming UTC
| b | DATE(timestampLtz, timeZone) | Extracts the DATE from *timestampLtz* (an instant; BigQuery's TIMESTAMP type) in *timeZone*
| b | DATE(string) | Equivalent to `CAST(string AS DATE)`
| b | DATE(year, month, day) | Returns a DATE value for *year*, *month*, and *day* (all of type INTEGER)
| p q | DATEADD(timeUnit, integer, datetime) | Equivalent to `TIMESTAMPADD(timeUnit, integer, datetime)`
| p q | DATEDIFF(timeUnit, datetime, datetime2) | Equivalent to `TIMESTAMPDIFF(timeUnit, datetime, datetime2)`
| q | DATEPART(timeUnit, datetime) | Equivalent to `EXTRACT(timeUnit FROM datetime)`
| b | DATETIME(year, month, day, hour, minute, second) | Creates a TIMESTAMP for *year*, *month*, *day*, *hour*, *minute*, *second* (all of type INTEGER)
| b | DATETIME(date, time) | Converts *date* and *time* to a TIMESTAMP
| b | DATETIME(date) | Converts *date* to a TIMESTAMP value (at midnight)
| b | DATETIME(date, timeZone) | Converts *date* to a TIMESTAMP value (at midnight), in *timeZone*
| b | DATETIME(year, month, day, hour, minute, second) | Creates a TIMESTAMP for *year*, *month*, *day*, *hour*, *minute*, *second* (all of type INTEGER)
| b | DATE_FROM_UNIX_DATE(integer) | Returns the DATE that is *integer* days after 1970-01-01
| p | DATE_PART(timeUnit, datetime) | Equivalent to `EXTRACT(timeUnit FROM datetime)`
| b | DATE_SUB(date, interval) | Returns the DATE value that occurs *interval* before *date*
Expand Down Expand Up @@ -2699,9 +2700,9 @@ BigQuery's type system uses confusingly different names for types and functions:
| b m o p | SUBSTR(string, position [, substringLength ]) | Returns a portion of *string*, beginning at character *position*, *substringLength* characters long. SUBSTR calculates lengths using characters as defined by the input character set
| b o | TANH(numeric) | Returns the hyperbolic tangent of *numeric*
| b | TIME(hour, minute, second) | Returns a TIME value *hour*, *minute*, *second* (all of type INTEGER)
| b | TIME(timestamp) | Extracts the TIME from *timestamp*
| b | TIME(instant) | Extracts the TIME from *instant*, assuming UTC
| b | TIME(instant, timeZone) | Extracts the time from *instant*, in *timeZone*
| b | TIME(timestamp) | Extracts the TIME from *timestamp* (a local time; BigQuery's DATETIME type)
| b | TIME(instant) | Extracts the TIME from *timestampLtz* (an instant; BigQuery's TIMESTAMP type), assuming UTC
| b | TIME(instant, timeZone) | Extracts the time from *timestampLtz* (an instant; BigQuery's TIMESTAMP type), in *timeZone*
| b | TIMESTAMP(string) | Equivalent to `CAST(string AS TIMESTAMP WITH LOCAL TIME ZONE)`
| b | TIMESTAMP(string, timeZone) | Equivalent to `CAST(string AS TIMESTAMP WITH LOCAL TIME ZONE)`, converted to *timeZone*
| b | TIMESTAMP(date) | Converts *date* to a TIMESTAMP WITH LOCAL TIME ZONE value (at midnight)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ private static class AddSchemaPostProcessor
}
}

/** Post-processor that adds a {@link RelProtoDataType} and sets it as default. */
/** Post-processor that adds a type. */
private static class AddTypePostProcessor
implements CalciteAssert.ConnectionPostProcessor {
private final String name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5100,8 +5100,10 @@ private static void checkIf(SqlOperatorFixture f) {
f.checkNull("timestamp_micros(cast(null as bigint))");
f.checkScalar("date_from_unix_date(0)", "1970-01-01", "DATE NOT NULL");

f.checkNull("CAST(null AS DATE)");
f.checkScalar("CAST('1985-12-06' AS DATE)", "1985-12-06", "DATE NOT NULL");
// DATE is a reserved keyword, but the parser has special treatment to
// allow it as a function.
f.checkNull("DATE(null)");
f.checkScalar("DATE('1985-12-06')", "1985-12-06", "DATE NOT NULL");
f.checkType("CURRENT_DATETIME()", "TIMESTAMP(0) NOT NULL");
f.checkType("CURRENT_DATETIME('America/Los_Angeles')", "TIMESTAMP(0) NOT NULL");
f.checkType("CURRENT_DATETIME(CAST(NULL AS VARCHAR(20)))", "TIMESTAMP(0)");
Expand Down

0 comments on commit e509cd6

Please sign in to comment.