Skip to content

Commit

Permalink
Add UNIX epoch <-> TIMESTAMP conversion functions (#330)
Browse files Browse the repository at this point in the history
  • Loading branch information
alancai98 committed Jan 25, 2021
1 parent 0ac50a8 commit 0496038
Show file tree
Hide file tree
Showing 7 changed files with 300 additions and 1 deletion.
59 changes: 59 additions & 0 deletions docs/user/BuiltInFunctions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
```
4 changes: 4 additions & 0 deletions lang/src/org/partiql/lang/eval/ExprValueExtensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion lang/src/org/partiql/lang/eval/builtins/BuiltinFunctions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
35 changes: 35 additions & 0 deletions lang/src/org/partiql/lang/eval/builtins/FromUnixTimeFunction.kt
Original file line number Diff line number Diff line change
@@ -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>): ExprValue {
val unixTimestamp = args[0].bigDecimalValue()

val numMillis = unixTimestamp.times(millisPerSecond).stripTrailingZeros()

val timestamp = Timestamp.forMillis(numMillis, null)
return valueFactory.newTimestamp(timestamp)
}
}
45 changes: 45 additions & 0 deletions lang/src/org/partiql/lang/eval/builtins/UnixTimestampFunction.kt
Original file line number Diff line number Diff line change
@@ -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>): 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)
}
}
Original file line number Diff line number Diff line change
@@ -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<Any> = 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)
}
Original file line number Diff line number Diff line change
@@ -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<Any> = 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<Any> = 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)
}

0 comments on commit 0496038

Please sign in to comment.