Skip to content

Commit 6b1fa2f

Browse files
authored
Fix corner cases when creating subscriptions (#1471)
1 parent e8bff87 commit 6b1fa2f

File tree

9 files changed

+73
-24
lines changed

9 files changed

+73
-24
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
### Fixed
1010
* Rare corruption causing 'Invalid streaming format cookie'-exception. Typically following compact, convert or copying to a new file. (Issue [#1440](https://github.com/realm/realm-kotlin/issues/1440))
1111
* Compiler error when using Kotlin 1.9.0 and backlinks. (Issue [#1469](https://github.com/realm/realm-kotlin/issues/1469))
12+
* [Sync] Changing a subscriptions query type or query itself will now trigger the `WaitForSync.FIRST_TIME` behaviour, rather than only checking changes to the name. (Issues [#1466](https://github.com/realm/realm-kotlin/issues/1466))
1213

1314
### Compatibility
1415
* File format: Generates Realms with file format v23.

packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/ext/RealmQueryExt.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ import kotlin.time.Duration
6060
* depend on which [mode] was used.
6161
* @throws kotlinx.coroutines.TimeoutCancellationException if the specified timeout was hit before
6262
* a query result could be returned.
63-
* @Throws IllegalStateException if this method is called on a Realm that isn't using Flexible Sync.
63+
* @throws IllegalStateException if this method is called on a Realm that isn't using Flexible Sync.
64+
* @throws io.realm.kotlin.mongodb.exceptions.BadFlexibleSyncQueryException if the server did not
65+
* accept the set of queries. The exact reason is found in the exception message.
6466
*/
6567
@ExperimentalFlexibleSyncApi
6668
public suspend fun <T : RealmObject> RealmQuery<T>.subscribe(

packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/ext/RealmResultsExt.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@ import kotlin.time.Duration
6262
* depend on which [mode] was used.
6363
* @throws kotlinx.coroutines.TimeoutCancellationException if the specified timeout was hit before
6464
* a query result could be returned.
65-
* @Throws IllegalStateException if this method is called on a Realm that isn't using Flexible Sync.
65+
* @throws IllegalStateException if this method is called on a Realm that isn't using Flexible Sync.
66+
* @throws io.realm.kotlin.mongodb.exceptions.BadFlexibleSyncQueryException if the server did not
67+
* accept the set of queries. The exact reason is found in the exception message.
6668
*/
6769
@ExperimentalFlexibleSyncApi
6870
public suspend fun <T : RealmObject> RealmResults<T>.subscribe(

packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmQueryExtImpl.kt

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,17 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
16+
@file:Suppress("invisible_reference", "invisible_member")
1717
package io.realm.kotlin.mongodb.internal
1818

1919
import io.realm.kotlin.Realm
2020
import io.realm.kotlin.internal.RealmImpl
2121
import io.realm.kotlin.internal.getRealm
22+
import io.realm.kotlin.internal.query.ObjectQuery
23+
import io.realm.kotlin.mongodb.exceptions.BadFlexibleSyncQueryException
2224
import io.realm.kotlin.mongodb.subscriptions
2325
import io.realm.kotlin.mongodb.sync.Subscription
26+
import io.realm.kotlin.mongodb.sync.SubscriptionSet
2427
import io.realm.kotlin.mongodb.sync.SyncConfiguration
2528
import io.realm.kotlin.mongodb.sync.WaitForSync
2629
import io.realm.kotlin.mongodb.syncSession
@@ -32,7 +35,6 @@ import kotlinx.coroutines.withContext
3235
import kotlinx.coroutines.withTimeout
3336
import kotlin.time.Duration
3437

35-
@Suppress("invisible_reference", "invisible_member")
3638
internal suspend fun <T : RealmObject> createSubscriptionFromQuery(
3739
query: RealmQuery<T>,
3840
name: String?,
@@ -41,7 +43,7 @@ internal suspend fun <T : RealmObject> createSubscriptionFromQuery(
4143
timeout: Duration
4244
): RealmResults<T> {
4345

44-
if (query !is io.realm.kotlin.internal.query.ObjectQuery<T>) {
46+
if (query !is ObjectQuery<T>) {
4547
throw IllegalStateException("Only queries on objects are supported. This was: ${query::class}")
4648
}
4749
if (query.realmReference.owner !is RealmImpl) {
@@ -53,8 +55,7 @@ internal suspend fun <T : RealmObject> createSubscriptionFromQuery(
5355

5456
return withTimeout(timeout) {
5557
withContext(appDispatcher) {
56-
val existingSubscription: Subscription? =
57-
if (name != null) subscriptions.findByName(name) else subscriptions.findByQuery(query)
58+
val existingSubscription: Subscription? = findExistingQueryInSubscriptions(name, query, subscriptions)
5859
if (existingSubscription == null || updateExisting) {
5960
subscriptions.update {
6061
add(query, name, updateExisting)
@@ -66,9 +67,33 @@ internal suspend fun <T : RealmObject> createSubscriptionFromQuery(
6667
// The subscription should already exist, just make sure we downloaded all
6768
// server data before continuing.
6869
realm.syncSession.downloadAllServerChanges()
70+
subscriptions.refresh()
71+
subscriptions.errorMessage?.let { errorMessage: String ->
72+
throw BadFlexibleSyncQueryException(errorMessage)
73+
}
6974
}
7075
// Rerun the query on the latest Realm version.
7176
realm.query(query.clazz, query.description()).find()
7277
}
7378
}
7479
}
80+
81+
// A subscription only matches if name, type and query all matches
82+
private fun <T : RealmObject> findExistingQueryInSubscriptions(
83+
name: String?,
84+
query: ObjectQuery<T>,
85+
subscriptions: SubscriptionSet<Realm>
86+
): Subscription? {
87+
return if (name != null) {
88+
val sub: Subscription? = subscriptions.findByName(name)
89+
val companion = io.realm.kotlin.internal.platform.realmObjectCompanionOrThrow(query.clazz)
90+
val userTypeName = companion.io_realm_kotlin_className
91+
if (sub?.queryDescription == query.description() && sub.objectType == userTypeName) {
92+
sub
93+
} else {
94+
null
95+
}
96+
} else {
97+
subscriptions.findByQuery(query)
98+
}
99+
}

packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SubscriptionImpl.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ internal class SubscriptionImpl(
3939
override val updatedAt: RealmInstant = RealmInstantImpl(RealmInterop.realm_sync_subscription_updated_at(nativePointer))
4040
override val name: String? = RealmInterop.realm_sync_subscription_name(nativePointer)
4141
override val objectType: String = RealmInterop.realm_sync_subscription_object_class_name(nativePointer)
42-
override val queryDescription: String = RealmInterop.realm_sync_subscription_query_string(nativePointer)
42+
// Trim the query to match the output of RealmQuery.description()
43+
override val queryDescription: String = RealmInterop.realm_sync_subscription_query_string(nativePointer).trim()
4344

4445
@Suppress("invisible_member")
4546
override fun <T : RealmObject> asQuery(type: KClass<T>): RealmQuery<T> {

packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/MutableSubscriptionSetTests.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ class MutableSubscriptionSetTests {
102102
assertEquals(SubscriptionSetState.PENDING, updatedSubs.state)
103103
val sub: Subscription = updatedSubs.first()
104104
assertEquals("test", sub.name)
105-
assertEquals("TRUEPREDICATE ", sub.queryDescription)
105+
assertEquals("TRUEPREDICATE", sub.queryDescription)
106106
assertEquals("FlexParentObject", sub.objectType)
107107
assertTrue(now <= sub.createdAt, "Was: $now <= ${sub.createdAt}")
108108
assertEquals(sub.updatedAt, sub.createdAt)
@@ -122,7 +122,7 @@ class MutableSubscriptionSetTests {
122122
assertEquals(SubscriptionSetState.PENDING, updatedSubs.state)
123123
val sub: Subscription = updatedSubs.first()
124124
assertNull(sub.name)
125-
assertEquals("TRUEPREDICATE ", sub.queryDescription)
125+
assertEquals("TRUEPREDICATE", sub.queryDescription)
126126
assertEquals("FlexParentObject", sub.objectType)
127127
assertTrue(now <= sub.createdAt, "Was: $now <= ${sub.createdAt}")
128128
assertEquals(sub.updatedAt, sub.createdAt)
@@ -185,7 +185,7 @@ class MutableSubscriptionSetTests {
185185
val sub = subs.first()
186186
assertEquals("sub1", sub.name)
187187
assertEquals("FlexParentObject", sub.objectType)
188-
assertEquals("name == \"red\" ", sub.queryDescription)
188+
assertEquals("name == \"red\"", sub.queryDescription)
189189
assertTrue(sub.createdAt < sub.updatedAt)
190190
assertEquals(createdAt, sub.createdAt)
191191
}

packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionExtensionsTests.kt

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ class SubscriptionExtensionsTests {
9292
assertEquals(1, subs.size)
9393
val sub: Subscription = subs.first()
9494
assertNull(sub.name)
95-
assertEquals("TRUEPREDICATE ", sub.queryDescription)
95+
assertEquals("TRUEPREDICATE", sub.queryDescription)
9696
assertEquals(FlexParentObject::class.simpleName, sub.objectType)
9797
}
9898

@@ -107,7 +107,7 @@ class SubscriptionExtensionsTests {
107107
assertEquals(1, subs.size)
108108
val sub: Subscription = subs.first()
109109
assertNull(sub.name)
110-
assertEquals("TRUEPREDICATE ", sub.queryDescription)
110+
assertEquals("TRUEPREDICATE", sub.queryDescription)
111111
assertEquals(FlexParentObject::class.simpleName, sub.objectType)
112112
}
113113

@@ -122,7 +122,7 @@ class SubscriptionExtensionsTests {
122122
assertEquals(1, subs.size)
123123
val sub: Subscription = subs.first()
124124
assertNull(sub.name)
125-
assertEquals("TRUEPREDICATE ", sub.queryDescription)
125+
assertEquals("TRUEPREDICATE", sub.queryDescription)
126126
assertEquals(FlexParentObject::class.simpleName, sub.objectType)
127127
}
128128

@@ -168,7 +168,7 @@ class SubscriptionExtensionsTests {
168168
assertEquals(SubscriptionSetState.COMPLETE, updatedSubs.state)
169169
var sub: Subscription = updatedSubs.first()
170170
assertNull(sub.name)
171-
assertEquals("section == $section ", sub.queryDescription)
171+
assertEquals("section == $section", sub.queryDescription)
172172
assertEquals("FlexParentObject", sub.objectType)
173173

174174
// Checking that we don't hit the network the 2nd time around
@@ -184,7 +184,7 @@ class SubscriptionExtensionsTests {
184184
assertEquals(SubscriptionSetState.COMPLETE, updatedSubs.state)
185185
sub = updatedSubs.last()
186186
assertEquals("my-name", sub.name)
187-
assertEquals("section == $section ", sub.queryDescription)
187+
assertEquals("section == $section", sub.queryDescription)
188188
assertEquals("FlexParentObject", sub.objectType)
189189

190190
// Checking that we don't hit the network the 2nd time around
@@ -269,7 +269,7 @@ class SubscriptionExtensionsTests {
269269
assertEquals(SubscriptionSetState.COMPLETE, updatedSubs.state)
270270
var sub: Subscription = updatedSubs.first()
271271
assertNull(sub.name)
272-
assertEquals("section == $section ", sub.queryDescription)
272+
assertEquals("section == $section", sub.queryDescription)
273273
assertEquals("FlexParentObject", sub.objectType)
274274

275275
// Checking that we don't hit the network the 2nd time around
@@ -285,7 +285,7 @@ class SubscriptionExtensionsTests {
285285
assertEquals(SubscriptionSetState.COMPLETE, updatedSubs.state)
286286
sub = updatedSubs.last()
287287
assertEquals("my-name", sub.name)
288-
assertEquals("section == $section ", sub.queryDescription)
288+
assertEquals("section == $section", sub.queryDescription)
289289
assertEquals("FlexParentObject", sub.objectType)
290290

291291
// Checking that we don't hit the network the 2nd time around
@@ -340,12 +340,12 @@ class SubscriptionExtensionsTests {
340340
subQueryResult.subscribe()
341341
val subs = realm.subscriptions
342342
assertEquals(1, subs.size)
343-
assertEquals("section == 42 and name == \"Jane\" ", subs.first().queryDescription)
343+
assertEquals("section == 42 and name == \"Jane\"", subs.first().queryDescription)
344344
subQueryResult.subscribe("my-name")
345345
assertEquals(2, subs.size)
346346
val lastSub = subs.last()
347347
assertEquals("my-name", lastSub.name)
348-
assertEquals("section == 42 and name == \"Jane\" ", lastSub.queryDescription)
348+
assertEquals("section == 42 and name == \"Jane\"", lastSub.queryDescription)
349349
}
350350

351351
@Test
@@ -374,6 +374,24 @@ class SubscriptionExtensionsTests {
374374
}
375375
}
376376

377+
@Test
378+
fun updatingOnlyQueryWillTriggerFirstTimeBehavior() = runBlocking<Unit> {
379+
val section = Random.nextInt()
380+
381+
// 1. Create a named subscription
382+
realm.query<FlexParentObject>("section = $0", section).subscribe("my-name", mode = WaitForSync.FIRST_TIME)
383+
384+
// 2. Pause the connection in order to go offline
385+
realm.syncSession.pause()
386+
387+
// 3. Update the query of the named subscription. This should trigger FIRST_TIME behavior again.
388+
// and because we are offline, the subscribe call should throw.
389+
val query = realm.query<FlexParentObject>("section = $0 AND TRUEPREDICATE", section)
390+
assertFailsWith<TimeoutCancellationException> {
391+
query.subscribe("my-name", updateExisting = true, mode = WaitForSync.FIRST_TIME, timeout = 1.seconds)
392+
}
393+
}
394+
377395
private suspend fun uploadServerData(sectionId: Int, noOfObjects: Int) {
378396
val user = app.createUserAndLogin()
379397
val config = SyncConfiguration.Builder(user, setOf(FlexParentObject::class, FlexChildObject::class, FlexEmbeddedObject::class))

packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionSetTests.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ class SubscriptionSetTests {
132132
val sub: Subscription = subscriptions.findByQuery(query)!!
133133
assertNotNull(sub)
134134
assertEquals("FlexParentObject", sub.objectType)
135-
assertEquals("TRUEPREDICATE ", sub.queryDescription)
135+
assertEquals("TRUEPREDICATE", sub.queryDescription)
136136
}
137137

138138
@Test

packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SubscriptionTests.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ class SubscriptionTests {
9090

9191
assertEquals("mySub", namedSub.name)
9292
assertEquals("ParentPk", namedSub.objectType)
93-
assertEquals("TRUEPREDICATE ", namedSub.queryDescription)
93+
assertEquals("TRUEPREDICATE", namedSub.queryDescription)
9494
assertTrue(now <= namedSub.updatedAt, "$now <= ${namedSub.updatedAt}")
9595
assertTrue(now <= namedSub.createdAt, "$now <= ${namedSub.createdAt}")
9696

@@ -100,7 +100,7 @@ class SubscriptionTests {
100100
}.first()
101101
assertNull(anonSub.name)
102102
assertEquals("ParentPk", anonSub.objectType)
103-
assertEquals("TRUEPREDICATE ", anonSub.queryDescription)
103+
assertEquals("TRUEPREDICATE", anonSub.queryDescription)
104104
assertTrue(now <= namedSub.updatedAt, "$now <= ${namedSub.updatedAt}")
105105
assertTrue(now <= namedSub.createdAt, "$now <= ${namedSub.createdAt}")
106106
}
@@ -119,7 +119,7 @@ class SubscriptionTests {
119119
// Check that properties still work even if subscription is deleted elsewhere
120120
assertEquals("mySub", snapshotSub.name)
121121
assertEquals("ParentPk", snapshotSub.objectType)
122-
assertEquals("TRUEPREDICATE ", snapshotSub.queryDescription)
122+
assertEquals("TRUEPREDICATE", snapshotSub.queryDescription)
123123
assertNotNull(snapshotSub.updatedAt)
124124
assertNotNull(snapshotSub.createdAt)
125125
Unit

0 commit comments

Comments
 (0)