diff --git a/core/src/main/java/io/substrait/dsl/SubstraitBuilder.java b/core/src/main/java/io/substrait/dsl/SubstraitBuilder.java index 9b26c6a2d..d0c842ed6 100644 --- a/core/src/main/java/io/substrait/dsl/SubstraitBuilder.java +++ b/core/src/main/java/io/substrait/dsl/SubstraitBuilder.java @@ -944,6 +944,34 @@ public Expression.StrLiteral str(String s) { return Expression.StrLiteral.builder().value(s).build(); } + /** + * Creates a {@code CURRENT_TIMESTAMP} execution context variable expression. + * + * @param precision the fractional-second precision of the timestamp + * @return a new {@link Expression.CurrentTimestamp} + */ + public Expression.CurrentTimestamp currentTimestamp(int precision) { + return Expression.CurrentTimestamp.builder().precision(precision).build(); + } + + /** + * Creates a {@code CURRENT_TIMEZONE} execution context variable expression. + * + * @return a new {@link Expression.CurrentTimezone} + */ + public Expression.CurrentTimezone currentTimezone() { + return Expression.CurrentTimezone.builder().build(); + } + + /** + * Creates a {@code CURRENT_DATE} execution context variable expression. + * + * @return a new {@link Expression.CurrentDate} + */ + public Expression.CurrentDate currentDate() { + return Expression.CurrentDate.builder().build(); + } + /** * Creates a cast expression that converts an expression to a different type. * diff --git a/core/src/main/java/io/substrait/expression/AbstractExpressionVisitor.java b/core/src/main/java/io/substrait/expression/AbstractExpressionVisitor.java index 3914587f3..1c1d8a926 100644 --- a/core/src/main/java/io/substrait/expression/AbstractExpressionVisitor.java +++ b/core/src/main/java/io/substrait/expression/AbstractExpressionVisitor.java @@ -616,4 +616,43 @@ public O visit(Expression.InPredicate expr, C context) throws E { public O visit(Expression.DynamicParameter expr, C context) throws E { return visitFallback(expr, context); } + + /** + * Visits a current timestamp execution context variable expression. + * + * @param expr the current timestamp expression + * @param context the visitation context + * @return the visit result + * @throws E if visitation fails + */ + @Override + public O visit(Expression.CurrentTimestamp expr, C context) throws E { + return visitFallback(expr, context); + } + + /** + * Visits a current timezone execution context variable expression. + * + * @param expr the current timezone expression + * @param context the visitation context + * @return the visit result + * @throws E if visitation fails + */ + @Override + public O visit(Expression.CurrentTimezone expr, C context) throws E { + return visitFallback(expr, context); + } + + /** + * Visits a current date execution context variable expression. + * + * @param expr the current date expression + * @param context the visitation context + * @return the visit result + * @throws E if visitation fails + */ + @Override + public O visit(Expression.CurrentDate expr, C context) throws E { + return visitFallback(expr, context); + } } diff --git a/core/src/main/java/io/substrait/expression/Expression.java b/core/src/main/java/io/substrait/expression/Expression.java index 5641a3758..0d4d88853 100644 --- a/core/src/main/java/io/substrait/expression/Expression.java +++ b/core/src/main/java/io/substrait/expression/Expression.java @@ -2113,6 +2113,112 @@ public R accept( } } + /** + * Marker interface for execution-context-dependent variables such as {@code CURRENT_TIMESTAMP}, + * {@code CURRENT_TIMEZONE}, and {@code CURRENT_DATE}. + * + *

The Substrait spec models these as expressions rather than functions because they take no + * input arguments, depend on the execution context rather than input data, and require evaluation + * mode control (see {@link io.substrait.plan.Plan.ExecutionBehavior}). The result type of an + * execution context variable always has {@code NULLABILITY_REQUIRED} nullability. + */ + interface ExecutionContextVariable extends Expression {} + + /** + * Execution context variable holding the current timestamp in the current session timezone. + * + *

Its result type is a {@code precision_timestamp_tz} with the configured {@link #precision()} + * and required nullability. + */ + @Value.Immutable + abstract class CurrentTimestamp implements ExecutionContextVariable { + /** + * Returns the fractional-second precision of the current timestamp, expressed as the number of + * digits after the decimal point (e.g. {@code 6} for microseconds). + * + * @return the timestamp precision + */ + public abstract int precision(); + + @Override + public Type getType() { + return TypeCreator.REQUIRED.precisionTimestampTZ(precision()); + } + + /** + * Creates a new builder for constructing a CurrentTimestamp. + * + * @return a new builder instance + */ + public static ImmutableExpression.CurrentTimestamp.Builder builder() { + return ImmutableExpression.CurrentTimestamp.builder(); + } + + @Override + public R accept( + ExpressionVisitor visitor, C context) throws E { + return visitor.visit(this, context); + } + } + + /** + * Execution context variable holding the current session timezone as a string defined by the IANA + * timezone database (https://www.iana.org/time-zones). + * + *

Its result type is a {@code string} with required nullability. + */ + @Value.Immutable + abstract class CurrentTimezone implements ExecutionContextVariable { + @Override + public Type getType() { + return TypeCreator.REQUIRED.STRING; + } + + /** + * Creates a new builder for constructing a CurrentTimezone. + * + * @return a new builder instance + */ + public static ImmutableExpression.CurrentTimezone.Builder builder() { + return ImmutableExpression.CurrentTimezone.builder(); + } + + @Override + public R accept( + ExpressionVisitor visitor, C context) throws E { + return visitor.visit(this, context); + } + } + + /** + * Execution context variable holding the current date. + * + *

Its result type is a {@code date} with required nullability. + */ + @Value.Immutable + abstract class CurrentDate implements ExecutionContextVariable { + @Override + public Type getType() { + return TypeCreator.REQUIRED.DATE; + } + + /** + * Creates a new builder for constructing a CurrentDate. + * + * @return a new builder instance + */ + public static ImmutableExpression.CurrentDate.Builder builder() { + return ImmutableExpression.CurrentDate.builder(); + } + + @Override + public R accept( + ExpressionVisitor visitor, C context) throws E { + return visitor.visit(this, context); + } + } + /** Defines the operation type for set predicates (EXISTS, UNIQUE). */ enum PredicateOp { /** Unspecified predicate operation. */ diff --git a/core/src/main/java/io/substrait/expression/ExpressionCreator.java b/core/src/main/java/io/substrait/expression/ExpressionCreator.java index 3738feed3..cc322f5fa 100644 --- a/core/src/main/java/io/substrait/expression/ExpressionCreator.java +++ b/core/src/main/java/io/substrait/expression/ExpressionCreator.java @@ -597,4 +597,32 @@ public static Expression cast( .failureBehavior(failureBehavior) .build(); } + + /** + * Creates a {@code CURRENT_TIMESTAMP} execution context variable. + * + * @param precision the fractional-second precision of the timestamp + * @return the current timestamp expression + */ + public static Expression.CurrentTimestamp currentTimestamp(int precision) { + return Expression.CurrentTimestamp.builder().precision(precision).build(); + } + + /** + * Creates a {@code CURRENT_TIMEZONE} execution context variable. + * + * @return the current timezone expression + */ + public static Expression.CurrentTimezone currentTimezone() { + return Expression.CurrentTimezone.builder().build(); + } + + /** + * Creates a {@code CURRENT_DATE} execution context variable. + * + * @return the current date expression + */ + public static Expression.CurrentDate currentDate() { + return Expression.CurrentDate.builder().build(); + } } diff --git a/core/src/main/java/io/substrait/expression/ExpressionVisitor.java b/core/src/main/java/io/substrait/expression/ExpressionVisitor.java index c0f97ee47..cb806436f 100644 --- a/core/src/main/java/io/substrait/expression/ExpressionVisitor.java +++ b/core/src/main/java/io/substrait/expression/ExpressionVisitor.java @@ -480,4 +480,34 @@ public interface ExpressionVisitor { private static final BoundConverter TO_BOUND_VISITOR = new BoundConverter(); diff --git a/core/src/main/java/io/substrait/expression/proto/ProtoExpressionConverter.java b/core/src/main/java/io/substrait/expression/proto/ProtoExpressionConverter.java index c96cd8768..abe7fae11 100644 --- a/core/src/main/java/io/substrait/expression/proto/ProtoExpressionConverter.java +++ b/core/src/main/java/io/substrait/expression/proto/ProtoExpressionConverter.java @@ -297,6 +297,25 @@ public Type visit(Type.Struct type) throws RuntimeException { .parameterReference(dp.getParameterReference()) .build(); } + case EXECUTION_CONTEXT_VARIABLE: + { + io.substrait.proto.Expression.ExecutionContextVariable ecv = + expr.getExecutionContextVariable(); + switch (ecv.getExecutionContextVariableTypeCase()) { + case CURRENT_TIMESTAMP: + return Expression.CurrentTimestamp.builder() + .precision(ecv.getCurrentTimestamp().getPrecision()) + .build(); + case CURRENT_TIMEZONE: + return Expression.CurrentTimezone.builder().build(); + case CURRENT_DATE: + return Expression.CurrentDate.builder().build(); + default: + throw new IllegalArgumentException( + "Unknown execution context variable type: " + + ecv.getExecutionContextVariableTypeCase()); + } + } // TODO enum. case ENUM: throw new UnsupportedOperationException("Unsupported type: " + expr.getRexTypeCase()); diff --git a/core/src/main/java/io/substrait/relation/ExpressionCopyOnWriteVisitor.java b/core/src/main/java/io/substrait/relation/ExpressionCopyOnWriteVisitor.java index 5bba4df2a..9f2b13b5a 100644 --- a/core/src/main/java/io/substrait/relation/ExpressionCopyOnWriteVisitor.java +++ b/core/src/main/java/io/substrait/relation/ExpressionCopyOnWriteVisitor.java @@ -461,6 +461,24 @@ public Optional visit( return Optional.empty(); } + @Override + public Optional visit( + Expression.CurrentTimestamp expr, EmptyVisitationContext context) throws E { + return Optional.empty(); + } + + @Override + public Optional visit(Expression.CurrentTimezone expr, EmptyVisitationContext context) + throws E { + return Optional.empty(); + } + + @Override + public Optional visit(Expression.CurrentDate expr, EmptyVisitationContext context) + throws E { + return Optional.empty(); + } + // utilities protected Optional> visitExprList( diff --git a/core/src/test/java/io/substrait/type/proto/ExecutionContextVariableRoundtripTest.java b/core/src/test/java/io/substrait/type/proto/ExecutionContextVariableRoundtripTest.java new file mode 100644 index 000000000..18d8b705a --- /dev/null +++ b/core/src/test/java/io/substrait/type/proto/ExecutionContextVariableRoundtripTest.java @@ -0,0 +1,43 @@ +package io.substrait.type.proto; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.substrait.TestBase; +import io.substrait.expression.Expression; +import io.substrait.type.TypeCreator; +import org.junit.jupiter.api.Test; + +class ExecutionContextVariableRoundtripTest extends TestBase { + + @Test + void currentTimestampMicros() { + Expression.CurrentTimestamp expr = Expression.CurrentTimestamp.builder().precision(6).build(); + + assertEquals(TypeCreator.REQUIRED.precisionTimestampTZ(6), expr.getType()); + verifyRoundTrip(expr); + } + + @Test + void currentTimestampSeconds() { + Expression.CurrentTimestamp expr = Expression.CurrentTimestamp.builder().precision(0).build(); + + assertEquals(TypeCreator.REQUIRED.precisionTimestampTZ(0), expr.getType()); + verifyRoundTrip(expr); + } + + @Test + void currentTimezone() { + Expression.CurrentTimezone expr = Expression.CurrentTimezone.builder().build(); + + assertEquals(TypeCreator.REQUIRED.STRING, expr.getType()); + verifyRoundTrip(expr); + } + + @Test + void currentDate() { + Expression.CurrentDate expr = Expression.CurrentDate.builder().build(); + + assertEquals(TypeCreator.REQUIRED.DATE, expr.getType()); + verifyRoundTrip(expr); + } +} diff --git a/examples/substrait-spark/src/main/java/io/substrait/examples/util/ExpressionStringify.java b/examples/substrait-spark/src/main/java/io/substrait/examples/util/ExpressionStringify.java index 1cb662b70..255cc2116 100644 --- a/examples/substrait-spark/src/main/java/io/substrait/examples/util/ExpressionStringify.java +++ b/examples/substrait-spark/src/main/java/io/substrait/examples/util/ExpressionStringify.java @@ -369,4 +369,22 @@ public String visit(Expression.DynamicParameter expr, EmptyVisitationContext con throws RuntimeException { return ""; } + + @Override + public String visit(Expression.CurrentTimestamp expr, EmptyVisitationContext context) + throws RuntimeException { + return ""; + } + + @Override + public String visit(Expression.CurrentTimezone expr, EmptyVisitationContext context) + throws RuntimeException { + return ""; + } + + @Override + public String visit(Expression.CurrentDate expr, EmptyVisitationContext context) + throws RuntimeException { + return ""; + } }