Skip to content

Commit 9ea166b

Browse files
committed
Timestamp expressions WIP
1 parent 2062c94 commit 9ea166b

File tree

5 files changed

+162
-21
lines changed

5 files changed

+162
-21
lines changed

firebase-firestore/src/main/java/com/google/firebase/firestore/model/Values.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,4 +731,8 @@ internal object Values {
731731
is VectorValue -> encodeValue(value)
732732
else -> throw IllegalArgumentException("Unexpected type: $value")
733733
}
734+
735+
@JvmStatic
736+
fun timestamp(seconds: Long, nanos: Int): Timestamp =
737+
Timestamp.newBuilder().setSeconds(seconds).setNanos(nanos).build()
734738
}

firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/EvaluateResult.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,11 @@ internal sealed class EvaluateResult(val value: Value?) {
1717
fun long(long: Long) = EvaluateResultValue(encodeValue(long))
1818
fun long(int: Int) = EvaluateResultValue(encodeValue(int.toLong()))
1919
fun string(string: String) = EvaluateResultValue(encodeValue(string))
20+
fun timestamp(timestamp: Timestamp): EvaluateResult =
21+
EvaluateResultValue(encodeValue(timestamp))
2022
fun timestamp(seconds: Long, nanos: Int): EvaluateResult =
2123
if (seconds !in -62_135_596_800 until 253_402_300_800) EvaluateResultError
22-
else
23-
EvaluateResultValue(
24-
encodeValue(Timestamp.newBuilder().setSeconds(seconds).setNanos(nanos).build())
25-
)
24+
else timestamp(Values.timestamp(seconds, nanos))
2625
}
2726
internal inline fun evaluateNonNull(f: (Value) -> EvaluateResult): EvaluateResult =
2827
if (value?.hasNullValue() == true) f(value) else this

firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/evaluation.kt

Lines changed: 120 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.google.firebase.firestore.pipeline
33
import com.google.common.math.LongMath
44
import com.google.common.math.LongMath.checkedAdd
55
import com.google.common.math.LongMath.checkedMultiply
6+
import com.google.common.math.LongMath.checkedSubtract
67
import com.google.firebase.firestore.UserDataReader
78
import com.google.firebase.firestore.model.MutableDocument
89
import com.google.firebase.firestore.model.Values
@@ -273,9 +274,71 @@ internal val evaluateStrJoin = notImplemented // TODO: Does not exist in express
273274

274275
// === Date / Timestamp Functions ===
275276

276-
internal val evaluateTimestampAdd = notImplemented
277+
private const val L_NANOS_PER_SECOND: Long = 1000_000_000
278+
private const val I_NANOS_PER_SECOND: Int = 1000_000_000
277279

