Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UNIX epoch <-> TIMESTAMP conversion functions #330

Merged
merged 3 commits into from
Jan 25, 2021
Merged
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
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)
}