From 04960382898d5fec3c0727a86d825c45be54279e Mon Sep 17 00:00:00 2001 From: Alan Cai Date: Mon, 25 Jan 2021 14:43:22 -0500 Subject: [PATCH] Add UNIX epoch <-> TIMESTAMP conversion functions (#330) --- docs/user/BuiltInFunctions.md | 59 ++++++++++++ .../partiql/lang/eval/ExprValueExtensions.kt | 4 + .../lang/eval/builtins/BuiltinFunctions.kt | 4 +- .../eval/builtins/FromUnixTimeFunction.kt | 35 ++++++++ .../eval/builtins/UnixTimestampFunction.kt | 45 ++++++++++ .../eval/builtins/FromUnixTimeFunctionTest.kt | 64 +++++++++++++ .../builtins/UnixTimestampFunctionTest.kt | 90 +++++++++++++++++++ 7 files changed, 300 insertions(+), 1 deletion(-) create mode 100644 lang/src/org/partiql/lang/eval/builtins/FromUnixTimeFunction.kt create mode 100644 lang/src/org/partiql/lang/eval/builtins/UnixTimestampFunction.kt create mode 100644 lang/test/org/partiql/lang/eval/builtins/FromUnixTimeFunctionTest.kt create mode 100644 lang/test/org/partiql/lang/eval/builtins/UnixTimestampFunctionTest.kt diff --git a/docs/user/BuiltInFunctions.md b/docs/user/BuiltInFunctions.md index 98c9f9cfcd..5408504719 100644 --- a/docs/user/BuiltInFunctions.md +++ b/docs/user/BuiltInFunctions.md @@ -764,3 +764,62 @@ Examples ```sql UTCNOW() -- 2017-10-13T16:02:11.123Z ``` + +### UNIX_TIMESTAMP + +With no `timestamp` argument, returns the number of seconds since the last epoch ('1970-01-01 00:00:00' UTC). + +With a `timestamp` argument, returns the number of seconds from the last epoch to the given `timestamp` +(possibly negative). + +Signature : `UNIX_TIMESTAMP: [Timestamp] -> Integer|Decimal` + +Header : `UNIX_TIMESTAMP([timestamp])` + +Purpose : `UNIX_TIMESTAMP()` called without a `timestamp` argument returns the number of whole seconds since the last +epoch ('1970-01-01 00:00:00' UTC) as an Integer using `UTCNOW`. + +`UNIX_TIMESTAMP()` called with a `timestamp` argument returns returns the number of seconds from the last epoch to the +`timestamp` argument. +If given a `timestamp` before the last epoch, `UNIX_TIMESTAMP` will return the number of seconds before the last epoch as a negative +number. +The return value will be a Decimal if and only if the given `timestamp` has a fractional seconds part. + +Examples : +```sql +UNIX_TIMESTAMP() -- 1507910531 (if current time is `2017-10-13T16:02:11Z`; # of seconds since last epoch as an Integer) +UNIX_TIMESTAMP(`2020T`) -- 1577836800 (seconds from 2020 to the last epoch as an Integer) +UNIX_TIMESTAMP(`2020-01T`) -- '' +UNIX_TIMESTAMP(`2020-01-01T`) -- '' +UNIX_TIMESTAMP(`2020-01-01T00:00Z`) -- '' +UNIX_TIMESTAMP(`2020-01-01T00:00:00Z`) -- '' +UNIX_TIMESTAMP(`2020-01-01T00:00:00.0Z`) -- 1577836800. (seconds from 2020 to the last epoch as a Decimal) +UNIX_TIMESTAMP(`2020-01-01T00:00:00.00Z`) -- '' +UNIX_TIMESTAMP(`2020-01-01T00:00:00.000Z`) -- '' +UNIX_TIMESTAMP(`2020-01-01T00:00:00.100Z`) -- 1577836800.1 +UNIX_TIMESTAMP(`1969T`) -- -31536000 (timestamp is before last epoch) +``` + +### FROM_UNIXTIME + +Converts the given unix epoch into a timestamp. + +Signature : `FROM_UNIXTIME: Integer|Decimal -> Timestamp` + +Header : `FROM_UNIXTIME(unix_timestamp)` + +Purpose : When given a non-negative numeric value, returns a timestamp after the last epoch. +When given a negative numeric value, returns a timestamp before the last epoch. +The returned timestamp has fractional seconds depending on if the value is a decimal. + +Examples : +```sql +FROM_UNIXTIME(-1) -- `1969-12-31T23:59:59-00:00` (negative unix_timestamp; returns timestamp before last epoch) +FROM_UNIXTIME(-0.1) -- `1969-12-31T23:59:59.9-00:00` (unix_timestamp is decimal so timestamp has fractional seconds) +FROM_UNIXTIME(0) -- `1970-01-01T00:00:00.000-00:00` +FROM_UNIXTIME(0.001) -- `1970-01-01T00:00:00.001-00:00` (decimal precision to fractional second precision) +FROM_UNIXTIME(0.01) -- `1970-01-01T00:00:00.01-00:00` +FROM_UNIXTIME(0.1) -- `1970-01-01T00:00:00.1-00:00` +FROM_UNIXTIME(1) -- `1970-01-01T00:00:01-00:00` +FROM_UNIXTIME(1577836800) -- `2020-01-01T00:00:00-00:00` (unix_timestamp is Integer so no fractional seconds) +``` diff --git a/lang/src/org/partiql/lang/eval/ExprValueExtensions.kt b/lang/src/org/partiql/lang/eval/ExprValueExtensions.kt index 84cb7d12ae..f5795ed501 100644 --- a/lang/src/org/partiql/lang/eval/ExprValueExtensions.kt +++ b/lang/src/org/partiql/lang/eval/ExprValueExtensions.kt @@ -100,6 +100,10 @@ internal fun ExprValue.datePartValue(): DatePart = internal fun ExprValue.intValue(): Int = this.numberValue().toInt() +internal fun ExprValue.longValue(): Long = this.numberValue().toLong() + +internal fun ExprValue.bigDecimalValue(): BigDecimal = this.numberValue().toString().toBigDecimal() + /** * Implements the `FROM` range operation. * Specifically, this is distinct from the normal [ExprValue.iterator] in that diff --git a/lang/src/org/partiql/lang/eval/builtins/BuiltinFunctions.kt b/lang/src/org/partiql/lang/eval/builtins/BuiltinFunctions.kt index 34028c4f53..1aacd26993 100644 --- a/lang/src/org/partiql/lang/eval/builtins/BuiltinFunctions.kt +++ b/lang/src/org/partiql/lang/eval/builtins/BuiltinFunctions.kt @@ -33,7 +33,9 @@ internal fun createBuiltinFunctions(valueFactory: ExprValueFactory) = TrimExprFunction(valueFactory), ToStringExprFunction(valueFactory), ToTimestampExprFunction(valueFactory), - SizeExprFunction(valueFactory)) + SizeExprFunction(valueFactory), + FromUnixTimeFunction(valueFactory), + UnixTimestampFunction(valueFactory)) internal fun createExists(valueFactory: ExprValueFactory): ExprFunction = object : ExprFunction { diff --git a/lang/src/org/partiql/lang/eval/builtins/FromUnixTimeFunction.kt b/lang/src/org/partiql/lang/eval/builtins/FromUnixTimeFunction.kt new file mode 100644 index 0000000000..5b5a96029e --- /dev/null +++ b/lang/src/org/partiql/lang/eval/builtins/FromUnixTimeFunction.kt @@ -0,0 +1,35 @@ +package org.partiql.lang.eval.builtins + +import com.amazon.ion.Timestamp +import org.partiql.lang.eval.Environment +import org.partiql.lang.eval.ExprValue +import org.partiql.lang.eval.ExprValueFactory +import org.partiql.lang.eval.NullPropagatingExprFunction +import org.partiql.lang.eval.bigDecimalValue +import java.math.BigDecimal + +/** + * Builtin function to convert the given unix epoch into a PartiQL `TIMESTAMP` [ExprValue]. A unix epoch represents + * the seconds since '1970-01-01 00:00:00' UTC. Largely based off MySQL's FROM_UNIXTIME. + * + * Syntax: `FROM_UNIXTIME(unix_timestamp)` + * Where unix_timestamp is a (potentially decimal) numeric value. If unix_timestamp is a decimal, the returned + * `TIMESTAMP` will have fractional seconds. If unix_timestamp is an integer, the returned `TIMESTAMP` will not have + * fractional seconds. + * + * When given a negative numeric value, this function returns a PartiQL `TIMESTAMP` [ExprValue] before the last epoch. + * When given a non-negative numeric value, this function returns a PartiQL `TIMESTAMP` [ExprValue] after the last + * epoch. + */ +internal class FromUnixTimeFunction(valueFactory: ExprValueFactory) : NullPropagatingExprFunction("from_unixtime", 1, valueFactory) { + private val millisPerSecond = BigDecimal(1000) + + override fun eval(env: Environment, args: List): ExprValue { + val unixTimestamp = args[0].bigDecimalValue() + + val numMillis = unixTimestamp.times(millisPerSecond).stripTrailingZeros() + + val timestamp = Timestamp.forMillis(numMillis, null) + return valueFactory.newTimestamp(timestamp) + } +} diff --git a/lang/src/org/partiql/lang/eval/builtins/UnixTimestampFunction.kt b/lang/src/org/partiql/lang/eval/builtins/UnixTimestampFunction.kt new file mode 100644 index 0000000000..978b3faf5d --- /dev/null +++ b/lang/src/org/partiql/lang/eval/builtins/UnixTimestampFunction.kt @@ -0,0 +1,45 @@ +package org.partiql.lang.eval.builtins + +import org.partiql.lang.eval.Environment +import org.partiql.lang.eval.ExprValue +import org.partiql.lang.eval.ExprValueFactory +import org.partiql.lang.eval.NullPropagatingExprFunction +import org.partiql.lang.eval.timestampValue +import java.math.BigDecimal + +/** + * Builtin function to convert the given PartiQL `TIMESTAMP` [ExprValue] into a unix epoch, where a unix epoch + * represents the seconds since '1970-01-01 00:00:00' UTC. Largely based off MySQL's UNIX_TIMESTAMP. + * + * Syntax: `UNIX_TIMESTAMP([timestamp])` + * + * If UNIX_TIMESTAMP() is called with no [timestamp] argument, it returns the number of whole seconds since + * '1970-01-01 00:00:00' UTC as a PartiQL `INT` [ExprValue] + * + * If UNIX_TIMESTAMP() is called with a [timestamp] argument, it returns the number of seconds from + * '1970-01-01 00:00:00' UTC to the given [timestamp] argument. If given a [timestamp] before the last epoch, will + * return the number of seconds before the last epoch as a negative number. The return value will be a decimal if and + * only if the given [timestamp] has a fractional seconds part. + * + * The valid range of argument values is the range of PartiQL's `TIMESTAMP` value. + */ +internal class UnixTimestampFunction(valueFactory: ExprValueFactory) : NullPropagatingExprFunction("unix_timestamp", 0..1, valueFactory) { + private val millisPerSecond = BigDecimal(1000) + + override fun eval(env: Environment, args: List): ExprValue { + val timestamp = if (args.isEmpty()) { + env.session.now + } else { + args[0].timestampValue() + } + + val numMillis = timestamp.decimalMillis + val epochTime = numMillis.divide(millisPerSecond) + + if (timestamp.decimalSecond.scale() == 0 || args.isEmpty()) { + return valueFactory.newInt(epochTime.toLong()) + } + + return valueFactory.newDecimal(epochTime) + } +} diff --git a/lang/test/org/partiql/lang/eval/builtins/FromUnixTimeFunctionTest.kt b/lang/test/org/partiql/lang/eval/builtins/FromUnixTimeFunctionTest.kt new file mode 100644 index 0000000000..b8741c9dc7 --- /dev/null +++ b/lang/test/org/partiql/lang/eval/builtins/FromUnixTimeFunctionTest.kt @@ -0,0 +1,64 @@ +package org.partiql.lang.eval.builtins + +import org.junit.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ArgumentsSource +import org.partiql.lang.errors.ErrorCode +import org.partiql.lang.errors.Property +import org.partiql.lang.eval.EvaluatorTestBase +import org.partiql.lang.util.ArgumentsProviderBase + +data class FromUnixTimeTestCase(val unixTimestamp: String, val expected: String) + +class FromUnixTimeFunctionTest : EvaluatorTestBase() { + private val testUnixTime = 1234567890 + + @Test + fun `from_unixtime 0 args`() = + checkInputThrowingEvaluationException( + "from_unixtime()", + ErrorCode.EVALUATOR_INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNC_CALL, + mapOf(Property.LINE_NUMBER to 1L, + Property.COLUMN_NUMBER to 1L, + Property.EXPECTED_ARITY_MIN to 1, + Property.EXPECTED_ARITY_MAX to 1)) + + @Test + fun `from_unixtime 2 args`() = + checkInputThrowingEvaluationException( + "from_unixtime($testUnixTime, $testUnixTime)", + ErrorCode.EVALUATOR_INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNC_CALL, + mapOf(Property.LINE_NUMBER to 1L, + Property.COLUMN_NUMBER to 1L, + Property.EXPECTED_ARITY_MIN to 1, + Property.EXPECTED_ARITY_MAX to 1)) + + @Test + fun `from_unixtime 3 args`() = + checkInputThrowingEvaluationException( + "from_unixtime($testUnixTime, $testUnixTime, $testUnixTime)", + ErrorCode.EVALUATOR_INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNC_CALL, + mapOf(Property.LINE_NUMBER to 1L, + Property.COLUMN_NUMBER to 1L, + Property.EXPECTED_ARITY_MIN to 1, + Property.EXPECTED_ARITY_MAX to 1)) + + + class FromUnixTimeTests : ArgumentsProviderBase() { + override fun getParameters(): List = listOf( + // negative unix epochs output timestamp before last epoch + FromUnixTimeTestCase("from_unixtime(-1)", "1969-12-31T23:59:59-00:00"), + FromUnixTimeTestCase("from_unixtime(-0.1)", "1969-12-31T23:59:59.9-00:00"), + // non-negative cases outputting a timestamp after last epoch + FromUnixTimeTestCase("from_unixtime(0)", "1970-01-01T00:00:00.000-00:00"), + FromUnixTimeTestCase("from_unixtime(0.001)", "1970-01-01T00:00:00.001-00:00"), + FromUnixTimeTestCase("from_unixtime(0.01)", "1970-01-01T00:00:00.01-00:00"), + FromUnixTimeTestCase("from_unixtime(0.1)", "1970-01-01T00:00:00.1-00:00"), + FromUnixTimeTestCase("from_unixtime(1)", "1970-01-01T00:00:01-00:00"), + FromUnixTimeTestCase("from_unixtime(1577836800)", "2020-01-01T00:00:00-00:00") + ) + } + @ParameterizedTest + @ArgumentsSource(FromUnixTimeTests::class) + fun runNoArgTests(tc: FromUnixTimeTestCase) = assertEval(tc.unixTimestamp, tc.expected) +} diff --git a/lang/test/org/partiql/lang/eval/builtins/UnixTimestampFunctionTest.kt b/lang/test/org/partiql/lang/eval/builtins/UnixTimestampFunctionTest.kt new file mode 100644 index 0000000000..21f026914d --- /dev/null +++ b/lang/test/org/partiql/lang/eval/builtins/UnixTimestampFunctionTest.kt @@ -0,0 +1,90 @@ +package org.partiql.lang.eval.builtins + +import com.amazon.ion.Timestamp +import org.junit.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ArgumentsSource +import org.partiql.lang.errors.ErrorCode +import org.partiql.lang.errors.Property +import org.partiql.lang.eval.EvaluationSession +import org.partiql.lang.eval.EvaluatorTestBase +import org.partiql.lang.util.ArgumentsProviderBase + +data class UnixTimestampNoArgTestCase(val numMillis: Long, val expected: String) +data class UnixTimestampOneArgTestCase(val timestamp: String, val expected: String) + +class UnixTimestampFunctionTest : EvaluatorTestBase() { + private val testTimestamp = "`2007-02-23T12:14Z`" + + @Test + fun `unix_timestamp 2 args`() = + checkInputThrowingEvaluationException( + "unix_timestamp($testTimestamp, $testTimestamp)", + ErrorCode.EVALUATOR_INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNC_CALL, + mapOf(Property.LINE_NUMBER to 1L, + Property.COLUMN_NUMBER to 1L, + Property.EXPECTED_ARITY_MIN to 0, + Property.EXPECTED_ARITY_MAX to 1)) + + @Test + fun `unix_timestamp 3 args`() = + checkInputThrowingEvaluationException( + "unix_timestamp($testTimestamp, $testTimestamp, $testTimestamp)", + ErrorCode.EVALUATOR_INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNC_CALL, + mapOf(Property.LINE_NUMBER to 1L, + Property.COLUMN_NUMBER to 1L, + Property.EXPECTED_ARITY_MIN to 0, + Property.EXPECTED_ARITY_MAX to 1)) + + + class NoArgsTests : ArgumentsProviderBase() { + override fun getParameters(): List = listOf( + // unix_timestamp no args, now = 0 + UnixTimestampNoArgTestCase(numMillis = 0, expected = "0"), + // nix_timestamp no args, now = 1ms + UnixTimestampNoArgTestCase(numMillis = 1, expected = "0"), + // unix_timestamp no args, now = 999ms + UnixTimestampNoArgTestCase(numMillis = 999, expected = "0"), + // unix_timestamp no args, now = 1s + UnixTimestampNoArgTestCase(numMillis = 1000, expected = "1"), + // unix_timestamp no args, now = 1001ms + UnixTimestampNoArgTestCase(numMillis = 1001, expected = "1") + ) + } + @ParameterizedTest + @ArgumentsSource(NoArgsTests::class) + fun runNoArgTests(tc: UnixTimestampNoArgTestCase) = + assertEval( + "unix_timestamp()", + tc.expected, + session = EvaluationSession.build { now(Timestamp.forMillis(tc.numMillis, 0)) }) + + + class OneArgTests : ArgumentsProviderBase() { + private val epoch2020 = "1577836800" + private val epoch2020Decimal = "1577836800." + + override fun getParameters(): List = listOf( + // time before the last epoch + UnixTimestampOneArgTestCase("unix_timestamp(`1969T`)", "-31536000"), + UnixTimestampOneArgTestCase("unix_timestamp(`1969-12-31T23:59:59.999Z`)", "-0.001"), + // exactly the last epoch + UnixTimestampOneArgTestCase("unix_timestamp(`1970T`)", "0"), + UnixTimestampOneArgTestCase("unix_timestamp(`1970-01-01T00:00:00.000Z`)", "0."), + // whole number unix epoch + UnixTimestampOneArgTestCase("unix_timestamp(`2020T`)", epoch2020), + UnixTimestampOneArgTestCase("unix_timestamp(`2020-01T`)", epoch2020), + UnixTimestampOneArgTestCase("unix_timestamp(`2020-01-01T`)", epoch2020), + UnixTimestampOneArgTestCase("unix_timestamp(`2020-01-01T00:00Z`)", epoch2020), + UnixTimestampOneArgTestCase("unix_timestamp(`2020-01-01T00:00:00Z`)", epoch2020), + // decimal unix epoch + UnixTimestampOneArgTestCase("unix_timestamp(`2020-01-01T00:00:00.0Z`)", epoch2020Decimal), + UnixTimestampOneArgTestCase("unix_timestamp(`2020-01-01T00:00:00.00Z`)", epoch2020Decimal), + UnixTimestampOneArgTestCase("unix_timestamp(`2020-01-01T00:00:00.000Z`)", epoch2020Decimal), + UnixTimestampOneArgTestCase("unix_timestamp(`2020-01-01T00:00:00.100Z`)", "1577836800.1") + ) + } + @ParameterizedTest + @ArgumentsSource(OneArgTests::class) + fun runOneArgTests(tc: UnixTimestampOneArgTestCase) = assertEval(tc.timestamp, tc.expected) +}