278-
internal val evaluateTimestampSub = notImplemented
280+
private const val L_MICROS_PER_SECOND: Long = 1000_000
281+
private const val I_MICROS_PER_SECOND: Int = 1000_000
282+
283+
private const val L_MILLIS_PER_SECOND: Long = 1000
284+
private const val I_MILLIS_PER_SECOND: Int = 1000
285+
286+
internal fun plus(t: Timestamp, seconds: Long, nanos: Long): Timestamp =
287+
if (nanos == 0L) {
288+
plus(t, seconds)
289+
} else {
290+
val nanoSum = t.nanos + nanos // Overflow not possible since nanos is 0 to 1 000 000.
291+
val secondsSum: Long = checkedAdd(checkedAdd(t.seconds, seconds), nanoSum / L_NANOS_PER_SECOND)
292+
Values.timestamp(secondsSum, (nanoSum % I_NANOS_PER_SECOND).toInt())
293+
}
294+
295+
private fun plus(t: Timestamp, seconds: Long): Timestamp =
296+
if (seconds == 0L) t
297+
else Values.timestamp(checkedAdd(t.seconds, seconds), t.nanos)
298+
299+
internal fun minus(t: Timestamp, seconds: Long, nanos: Long): Timestamp =
300+
if (nanos == 0L) {
301+
minus(t, seconds)
302+
} else {
303+
val nanoSum = t.nanos - nanos // Overflow not possible since nanos is 0 to 1 000 000.
304+
val secondsSum: Long = checkedSubtract(t.seconds, checkedSubtract(seconds, nanoSum / L_NANOS_PER_SECOND))
305+
Values.timestamp(secondsSum, (nanoSum % I_NANOS_PER_SECOND).toInt())
306+
}
307+
308+
private fun minus(t: Timestamp, seconds: Long): Timestamp =
309+
if (seconds == 0L) t
310+
else Values.timestamp(checkedSubtract(t.seconds, seconds), t.nanos)
311+
312+
313+
internal val evaluateTimestampAdd =
314+
ternaryTimestampFunction { t: Timestamp, u: String, n: Long ->
315+
EvaluateResult.timestamp(
316+
when (u) {
317+
"microsecond" -> plus(t, n / L_MICROS_PER_SECOND, (n % L_MICROS_PER_SECOND) * 1000)
318+
"millisecond" -> plus(t, n / L_MILLIS_PER_SECOND, (n % L_MILLIS_PER_SECOND) * 1000_000)
319+
"second" -> plus(t, n)
320+
"minute" -> plus(t, checkedMultiply(n, 60))
321+
"hour" -> plus(t, checkedMultiply(n, 3600))
322+
"day" -> plus(t, checkedMultiply(n, 86400))
323+
else -> return@ternaryTimestampFunction EvaluateResultError
324+
}
325+
)
326+
}
327+
328+
internal val evaluateTimestampSub =
329+
ternaryTimestampFunction { t: Timestamp, u: String, n: Long ->
330+
EvaluateResult.timestamp(
331+
when (u) {
332+
"microsecond" -> minus(t, n / L_MICROS_PER_SECOND, (n % L_MICROS_PER_SECOND) * 1000)
333+
"millisecond" -> minus(t, n / L_MILLIS_PER_SECOND, (n % L_MILLIS_PER_SECOND) * 1000_000)
334+
"second" -> minus(t, n)
335+
"minute" -> minus(t, checkedMultiply(n, 60))
336+
"hour" -> minus(t, checkedMultiply(n, 3600))
337+
"day" -> minus(t, checkedMultiply(n, 86400))
338+
else -> return@ternaryTimestampFunction EvaluateResultError
339+
}
340+
)
341+
}
279342

280343
internal val evaluateTimestampTrunc = notImplemented // TODO: Does not exist in expressions.kt yet.
281344

@@ -284,39 +347,46 @@ internal val evaluateTimestampToUnixMicros = unaryFunction { t: Timestamp ->
284347
if (t.seconds < Long.MIN_VALUE / 1_000_000) {
285348
// To avoid overflow when very close to Long.MIN_VALUE, add 1 second, multiply, then subtract
286349
// again.
287-
val micros = checkedMultiply(t.seconds + 1, 1_000_000)
288-
val adjustment = t.nanos.toLong() / 1_000 - 1_000_000
350+
val micros = checkedMultiply(t.seconds + 1, L_MICROS_PER_SECOND)
351+
val adjustment = t.nanos.toLong() / L_MILLIS_PER_SECOND - L_MICROS_PER_SECOND
289352
checkedAdd(micros, adjustment)
290353
} else {
291-
val micros = checkedMultiply(t.seconds, 1_000_000)
292-
checkedAdd(micros, t.nanos.toLong() / 1_000)
354+
val micros = checkedMultiply(t.seconds, L_MICROS_PER_SECOND)
355+
checkedAdd(micros, t.nanos.toLong() / L_MILLIS_PER_SECOND)
293356
}
294357
)
295358
}
296359

