diff --git a/lang/src/org/partiql/lang/eval/EvaluatingCompiler.kt b/lang/src/org/partiql/lang/eval/EvaluatingCompiler.kt index 55899da1dc..b516cddbc7 100644 --- a/lang/src/org/partiql/lang/eval/EvaluatingCompiler.kt +++ b/lang/src/org/partiql/lang/eval/EvaluatingCompiler.kt @@ -28,7 +28,6 @@ import org.partiql.lang.eval.time.Time import org.partiql.lang.eval.visitors.PartiqlAstSanityValidator import org.partiql.lang.syntax.SqlParser import org.partiql.lang.util.* -import org.partiql.lang.util.DEFAULT_TIMEZONE_OFFSET import java.math.* import java.util.* import kotlin.collections.* @@ -811,7 +810,7 @@ internal class EvaluatingCompiler( val locationMeta = metas.sourceLocationMeta thunkFactory.thunkEnv(metas) { env -> val valueToCast = expThunk(env) - valueToCast.cast(dataType, valueFactory, locationMeta) + valueToCast.cast(dataType, valueFactory, locationMeta, env.session) } } } @@ -2010,7 +2009,7 @@ internal class EvaluatingCompiler( second, nano, precision, - if (with_time_zone && tz_minutes == null) DEFAULT_TIMEZONE_OFFSET.totalMinutes else tz_minutes + if (with_time_zone && tz_minutes == null) it.session.defaultTimezoneOffset.totalMinutes else tz_minutes ) ) } diff --git a/lang/src/org/partiql/lang/eval/EvaluationSession.kt b/lang/src/org/partiql/lang/eval/EvaluationSession.kt index e9406d3151..dcb0015b4b 100644 --- a/lang/src/org/partiql/lang/eval/EvaluationSession.kt +++ b/lang/src/org/partiql/lang/eval/EvaluationSession.kt @@ -15,6 +15,7 @@ package org.partiql.lang.eval import com.amazon.ion.* +import java.time.ZoneOffset /** * Evaluation Session. Holds user defined constants used during evaluation. Each value has a default value that can @@ -23,10 +24,12 @@ import com.amazon.ion.* * @property globals The global bindings. Defaults to [Bindings.empty] * @property parameters List of parameters to be substituted for positional placeholders * @property now Timestamp to consider as the current time, used by functions like `utcnow()` and `now()`. Defaults to [Timestamp.nowZ] + * @property defaultTimezoneOffset Default timezone offset to be used when TIME WITH TIME ZONE does not explicitily specify the time zone. Defaults to [ZoneOffset.UTC] */ class EvaluationSession private constructor(val globals: Bindings, val parameters: List, - val now: Timestamp) { + val now: Timestamp, + val defaultTimezoneOffset: ZoneOffset) { companion object { /** * Java style builder to construct a new [EvaluationSession]. Uses the default value for any non specified field @@ -68,8 +71,15 @@ class EvaluationSession private constructor(val globals: Bindings, return this } + private var defaultTimezoneOffset: ZoneOffset = ZoneOffset.UTC + fun defaultTimezoneOffset(value: ZoneOffset): Builder { + defaultTimezoneOffset = value + return this + } + fun build(): EvaluationSession = EvaluationSession(now = now ?: Timestamp.nowZ(), parameters = parameters, - globals = globals) + globals = globals, + defaultTimezoneOffset = defaultTimezoneOffset) } } diff --git a/lang/src/org/partiql/lang/eval/ExprValueExtensions.kt b/lang/src/org/partiql/lang/eval/ExprValueExtensions.kt index 48037f4cd2..0aae7c4e37 100644 --- a/lang/src/org/partiql/lang/eval/ExprValueExtensions.kt +++ b/lang/src/org/partiql/lang/eval/ExprValueExtensions.kt @@ -223,11 +223,13 @@ private val genericTimeRegex = Regex("\\d\\d:\\d\\d:\\d\\d(\\.\\d*)?([+|-]\\d\\d * * @param ion The ion system to synthesize values with. * @param targetDataType The target type to cast this value to. + * @param session The EvaluationSession which provides necessary information for evaluation. */ fun ExprValue.cast( targetDataType: DataType, valueFactory: ExprValueFactory, - locationMeta: SourceLocationMeta? + locationMeta: SourceLocationMeta?, + session: EvaluationSession ): ExprValue { val targetSqlDataType = targetDataType.sqlDataType @@ -362,7 +364,7 @@ fun ExprValue.cast( type == TIME -> { val time = timeValue() val timeZoneOffset = when (targetSqlDataType) { - SqlDataType.TIME_WITH_TIME_ZONE -> time.zoneOffset?: DEFAULT_TIMEZONE_OFFSET + SqlDataType.TIME_WITH_TIME_ZONE -> time.zoneOffset?: session.defaultTimezoneOffset else -> null } return valueFactory.newTime( @@ -405,7 +407,7 @@ fun ExprValue.cast( // Note that the [genericTimeRegex] has a group to extract the zone offset. val zoneOffsetString = matcher.group(2) - val zoneOffset = zoneOffsetString?.let { ZoneOffset.of(it) } ?: DEFAULT_TIMEZONE_OFFSET + val zoneOffset = zoneOffsetString?.let { ZoneOffset.of(it) } ?: session.defaultTimezoneOffset return valueFactory.newTime( Time.of( diff --git a/lang/src/org/partiql/lang/eval/time/TimeExtensions.kt b/lang/src/org/partiql/lang/eval/time/TimeExtensions.kt index 865d1587f6..788ea58801 100644 --- a/lang/src/org/partiql/lang/eval/time/TimeExtensions.kt +++ b/lang/src/org/partiql/lang/eval/time/TimeExtensions.kt @@ -18,14 +18,6 @@ internal val genericTimeRegex = Regex("\\d\\d:\\d\\d:\\d\\d(\\.\\d*)?([+|-]\\d\\ */ internal val DATE_PATTERN_REGEX = Regex("\\d\\d\\d\\d-\\d\\d-\\d\\d") -/** - * If the default timezone offset is not provided with [CompileOptions], it defaults to [ZoneOffset.UTC]. - * (The option to specify default timezone offset will be available once [#410](https://github.com/partiql/partiql-lang-kotlin/issues/410) is resolved) - * - * If timezone offset is not specified explicitly (when using `TIME WITH TIME ZONE`), the default time zone offset is used. - */ -internal val DEFAULT_TIMEZONE_OFFSET = ZoneOffset.UTC - /** * Returns the string representation of the [ZoneOffset] in HH:mm format. */ diff --git a/lang/test/org/partiql/lang/eval/EvaluatingCompilerCastTest.kt b/lang/test/org/partiql/lang/eval/EvaluatingCompilerCastTest.kt index 032f429097..5ed514c68e 100644 --- a/lang/test/org/partiql/lang/eval/EvaluatingCompilerCastTest.kt +++ b/lang/test/org/partiql/lang/eval/EvaluatingCompilerCastTest.kt @@ -21,14 +21,15 @@ import junitparams.Parameters import junitparams.naming.TestCaseName import org.junit.Test import org.partiql.lang.syntax.ParserException -import org.partiql.lang.util.DEFAULT_TIMEZONE_OFFSET import org.partiql.lang.util.getOffsetHHmm +import java.time.ZoneOffset class EvaluatingCompilerCastTest : EvaluatorTestBase() { private val allTypeNames = ExprValueType.values().flatMap { it.sqlTextNames } // cast as NULL is tested by castMissingAsNull fun parametersForCastMissing() = allTypeNames - "NULL" + @Test @Parameters @TestCaseName("CAST(MISSING AS {0})") @@ -47,6 +48,7 @@ class EvaluatingCompilerCastTest : EvaluatorTestBase() { // cast as MISSING is tested by castNullAsMissing fun parametersForCastNull() = allTypeNames - "MISSING" + @Test @Parameters @TestCaseName("CAST(NULL AS {0})") @@ -69,7 +71,13 @@ class EvaluatingCompilerCastTest : EvaluatorTestBase() { * @param expectedErrorCode The expected error code of any [EvaluationException] or `null` when no exception * is to be expected. */ - data class CastCase(val source: String, val type: String, val expected: String?, val expectedErrorCode: ErrorCode?) { + data class CastCase( + val source: String, + val type: String, + val expected: String?, + val expectedErrorCode: ErrorCode?, + val session: EvaluationSession = EvaluationSession.standard() + ) { val expression = "CAST($source AS $type)" override fun toString(): String = expression } @@ -84,6 +92,11 @@ class EvaluatingCompilerCastTest : EvaluatorTestBase() { */ fun case(source: String, type: String, expected: String) = CastCase(source, type, expected, null) + /** + * Function to create explicit CAST( to ) CastCase with EvaluationSession. + */ + fun case(source: String, type: String, expected: String, session: EvaluationSession) = CastCase(source, type, expected, null, session) + /** * Function to create explicit CAST( to ) CastCase throwing error. */ @@ -96,7 +109,7 @@ class EvaluatingCompilerCastTest : EvaluatorTestBase() { /** For each partial case, apply each of the given types to generate a concrete cast case. */ fun List<(String) -> CastCase>.types(types: List): List = - this.flatMap { partial -> types.map { type -> partial(type) } } + this.flatMap { partial -> types.map { type -> partial(type) } } fun parametersForCast() = listOf( @@ -345,7 +358,7 @@ class EvaluatingCompilerCastTest : EvaluatorTestBase() { // bag case("<<>>", EVALUATOR_INVALID_CAST) ).types(DATE.sqlTextNames), - // Find more coverage for the "Cast as Time" tests in `castDateAndTime`. + // Find more coverage for the "Cast as Time" tests in `castDateAndTime`. listOf( // booleans case("TRUE AND FALSE", EVALUATOR_INVALID_CAST), @@ -784,6 +797,10 @@ class EvaluatingCompilerCastTest : EvaluatorTestBase() { else -> assertEval(castCase.expression, castCase.expected) } + private val defaultTimezoneOffset = ZoneOffset.UTC + + private fun buildSession(hours: Int = 0, minutes: Int = 0) = EvaluationSession.build { defaultTimezoneOffset(ZoneOffset.ofHoursMinutes(hours, minutes)) } + fun parametersForCastDateAndTime() = listOf( listOf( case("DATE '2007-10-10'", "2007-10-10") @@ -797,21 +814,21 @@ class EvaluatingCompilerCastTest : EvaluatorTestBase() { listOf( // CAST(