Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ public List<CallConverter> getCallConverters() {
callConverters.add(CallConverters.ROW);
callConverters.add(CallConverters.CAST.apply(typeConverter));
callConverters.add(CallConverters.REINTERPRET.apply(typeConverter));
callConverters.add(CallConverters.EXECUTION_CONTEXT_VARIABLE);
callConverters.add(new SqlArrayValueConstructorCallConverter(typeConverter));
callConverters.add(new SqlMapValueConstructorCallConverter());
callConverters.add(CallConverters.CREATE_SEARCH_CONV.apply(new RexBuilder(typeFactory)));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.substrait.isthmus.calcite;

import io.substrait.isthmus.AggregateFunctions;
import io.substrait.isthmus.expression.CurrentTimezoneFunction;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
Expand Down Expand Up @@ -49,6 +50,13 @@ public class SubstraitOperatorTable implements SqlOperatorTable {
.map(SqlOperator::getKind)
.collect(Collectors.toList()));

// Additional Substrait-specific scalar operators that have no standard Calcite equivalent (e.g.
// the session-timezone context variable). These are looked up by name, but deliberately do NOT
// feed OVERRIDE_KINDS: they share generic kinds such as OTHER_FUNCTION with many standard
// operators, which we must not shadow.
private static final SqlOperatorTable SUBSTRAIT_SCALAR_OPERATOR_TABLE =
SqlOperatorTables.of(List.of(CurrentTimezoneFunction.INSTANCE));

// Utilisation of extended library operators available from calcite 1.35+, i.e hyperbolic
// functions
private static final SqlOperatorTable LIBRARY_OPERATOR_TABLE =
Expand All @@ -64,13 +72,14 @@ public class SubstraitOperatorTable implements SqlOperatorTable {
private static final SqlOperatorTable STANDARD_OPERATOR_TABLE = SqlStdOperatorTable.instance();

private static final List<SqlOperator> OPERATOR_LIST =
Stream.concat(
Stream.of(
SUBSTRAIT_OPERATOR_TABLE.getOperatorList().stream(),
Stream.concat(
LIBRARY_OPERATOR_TABLE.getOperatorList().stream(),
// filter out the kinds that have been overriden from the standard operator table
STANDARD_OPERATOR_TABLE.getOperatorList().stream()
.filter(op -> !OVERRIDE_KINDS.contains(op.kind))))
SUBSTRAIT_SCALAR_OPERATOR_TABLE.getOperatorList().stream(),
LIBRARY_OPERATOR_TABLE.getOperatorList().stream(),
// filter out the kinds that have been overriden from the standard operator table
STANDARD_OPERATOR_TABLE.getOperatorList().stream()
.filter(op -> !OVERRIDE_KINDS.contains(op.kind)))
.flatMap(s -> s)
.collect(Collectors.toUnmodifiableList());

/** Private constructor. */
Expand Down Expand Up @@ -105,6 +114,12 @@ public void lookupOperatorOverloads(
return;
}

SUBSTRAIT_SCALAR_OPERATOR_TABLE.lookupOperatorOverloads(
opName, category, syntax, operatorList, nameMatcher);
if (!operatorList.isEmpty()) {
return;
}

LIBRARY_OPERATOR_TABLE.lookupOperatorOverloads(
opName, category, syntax, operatorList, nameMatcher);
if (!operatorList.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.apache.calcite.rex.RexProgram;
import org.apache.calcite.rex.RexUtil;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.jspecify.annotations.Nullable;

/**
Expand Down Expand Up @@ -236,6 +237,28 @@ else if (operand instanceof Expression.StructLiteral
}
};

/**
* Converts Calcite's niladic execution-context operators to Substrait execution context variable
* {@link Expression}s: {@link SqlStdOperatorTable#CURRENT_TIMESTAMP} to {@link
* Expression.CurrentTimestamp} (with the precision taken from the call's result type), {@link
* SqlStdOperatorTable#CURRENT_DATE} to {@link Expression.CurrentDate}, and {@link
* CurrentTimezoneFunction} to {@link Expression.CurrentTimezone}.
*
* <p>Matching is done on operator identity (these are niladic {@link SqlKind#OTHER_FUNCTION}
* functions with no dedicated {@link SqlKind}).
*/
public static SimpleCallConverter EXECUTION_CONTEXT_VARIABLE =
(call, visitor) -> {
if (call.getOperator() == SqlStdOperatorTable.CURRENT_TIMESTAMP) {
return ExpressionCreator.currentTimestamp(call.getType().getPrecision());
} else if (call.getOperator() == SqlStdOperatorTable.CURRENT_DATE) {
return ExpressionCreator.currentDate();
} else if (call.getOperator() == CurrentTimezoneFunction.INSTANCE) {
return ExpressionCreator.currentTimezone();
}
return null;
};

/**
* Returns the default set of converters for common calls.
*
Expand All @@ -249,6 +272,7 @@ public static List<CallConverter> defaults(TypeConverter typeConverter) {
CallConverters.ROW,
CallConverters.CAST.apply(typeConverter),
CallConverters.REINTERPRET.apply(typeConverter),
CallConverters.EXECUTION_CONTEXT_VARIABLE,
new SqlArrayValueConstructorCallConverter(typeConverter),
new SqlMapValueConstructorCallConverter());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.substrait.isthmus.expression;

import org.apache.calcite.sql.SqlFunctionCategory;
import org.apache.calcite.sql.fun.SqlBaseContextVariable;
import org.apache.calcite.sql.type.ReturnTypes;

/**
* Niladic Substrait-specific operator representing the current session timezone (Substrait {@code
* current_timezone}).
*
* <p>Calcite has no built-in operator for the session timezone, so this is modeled on Calcite's own
* string context variables such as {@code CURRENT_ROLE} / {@code CURRENT_USER} (see {@link
* org.apache.calcite.sql.fun.SqlStringContextVariable}). Being a {@link SqlBaseContextVariable} it
* is niladic, has {@link org.apache.calcite.sql.SqlSyntax#FUNCTION_ID} syntax and is a dynamic
* function (plans referencing it are never cached).
*/
public class CurrentTimezoneFunction extends SqlBaseContextVariable {

/** Singleton instance used by the Substrait ⇄ Calcite expression converters. */
public static final CurrentTimezoneFunction INSTANCE = new CurrentTimezoneFunction();

private CurrentTimezoneFunction() {
super("CURRENT_TIMEZONE", ReturnTypes.VARCHAR_2000, SqlFunctionCategory.SYSTEM);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,32 @@ public RexNode visit(Expression.DynamicParameter expr, Context context) throws R
return rexBuilder.makeDynamicParam(calciteType, expr.parameterReference());
}

@Override
public RexNode visit(Expression.CurrentTimestamp expr, Context context) throws RuntimeException {
// Substrait current_timestamp is a precision_timestamp_tz, so force the return type to the
// corresponding Calcite TIMESTAMP_WITH_LOCAL_TIME_ZONE(precision); Calcite's CURRENT_TIMESTAMP
// operator would otherwise infer a plain (timezone-less) TIMESTAMP.
RelDataType returnType = typeConverter.toCalcite(typeFactory, expr.getType());
return rexBuilder.makeCall(
returnType, SqlStdOperatorTable.CURRENT_TIMESTAMP, Collections.emptyList());
}

@Override
public RexNode visit(Expression.CurrentDate expr, Context context) throws RuntimeException {
RelDataType returnType = typeConverter.toCalcite(typeFactory, expr.getType());
return rexBuilder.makeCall(
returnType, SqlStdOperatorTable.CURRENT_DATE, Collections.emptyList());
}

@Override
public RexNode visit(Expression.CurrentTimezone expr, Context context) throws RuntimeException {
// Calcite has no built-in session-timezone operator; use the Substrait-specific niladic
// CurrentTimezoneFunction, forcing the (required) string return type.
RelDataType returnType = typeConverter.toCalcite(typeFactory, expr.getType());
return rexBuilder.makeCall(
returnType, CurrentTimezoneFunction.INSTANCE, Collections.emptyList());
}

/**
* Helper method to create a Calcite ROW expression for encoding UDT struct literals.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package io.substrait.isthmus;

import static org.junit.jupiter.api.Assertions.assertEquals;

import io.substrait.expression.Expression;
import io.substrait.expression.ExpressionCreator;
import io.substrait.extension.DefaultExtensionCatalog;
import io.substrait.extension.SimpleExtension;
import io.substrait.isthmus.SubstraitRelNodeConverter.Context;
import io.substrait.isthmus.expression.CurrentTimezoneFunction;
import io.substrait.isthmus.expression.ExpressionRexConverter;
import io.substrait.isthmus.expression.RexExpressionConverter;
import io.substrait.isthmus.expression.ScalarFunctionConverter;
import io.substrait.type.TypeCreator;
import java.util.Collections;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.junit.jupiter.api.Test;

/**
* Bi-directional conversion tests for the three Substrait execution context variables ({@code
* current_timestamp}, {@code current_date}, {@code current_timezone}) ⇄ Calcite.
*/
class ExecutionContextVariableConversionTest extends CalciteObjs {

protected static final SimpleExtension.ExtensionCollection EXTENSION_COLLECTION =
DefaultExtensionCatalog.DEFAULT_COLLECTION;

private final ScalarFunctionConverter scalarFunctionConverter =
new ScalarFunctionConverter(EXTENSION_COLLECTION.scalarFunctions(), type);

private final ExpressionRexConverter expressionRexConverter =
new ExpressionRexConverter(type, scalarFunctionConverter, null, TypeConverter.DEFAULT);

private final RexExpressionConverter rexExpressionConverter = new RexExpressionConverter();

@Test
void currentTimestamp() {
// Substrait current_timestamp is a precision_timestamp_tz -> Calcite
// TIMESTAMP_WITH_LOCAL_TIME_ZONE, carrying the fractional-second precision.
RelDataType returnType =
TypeConverter.DEFAULT.toCalcite(type, TypeCreator.REQUIRED.precisionTimestampTZ(6));
bitest(
ExpressionCreator.currentTimestamp(6),
rex.makeCall(returnType, SqlStdOperatorTable.CURRENT_TIMESTAMP, Collections.emptyList()));
}

@Test
void currentDate() {
RelDataType returnType = TypeConverter.DEFAULT.toCalcite(type, TypeCreator.REQUIRED.DATE);
bitest(
ExpressionCreator.currentDate(),
rex.makeCall(returnType, SqlStdOperatorTable.CURRENT_DATE, Collections.emptyList()));
}

@Test
void currentTimezone() {
// Calcite has no built-in session-timezone operator; the Substrait-specific
// CurrentTimezoneFunction is used instead.
RelDataType returnType = TypeConverter.DEFAULT.toCalcite(type, TypeCreator.REQUIRED.STRING);
bitest(
ExpressionCreator.currentTimezone(),
rex.makeCall(returnType, CurrentTimezoneFunction.INSTANCE, Collections.emptyList()));
}

// bi-directional test: 1) rex -> substrait, 2) substrait -> rex2, compare against expectations
void bitest(Expression expression, RexNode rexNode) {
assertEquals(expression, rexNode.accept(rexExpressionConverter));
RexNode convertedRex = expression.accept(expressionRexConverter, Context.newContext());
assertEquals(rexNode, convertedRex);
}
}
23 changes: 23 additions & 0 deletions isthmus/src/test/java/io/substrait/isthmus/Substrait2SqlTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertTrue;

import io.substrait.isthmus.utils.SetUtils;
import io.substrait.plan.Plan;
Expand Down Expand Up @@ -38,6 +39,28 @@ void simpleTest2() throws Exception {
assertFullRoundTrip(query);
}

@Test
void currentTimestamp() throws Exception {
assertFullRoundTrip("select current_timestamp from part");
}

@Test
void currentDate() throws Exception {
assertFullRoundTrip("select current_date from part");
}

@Test
void currentTimezone() throws Exception {
// CURRENT_TIMEZONE is a Substrait-specific niladic operator with no standard Calcite
// equivalent; it is registered in SubstraitOperatorTable so it parses without parentheses.
assertFullRoundTrip("select current_timezone from part");

// ...and it is emitted back as the bare niladic keyword (no dialect changes required).
Plan plan = toSubstraitPlan("select current_timezone from part", TPCH_CATALOG);
assertTrue(
toSql(plan).contains("CURRENT_TIMEZONE"), "expected CURRENT_TIMEZONE in emitted SQL");
}

@Test
void simpleTestDateInterval() throws Exception {
assertFullRoundTrip(
Expand Down
Loading