297360
internal val evaluateTimestampToUnixMillis = unaryFunction { t: Timestamp ->
298361
EvaluateResult.long(
299362
if (t.seconds < 0 && t.nanos > 0) {
300-
val millis = checkedMultiply(t.seconds + 1, 1000)
301-
val adjustment = t.nanos.toLong() / 1000_000 - 1000
363+
val millis = checkedMultiply(t.seconds + 1, L_MILLIS_PER_SECOND)
364+
val adjustment = t.nanos.toLong() / L_MICROS_PER_SECOND - L_MILLIS_PER_SECOND
302365
checkedAdd(millis, adjustment)
303366
} else {
304-
val millis = checkedMultiply(t.seconds, 1000)
305-
checkedAdd(millis, t.nanos.toLong() / 1000_000)
367+
val millis = checkedMultiply(t.seconds, L_MILLIS_PER_SECOND)
368+
checkedAdd(millis, t.nanos.toLong() / L_MICROS_PER_SECOND)
306369
}
307370
)
308371
}
309372

310373
internal val evaluateTimestampToUnixSeconds = unaryFunction { t: Timestamp ->
311-
if (t.nanos !in 0 until 1_000_000_000) EvaluateResultError else EvaluateResult.long(t.seconds)
374+
if (t.nanos !in 0 until L_NANOS_PER_SECOND) EvaluateResultError
375+
else EvaluateResult.long(t.seconds)
312376
}
313377

314378
internal val evaluateUnixMicrosToTimestamp = unaryFunction { micros: Long ->
315-
EvaluateResult.timestamp(Math.floorDiv(micros, 1000_000), Math.floorMod(micros, 1000_000))
379+
EvaluateResult.timestamp(
380+
Math.floorDiv(micros, L_MICROS_PER_SECOND),
381+
Math.floorMod(micros, I_MICROS_PER_SECOND)
382+
)
316383
}
317384

318385
internal val evaluateUnixMillisToTimestamp = unaryFunction { millis: Long ->
319-
EvaluateResult.timestamp(Math.floorDiv(millis, 1000), Math.floorMod(millis, 1000))
386+
EvaluateResult.timestamp(
387+
Math.floorDiv(millis, L_MILLIS_PER_SECOND),
388+
Math.floorMod(millis, I_MILLIS_PER_SECOND)
389+
)
320390
}
321391

