Skip to content

Commit e11ca48

Browse files
Index-free: Drop last limbo free snapshot if re-upgraded (#734)
1 parent 2092857 commit e11ca48

File tree

2 files changed

+118
-8
lines changed

2 files changed

+118
-8
lines changed

firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteSchema.java

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
package com.google.firebase.firestore.local;
1616

17+
import static com.google.firebase.firestore.util.Assert.fail;
1718
import static com.google.firebase.firestore.util.Assert.hardAssert;
1819

1920
import android.content.ContentValues;
@@ -26,7 +27,9 @@
2627
import androidx.annotation.VisibleForTesting;
2728
import com.google.common.base.Preconditions;
2829
import com.google.firebase.firestore.model.ResourcePath;
30+
import com.google.firebase.firestore.proto.Target;
2931
import com.google.firebase.firestore.util.Consumer;
32+
import com.google.protobuf.InvalidProtocolBufferException;
3033
import java.util.ArrayList;
3134
import java.util.List;
3235

@@ -136,7 +139,16 @@ void runMigrations(int fromVersion, int toVersion) {
136139
}
137140

138141
if (fromVersion < 9 && toVersion >= 9) {
139-
addReadTime();
142+
if (!hasReadTime()) {
143+
addReadTime();
144+
} else {
145+
// Index-free queries rely on the fact that documents updated after a query's last limbo
146+
// free snapshot version are persisted with their read-time. If a customer upgrades to
147+
// schema version 9, downgrades and then upgrades again, some queries may have a last limbo
148+
// free snapshot version despite the fact that not all updated document have an associated
149+
// read time.
150+
dropLastLimboFreeSnapshotVersion();
151+
}
140152
}
141153

142154
/*
@@ -363,14 +375,39 @@ private void addSequenceNumber() {
363375
}
364376
}
365377

378+
private boolean hasReadTime() {
379+
boolean hasReadTimeSeconds = tableContainsColumn("remote_documents", "read_time_seconds");
380+
boolean hasReadTimeNanos = tableContainsColumn("remote_documents", "read_time_nanos");
381+
382+
hardAssert(
383+
hasReadTimeSeconds == hasReadTimeNanos,
384+
"Table contained just one of read_time_seconds or read_time_nanos");
385+
386+
return hasReadTimeSeconds && hasReadTimeNanos;
387+
}
388+
366389
private void addReadTime() {
367-
if (!tableContainsColumn("remote_documents", "read_time_seconds")) {
368-
hardAssert(
369-
!tableContainsColumn("remote_documents", "read_time_nanos"),
370-
"Table contained read_time_nanos, but is missing read_time_seconds");
371-
db.execSQL("ALTER TABLE remote_documents ADD COLUMN read_time_seconds INTEGER");
372-
db.execSQL("ALTER TABLE remote_documents ADD COLUMN read_time_nanos INTEGER");
373-
}
390+
db.execSQL("ALTER TABLE remote_documents ADD COLUMN read_time_seconds INTEGER");
391+
db.execSQL("ALTER TABLE remote_documents ADD COLUMN read_time_nanos INTEGER");
392+
}
393+
394+
private void dropLastLimboFreeSnapshotVersion() {
395+
new SQLitePersistence.Query(db, "SELECT target_id, target_proto FROM targets")
396+
.forEach(
397+
cursor -> {
398+
int targetId = cursor.getInt(0);
399+
byte[] targetProtoBytes = cursor.getBlob(1);
400+
401+
try {
402+
Target targetProto = Target.parseFrom(targetProtoBytes);
403+
targetProto = targetProto.toBuilder().clearLastLimboFreeSnapshotVersion().build();
404+
db.execSQL(
405+
"UPDATE targets SET target_proto = ? WHERE target_id = ?",
406+
new Object[] {targetProto.toByteArray(), targetId});
407+
} catch (InvalidProtocolBufferException e) {
408+
throw fail("Failed to decode Query data for target %s", targetId);
409+
}
410+
});
374411
}
375412

376413
/**

firebase-firestore/src/test/java/com/google/firebase/firestore/local/SQLiteSchemaTest.java

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import static com.google.firebase.firestore.testutil.TestUtil.path;
2222
import static com.google.firebase.firestore.testutil.TestUtil.query;
2323
import static com.google.firebase.firestore.testutil.TestUtil.version;
24+
import static com.google.firebase.firestore.util.Assert.fail;
2425
import static java.util.Arrays.asList;
2526
import static org.junit.Assert.assertEquals;
2627
import static org.junit.Assert.assertFalse;
@@ -36,10 +37,13 @@
3637
import com.google.firebase.firestore.model.DocumentKey;
3738
import com.google.firebase.firestore.model.ResourcePath;
3839
import com.google.firebase.firestore.proto.MaybeDocument;
40+
import com.google.firebase.firestore.proto.Target;
3941
import com.google.firebase.firestore.proto.WriteBatch;
4042
import com.google.firebase.firestore.remote.RemoteSerializer;
4143
import com.google.firestore.v1.Document;
4244
import com.google.firestore.v1.Write;
45+
import com.google.protobuf.InvalidProtocolBufferException;
46+
import com.google.protobuf.Timestamp;
4347
import java.util.ArrayList;
4448
import java.util.HashMap;
4549
import java.util.HashSet;
@@ -436,6 +440,68 @@ public void existingDocumentsRemainReadableAfterIndexFreeMigration() {
436440
assertResultsContain(results, "coll/new");
437441
}
438442

443+
@Test
444+
public void dropsLastLimboFreeSnapshotIfPreviouslyDowngraded() {
445+
schema.runMigrations(0, 9);
446+
447+
db.execSQL(
448+
"INSERT INTO targets (target_id, canonical_id, target_proto) VALUES (?,?, ?)",
449+
new Object[] {1, "foo", createDummyQueryTargetWithLimboFreeVersion(1).toByteArray()});
450+
db.execSQL(
451+
"INSERT INTO targets (target_id, canonical_id, target_proto) VALUES (?, ?, ?)",
452+
new Object[] {2, "bar", createDummyQueryTargetWithLimboFreeVersion(2).toByteArray()});
453+
db.execSQL(
454+
"INSERT INTO targets (target_id, canonical_id, target_proto) VALUES (?,?, ?)",
455+
new Object[] {3, "baz", createDummyQueryTargetWithLimboFreeVersion(3).toByteArray()});
456+
457+
schema.runMigrations(0, 8);
458+
schema.runMigrations(8, 9);
459+
460+
int rowCount =
461+
new SQLitePersistence.Query(db, "SELECT target_id, target_proto FROM targets")
462+
.forEach(
463+
cursor -> {
464+
int targetId = cursor.getInt(0);
465+
byte[] targetProtoBytes = cursor.getBlob(1);
466+
467+
try {
468+
Target targetProto = Target.parseFrom(targetProtoBytes);
469+
assertEquals(targetId, targetProto.getTargetId());
470+
assertFalse(targetProto.hasLastLimboFreeSnapshotVersion());
471+
} catch (InvalidProtocolBufferException e) {
472+
fail("Failed to decode Query data");
473+
}
474+
});
475+
476+
assertEquals(3, rowCount);
477+
}
478+
479+
@Test
480+
public void keepsLastLimboFreeSnapshotIfNotDowngraded() {
481+
schema.runMigrations(0, 9);
482+
483+
db.execSQL(
484+
"INSERT INTO targets (target_id, canonical_id, target_proto) VALUES (?,?, ?)",
485+
new Object[] {1, "foo", createDummyQueryTargetWithLimboFreeVersion(1).toByteArray()});
486+
487+
// Make sure that we don't drop the lastLimboFreeSnapshotVersion if we are already on schema
488+
// version 9.
489+
schema.runMigrations(9, 9);
490+
491+
new SQLitePersistence.Query(db, "SELECT target_proto FROM targets")
492+
.forEach(
493+
cursor -> {
494+
byte[] targetProtoBytes = cursor.getBlob(0);
495+
496+
try {
497+
Target targetProto = Target.parseFrom(targetProtoBytes);
498+
assertTrue(targetProto.hasLastLimboFreeSnapshotVersion());
499+
} catch (InvalidProtocolBufferException e) {
500+
fail("Failed to decode Query data");
501+
}
502+
});
503+
}
504+
439505
private SQLiteRemoteDocumentCache createRemoteDocumentCache() {
440506
DatabaseId databaseId = DatabaseId.forProject("foo");
441507
LocalSerializer serializer = new LocalSerializer(new RemoteSerializer(databaseId));
@@ -460,6 +526,13 @@ private byte[] createDummyDocument(String name) {
460526
.toByteArray();
461527
}
462528

529+
private Target createDummyQueryTargetWithLimboFreeVersion(int targetId) {
530+
return Target.newBuilder()
531+
.setTargetId(targetId)
532+
.setLastLimboFreeSnapshotVersion(Timestamp.newBuilder().setSeconds(42))
533+
.build();
534+
}
535+
463536
private void assertResultsContain(
464537
ImmutableSortedMap<DocumentKey, com.google.firebase.firestore.model.Document> actualResults,
465538
String... docs) {

0 commit comments

Comments
 (0)