Skip to content

Commit 37e25e7

Browse files
committed
Implement and test realtime debug functions
1 parent 8c516fd commit 37e25e7

File tree

4 files changed

+183
-27
lines changed

4 files changed

+183
-27
lines changed

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

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@ import com.google.protobuf.Timestamp
77

88
internal sealed class EvaluateResult(val value: Value?) {
99
abstract val isError: Boolean
10-
val isSuccess: Boolean
11-
get() = this is EvaluateResultValue
12-
val isUnset: Boolean
13-
get() = this is EvaluateResultUnset
10+
abstract val isSuccess: Boolean
11+
abstract val isUnset: Boolean
1412

1513
companion object {
1614
val TRUE: EvaluateResultValue = EvaluateResultValue(Values.TRUE_VALUE)
@@ -33,14 +31,21 @@ internal sealed class EvaluateResult(val value: Value?) {
3331
}
3432
}
3533

34+
internal class EvaluateResultValue(value: Value) : EvaluateResult(value) {
35+
override val isSuccess: Boolean = true
36+
override val isError: Boolean = false
37+
override val isUnset: Boolean = false
38+
}
39+
3640
internal object EvaluateResultError : EvaluateResult(null) {
41+
override val isSuccess: Boolean = false
3742
override val isError: Boolean = true
43+
override val isUnset: Boolean = false
3844
}
3945

4046
internal object EvaluateResultUnset : EvaluateResult(null) {
47+
override val isSuccess: Boolean = false
4148
override val isError: Boolean = false
49+
override val isUnset: Boolean = true
4250
}
4351

44-
internal class EvaluateResultValue(value: Value) : EvaluateResult(value) {
45-
override val isError: Boolean = false
46-
}

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

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,21 @@ internal typealias EvaluateFunction = (params: List<EvaluateDocument>) -> Evalua
3333

3434
internal val notImplemented: EvaluateFunction = { _ -> throw NotImplementedError() }
3535

36+
// === Debug Functions ===
37+
38+
internal val evaluateIsError: EvaluateFunction = unaryFunction { r: EvaluateResult ->
39+
EvaluateResult.boolean(r.isError)
40+
}
41+
3642
// === Logical Functions ===
3743

38-
internal val evaluateExists: EvaluateFunction = notImplemented
44+
internal val evaluateExists: EvaluateFunction = unaryFunction { r: EvaluateResult ->
45+
when (r) {
46+
EvaluateResultError -> r
47+
EvaluateResultUnset -> EvaluateResult.FALSE
48+
is EvaluateResultValue -> EvaluateResult.TRUE
49+
}
50+
}
3951

4052
internal val evaluateAnd: EvaluateFunction = { params ->
4153
fun(input: MutableDocument): EvaluateResult {
@@ -492,18 +504,22 @@ private inline fun catch(f: () -> EvaluateResult): EvaluateResult =
492504
EvaluateResultError
493505
}
494506

495-
@JvmName("unaryValueFunction")
496507
private inline fun unaryFunction(
497-
crossinline function: (Value) -> EvaluateResult
508+
crossinline function: (EvaluateResult) -> EvaluateResult
498509
): EvaluateFunction = { params ->
499510
if (params.size != 1)
500511
throw Assert.fail("Function should have exactly 1 params, but %d were given.", params.size)
501512
val p = params[0]
502-
block@{ input: MutableDocument ->
503-
val v = p(input).value ?: return@block EvaluateResultError
504-
if (v.hasNullValue()) return@block EvaluateResult.NULL
505-
catch { function(v) }
506-
}
513+
{ input: MutableDocument -> catch { function(p(input)) } }
514+
}
515+
516+
@JvmName("unaryValueFunction")
517+
private inline fun unaryFunction(
518+
crossinline function: (Value) -> EvaluateResult
519+
): EvaluateFunction = unaryFunction { r: EvaluateResult ->
520+
val v = r.value
521+
if (v === null) EvaluateResultError
522+
else if (v.hasNullValue()) EvaluateResult.NULL else function(v)
507523
}
508524

509525
@JvmName("unaryBooleanFunction")
@@ -563,17 +579,12 @@ private inline fun <T> unaryFunctionType(
563579
valueTypeCase: Value.ValueTypeCase,
564580
crossinline valueExtractor: (Value) -> T,
565581
crossinline function: (T) -> EvaluateResult
566-
): EvaluateFunction = { params ->
567-
if (params.size != 1)
568-
throw Assert.fail("Function should have exactly 1 params, but %d were given.", params.size)
569-
val p = params[0]
570-
block@{ input: MutableDocument ->
571-
val v = p(input).value ?: return@block EvaluateResultError
572-
when (v.valueTypeCase) {
573-
Value.ValueTypeCase.NULL_VALUE -> EvaluateResult.NULL
574-
valueTypeCase -> catch { function(valueExtractor(v)) }
575-
else -> EvaluateResultError
576-
}
582+
): EvaluateFunction = unaryFunction { r: EvaluateResult ->
583+
val v = r.value
584+
if (v === null) EvaluateResultError else when (v.valueTypeCase) {
585+
Value.ValueTypeCase.NULL_VALUE -> EvaluateResult.NULL
586+
valueTypeCase -> catch { function(valueExtractor(v)) }
587+
else -> EvaluateResultError
577588
}
578589
}
579590

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import com.google.firebase.firestore.model.FieldPath as ModelFieldPath
2727
import com.google.firebase.firestore.model.MutableDocument
2828
import com.google.firebase.firestore.model.Values
2929
import com.google.firebase.firestore.model.Values.encodeValue
30+
import com.google.firebase.firestore.pipeline.Expr.Companion
3031
import com.google.firebase.firestore.pipeline.Expr.Companion.field
3132
import com.google.firebase.firestore.util.CustomClassMapper
3233
import com.google.firestore.v1.MapValue
@@ -2979,6 +2980,14 @@ abstract class Expr internal constructor() {
29792980
fun ifError(tryExpr: BooleanExpr, catchExpr: BooleanExpr): BooleanExpr =
29802981
BooleanExpr("if_error", notImplemented, tryExpr, catchExpr)
29812982

2983+
/**
2984+
* Creates an expression that checks if a given expression produces an error.
2985+
*
2986+
* @param expr The expression to check.
2987+
* @return A new [BooleanExpr] representing the `isError` check.
2988+
*/
2989+
@JvmStatic fun isError(expr: Expr): BooleanExpr = BooleanExpr("is_error", evaluateIsError, expr)
2990+
29822991
/**
29832992
* Creates an expression that returns the [catchValue] argument if there is an error, else
29842993
* return the result of the [tryExpr] argument evaluation.
@@ -4082,6 +4091,13 @@ abstract class Expr internal constructor() {
40824091
*/
40834092
fun ifError(catchValue: Any): Expr = Companion.ifError(this, catchValue)
40844093

4094+
/**
4095+
* Creates an expression that checks if this expression produces an error.
4096+
*
4097+
* @return A new [BooleanExpr] representing the `isError` check.
4098+
*/
4099+
fun isError(): BooleanExpr = Companion.isError(this)
4100+
40854101
internal abstract fun toProto(userDataReader: UserDataReader): Value
40864102

40874103
internal abstract fun evaluateContext(context: EvaluationContext): EvaluateDocument
@@ -4145,7 +4161,7 @@ class Field internal constructor(private val fieldPath: ModelFieldPath) : Select
41454161

41464162
private fun evaluateInternal(input: MutableDocument): EvaluateResult {
41474163
val value: Value? = input.getField(fieldPath)
4148-
return if (value == null) EvaluateResultUnset else EvaluateResultValue(value)
4164+
return if (value === null) EvaluateResultUnset else EvaluateResultValue(value)
41494165
}
41504166
}
41514167

@@ -4311,6 +4327,18 @@ internal constructor(name: String, function: EvaluateFunction, params: Array<out
43114327
* @return A new [BooleanExpr] representing the not operation.
43124328
*/
43134329
fun not(): BooleanExpr = Expr.Companion.not(this)
4330+
4331+
/**
4332+
* Creates an expression that returns the [catchExpr] argument if there is an error, else return
4333+
* the result of this expression.
4334+
*
4335+
* This overload will return [BooleanExpr] because the [catchExpr] is a [BooleanExpr].
4336+
*
4337+
* @param catchExpr The catch expression that will be evaluated and returned if the this
4338+
* expression produces an error.
4339+
* @return A new [BooleanExpr] representing the ifError operation.
4340+
*/
4341+
fun ifError(catchExpr: BooleanExpr): BooleanExpr = Expr.Companion.ifError(this, catchExpr)
43144342
}
43154343

43164344
/**
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package com.google.firebase.firestore.pipeline
2+
3+
import com.google.firebase.firestore.pipeline.Expr.Companion.array
4+
import com.google.firebase.firestore.pipeline.Expr.Companion.arrayLength
5+
import com.google.firebase.firestore.pipeline.Expr.Companion.constant
6+
import com.google.firebase.firestore.pipeline.Expr.Companion.exists
7+
import com.google.firebase.firestore.pipeline.Expr.Companion.field
8+
import com.google.firebase.firestore.pipeline.Expr.Companion.isError
9+
import com.google.firebase.firestore.pipeline.Expr.Companion.map
10+
import com.google.firebase.firestore.pipeline.Expr.Companion.not
11+
import com.google.firebase.firestore.pipeline.Expr.Companion.nullValue
12+
import com.google.firebase.firestore.testutil.TestUtil
13+
import com.google.firebase.firestore.testutil.TestUtil.doc
14+
import org.junit.Test
15+
import org.junit.runner.RunWith
16+
import org.robolectric.RobolectricTestRunner
17+
18+
@RunWith(RobolectricTestRunner::class)
19+
class DebugTests {
20+
21+
// --- Exists Tests ---
22+
23+
@Test
24+
fun `valid field returns true for exists`() {
25+
val existsExpr = exists(field("x"))
26+
val doc = doc("coll/doc1", 1, mapOf("x" to 1))
27+
assertEvaluatesTo(evaluate(existsExpr, doc), true, "exists(existent-field))")
28+
}
29+
30+
@Test
31+
fun `anything but unset returns true for exists`() {
32+
ComparisonTestData.allSupportedComparableValues.forEach { valueExpr ->
33+
assertEvaluatesTo(evaluate(exists(valueExpr)), true, "exists(%s)", valueExpr)
34+
}
35+
}
36+
37+
@Test
38+
fun `null returns true for exists`() {
39+
assertEvaluatesTo(evaluate(exists(nullValue())), true, "exists(null)")
40+
}
41+
42+
@Test
43+
fun `error returns error for exists`() {
44+
val errorProducingExpr = arrayLength(constant("notAnArray"))
45+
assertEvaluatesToError(evaluate(exists(errorProducingExpr)), "exists(error_expr)")
46+
}
47+
48+
@Test
49+
fun `unset with not exists returns true`() {
50+
val unsetExpr = field("non-existent-field")
51+
val existsExpr = exists(unsetExpr)
52+
assertEvaluatesTo(evaluate(not(existsExpr)), true, "not(exists(non-existent-field))")
53+
}
54+
55+
@Test
56+
fun `unset returns false for exists`() {
57+
val unsetExpr = field("non-existent-field")
58+
assertEvaluatesTo(evaluate(exists(unsetExpr)), false, "exists(non-existent-field)")
59+
}
60+
61+
@Test
62+
fun `empty array returns true for exists`() {
63+
assertEvaluatesTo(evaluate(exists(array())), true, "exists([])")
64+
}
65+
66+
@Test
67+
fun `empty map returns true for exists`() {
68+
// Expr.map() creates an empty map expression
69+
assertEvaluatesTo(evaluate(exists(map(emptyMap()))), true, "exists({})")
70+
}
71+
72+
// --- IsError Tests ---
73+
74+
@Test
75+
fun `isError error returns true`() {
76+
val errorProducingExpr = arrayLength(constant("notAnArray"))
77+
assertEvaluatesTo(evaluate(isError(errorProducingExpr)), true, "isError(error_expr)")
78+
}
79+
80+
@Test
81+
fun `isError field missing returns false`() {
82+
// Evaluating a missing field results in UNSET. isError(UNSET) should be false.
83+
val fieldExpr = field("target")
84+
assertEvaluatesTo(evaluate(isError(fieldExpr)), false, "isError(missing_field)")
85+
}
86+
87+
@Test
88+
fun `isError non-error returns false`() {
89+
assertEvaluatesTo(evaluate(isError(constant(42L))), false, "isError(42L)")
90+
}
91+
92+
@Test
93+
fun `isError explicit null returns false`() {
94+
assertEvaluatesTo(evaluate(isError(nullValue())), false, "isError(null)")
95+
}
96+
97+
@Test
98+
fun `isError unset returns false`() {
99+
// Evaluating a non-existent field results in UNSET. isError(UNSET) should be false.
100+
val unsetExpr = field("non-existent-field")
101+
assertEvaluatesTo(evaluate(isError(unsetExpr)), false, "isError(non-existent-field)")
102+
}
103+
104+
@Test
105+
fun `isError anything but error returns false`() {
106+
ComparisonTestData.allSupportedComparableValues.forEach { valueExpr ->
107+
assertEvaluatesTo(evaluate(isError(valueExpr)), false, "isError(%s)", valueExpr)
108+
}
109+
assertEvaluatesTo(evaluate(isError(nullValue())), false, "isError(null)")
110+
assertEvaluatesTo(evaluate(isError(constant(0L))), false, "isError(0L)")
111+
}
112+
}

0 commit comments

Comments
 (0)