322392
internal val evaluateUnixSecondsToTimestamp = unaryFunction { seconds: Long ->
@@ -457,6 +527,43 @@ private inline fun binaryFunction(crossinline function: (String, String) -> Eval
457527
function
458528
)
459529

530+
private inline fun ternaryTimestampFunction(
531+
crossinline function: (Timestamp, String, Long) -> EvaluateResult
532+
): EvaluateFunction = ternaryNullableValueFunction { timestamp: Value, unit: Value, number: Value ->
533+
val t: Timestamp =
534+
when (timestamp.valueTypeCase) {
535+
Value.ValueTypeCase.NULL_VALUE -> return@ternaryNullableValueFunction EvaluateResult.NULL
536+
Value.ValueTypeCase.TIMESTAMP_VALUE -> timestamp.timestampValue
537+
else -> return@ternaryNullableValueFunction EvaluateResultError
538+
}
539+
val u: String =
540+
if (unit.hasStringValue()) unit.stringValue
541+
else return@ternaryNullableValueFunction EvaluateResultError
542+
val n: Long =
543+
when (number.valueTypeCase) {
544+
Value.ValueTypeCase.NULL_VALUE -> return@ternaryNullableValueFunction EvaluateResult.NULL
545+
Value.ValueTypeCase.INTEGER_VALUE -> number.integerValue
546+
else -> return@ternaryNullableValueFunction EvaluateResultError
547+
}
548+
function(t, u, n)
549+
}
550+
551+
private inline fun ternaryNullableValueFunction(
552+
crossinline function: (Value, Value, Value) -> EvaluateResult
553+
): EvaluateFunction = { params ->
554+
if (params.size != 3)
555+
throw Assert.fail("Function should have exactly 3 params, but %d were given.", params.size)
556+
val p1 = params[0]
557+
val p2 = params[1]
558+
val p3 = params[2]
559+
block@{ input: MutableDocument ->
560+
val v1 = p1(input).value ?: return@block EvaluateResultError
561+
val v2 = p2(input).value ?: return@block EvaluateResultError
562+
val v3 = p3(input).value ?: return@block EvaluateResultError
563+
catch { function(v1, v2, v3) }
564+
}
565+
}
566+
460567
private inline fun <T1, T2> binaryFunctionType(
461568
valueTypeCase1: Value.ValueTypeCase,
462569
crossinline valueExtractor1: (Value) -> T1,

firebase-firestore/src/main/java/com/google/firebase/firestore/pipeline/expressions.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -780,8 +780,7 @@ abstract class Expr internal constructor() {
780780
* @return A new [Expr] representing the addition operation.
781781
*/
782782
@JvmStatic
783-
fun add(first: Expr, second: Expr): Expr =
784-
FunctionExpr("add", evaluateAdd, first, second)
783+
fun add(first: Expr, second: Expr): Expr = FunctionExpr("add", evaluateAdd, first, second)
785784

786785
/**
787786
* Creates an expression that adds numeric expressions with a constant.
@@ -791,8 +790,7 @@ abstract class Expr internal constructor() {
791790
* @return A new [Expr] representing the addition operation.
792791
*/
793792
@JvmStatic
794-
fun add(first: Expr, second: Number): Expr =
795-
FunctionExpr("add", evaluateAdd, first, second)
793+
fun add(first: Expr, second: Number): Expr = FunctionExpr("add", evaluateAdd, first, second)
796794

797795
/**
798796
* Creates an expression that adds a numeric field with a numeric expression.

firebase-firestore/src/test/java/com/google/firebase/firestore/core/PipelineTests.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@ import com.google.common.truth.Truth.assertThat
44
import com.google.firebase.firestore.RealtimePipelineSource
55
import com.google.firebase.firestore.TestUtil
66
import com.google.firebase.firestore.model.MutableDocument
7+
import com.google.firebase.firestore.model.Values
78
import com.google.firebase.firestore.pipeline.Expr.Companion.field
9+
import com.google.firebase.firestore.pipeline.minus
10+
import com.google.firebase.firestore.pipeline.plus
811
import com.google.firebase.firestore.testutil.TestUtilKtx.doc
12+
import com.google.protobuf.Timestamp
913
import kotlinx.coroutines.flow.flowOf
1014
import kotlinx.coroutines.flow.toList
1115
import kotlinx.coroutines.runBlocking
@@ -26,4 +30,33 @@ internal class PipelineTests {
2630

2731
assertThat(list).hasSize(1)
2832
}
33+
34+
@Test
35+
fun xxx(): Unit = runBlocking {
36+
val zero: Timestamp = Values.timestamp(0, 0)
37+
38+
assertThat(plus(zero, 0, 0))
39+
.isEqualTo(zero)
40+
41+
assertThat(plus(Values.timestamp(1, 1), 1, 1))
42+
.isEqualTo(Values.timestamp(2, 2))
43+
44+
assertThat(plus(Values.timestamp(1, 1), 0, 1))
45+
.isEqualTo(Values.timestamp(1, 2))
46+
47+
assertThat(plus(Values.timestamp(1, 1), 1, 0))
48+
.isEqualTo(Values.timestamp(2, 1))
49+
50+
assertThat(minus(zero, 0, 0))
51+
.isEqualTo(zero)
52+
53+
assertThat(minus(Values.timestamp(1, 1), 1, 1))
54+
.isEqualTo(zero)
55+
56+
assertThat(minus(Values.timestamp(1, 1), 0, 1))
57+
.isEqualTo(Values.timestamp(1, 0))
58+
59+
assertThat(minus(Values.timestamp(1, 1), 1, 0))
60+
.isEqualTo(Values.timestamp(0, 1))
61+
}
2962
}

0 commit comments

Comments
 (0)