Skip to content

Commit b5fcc0f

Browse files
Schema migration that drops held write acks (#33)
1 parent c544bdc commit b5fcc0f

File tree

3 files changed

+116
-8
lines changed

3 files changed

+116
-8
lines changed

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ int execute(SQLiteStatement statement, Object... args) {
286286
* chaining further methods off the query.
287287
*/
288288
Query query(String sql) {
289-
return new Query(sql);
289+
return new Query(db, sql);
290290
}
291291

292292
/**
@@ -326,11 +326,13 @@ Query query(String sql) {
326326
* return result;
327327
* </pre>
328328
*/
329-
class Query {
329+
static class Query {
330+
private final SQLiteDatabase db;
330331
private final String sql;
331332
private CursorFactory cursorFactory;
332333

333-
private Query(String sql) {
334+
Query(SQLiteDatabase db, String sql) {
335+
this.db = db;
334336
this.sql = sql;
335337
}
336338

@@ -464,7 +466,7 @@ private Cursor startQuery() {
464466
* This method bridges the gap by examining the types of the bindArgs and calling to the
465467
* appropriate bind method on the program.
466468
*/
467-
private void bind(SQLiteProgram program, Object[] bindArgs) {
469+
private static void bind(SQLiteProgram program, Object[] bindArgs) {
468470
for (int i = 0; i < bindArgs.length; i++) {
469471
Object arg = bindArgs[i];
470472
if (arg == null) {

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

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,13 @@
1414

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

17+
import static com.google.firebase.firestore.util.Assert.hardAssert;
18+
1719
import android.content.ContentValues;
1820
import android.database.DatabaseUtils;
1921
import android.database.sqlite.SQLiteDatabase;
22+
import android.database.sqlite.SQLiteStatement;
23+
import com.google.common.base.Preconditions;
2024

2125
/**
2226
* Migrates schemas from version 0 (empty) to whatever the current version is.
@@ -34,10 +38,11 @@ class SQLiteSchema {
3438
* The version of the schema. Increase this by one for each migration added to runMigrations
3539
* below.
3640
*/
37-
static final int VERSION = (Persistence.INDEXING_SUPPORT_ENABLED) ? 6 : 5;
41+
static final int VERSION = (Persistence.INDEXING_SUPPORT_ENABLED) ? 7 : 6;
3842

3943
private final SQLiteDatabase db;
4044

45+
// PORTING NOTE: The Android client doesn't need to use a serializer to remove held write acks.
4146
SQLiteSchema(SQLiteDatabase db) {
4247
this.db = db;
4348
}
@@ -90,9 +95,12 @@ void runMigrations(int fromVersion, int toVersion) {
9095
}
9196

9297
if (fromVersion < 6 && toVersion >= 6) {
93-
if (Persistence.INDEXING_SUPPORT_ENABLED) {
94-
createLocalDocumentsCollectionIndex();
95-
}
98+
removeAcknowledgedMutations();
99+
}
100+
101+
if (fromVersion < 7 && toVersion >= 7) {
102+
Preconditions.checkState(Persistence.INDEXING_SUPPORT_ENABLED);
103+
createLocalDocumentsCollectionIndex();
96104
}
97105
}
98106

@@ -122,6 +130,38 @@ private void createMutationQueue() {
122130
+ "PRIMARY KEY (uid, path, batch_id))");
123131
}
124132

133+
private void removeAcknowledgedMutations() {
134+
SQLitePersistence.Query mutationQueuesQuery =
135+
new SQLitePersistence.Query(
136+
db, "SELECT uid, last_acknowledged_batch_id FROM mutation_queues");
137+
138+
mutationQueuesQuery.forEach(
139+
mutationQueueEntry -> {
140+
String uid = mutationQueueEntry.getString(0);
141+
long lastAcknowledgedBatchId = mutationQueueEntry.getLong(1);
142+
143+
SQLitePersistence.Query mutationsQuery =
144+
new SQLitePersistence.Query(
145+
db, "SELECT batch_id FROM mutations WHERE uid = ? AND batch_id <= ?")
146+
.binding(uid, lastAcknowledgedBatchId);
147+
mutationsQuery.forEach(value -> removeMutationBatch(uid, value.getInt(0)));
148+
});
149+
}
150+
151+
private void removeMutationBatch(String uid, int batchId) {
152+
SQLiteStatement mutationDeleter =
153+
db.compileStatement("DELETE FROM mutations WHERE uid = ? AND batch_id = ?");
154+
mutationDeleter.bindString(1, uid);
155+
mutationDeleter.bindLong(2, batchId);
156+
int deleted = mutationDeleter.executeUpdateDelete();
157+
hardAssert(deleted != 0, "Mutatiohn batch (%s, %d) did not exist", uid, batchId);
158+
159+
// Delete all index entries for this batch
160+
db.execSQL(
161+
"DELETE FROM document_mutations WHERE uid = ? AND batch_id = ?",
162+
new Object[] {uid, batchId});
163+
}
164+
125165
private void createQueryCache() {
126166
// A cache of targets and associated metadata
127167
db.execSQL(

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

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
import android.database.sqlite.SQLiteDatabase;
2323
import android.database.sqlite.SQLiteOpenHelper;
2424
import com.google.firebase.firestore.model.DatabaseId;
25+
import com.google.firebase.firestore.model.ResourcePath;
26+
import com.google.firebase.firestore.proto.WriteBatch;
27+
import com.google.firestore.v1beta1.Document;
28+
import com.google.firestore.v1beta1.Write;
2529
import org.junit.After;
2630
import org.junit.Before;
2731
import org.junit.Test;
@@ -123,6 +127,68 @@ public void testDatabaseName() {
123127
"[DEFAULT]", DatabaseId.forDatabase("my-project", "my-database")));
124128
}
125129

130+
@Test
131+
public void dropsHeldWriteAcks() {
132+
// This test creates a database with schema version 5 that has two users, both of which have
133+
// acknowledged mutations that haven't yet been removed from IndexedDb ("heldWriteAcks").
134+
// Schema version 6 removes heldWriteAcks, and as such these mutations are deleted.
135+
schema.runMigrations(0, 5);
136+
137+
// User 'userA' has two acknowledged mutations and one that is pending.
138+
// User 'userB' has one acknowledged mutation and one that is pending.
139+
addMutationBatch(db, 1, "userA", "docs/foo");
140+
addMutationBatch(db, 2, "userA", "docs/foo");
141+
addMutationBatch(db, 3, "userB", "docs/bar", "doc/baz");
142+
addMutationBatch(db, 4, "userB", "docs/pending");
143+
addMutationBatch(db, 5, "userA", "docs/pending");
144+
145+
// Populate the mutation queues' metadata
146+
db.execSQL(
147+
"INSERT INTO mutation_queues (uid, last_acknowledged_batch_id) VALUES (?, ?)",
148+
new Object[] {"userA", 2});
149+
db.execSQL(
150+
"INSERT INTO mutation_queues (uid, last_acknowledged_batch_id) VALUES (?, ?)",
151+
new Object[] {"userB", 3});
152+
db.execSQL(
153+
"INSERT INTO mutation_queues (uid, last_acknowledged_batch_id) VALUES (?, ?)",
154+
new Object[] {"userC", -1});
155+
156+
schema.runMigrations(5, 6);
157+
158+
// Verify that all but the two pending mutations have been cleared by the migration.
159+
new SQLitePersistence.Query(db, "SELECT COUNT(*) FROM mutations")
160+
.first(value -> assertEquals(2, value.getInt(0)));
161+
162+
// Verify that we still have two index entries for the pending documents
163+
new SQLitePersistence.Query(db, "SELECT COUNT(*) FROM document_mutations")
164+
.first(value -> assertEquals(2, value.getInt(0)));
165+
166+
// Verify that we still have one metadata entry for each existing queue
167+
new SQLitePersistence.Query(db, "SELECT COUNT(*) FROM mutation_queues")
168+
.first(value -> assertEquals(3, value.getInt(0)));
169+
}
170+
171+
private void addMutationBatch(SQLiteDatabase db, int batchId, String uid, String... docs) {
172+
WriteBatch.Builder write = WriteBatch.newBuilder();
173+
write.setBatchId(batchId);
174+
175+
for (String doc : docs) {
176+
db.execSQL(
177+
"INSERT INTO document_mutations (uid, path, batch_id) VALUES (?, ?, ?)",
178+
new Object[] {uid, EncodedPath.encode(ResourcePath.fromString(doc)), batchId});
179+
180+
write.addWrites(
181+
Write.newBuilder()
182+
.setUpdate(
183+
Document.newBuilder()
184+
.setName("projects/projectId/databases/(default)/documents/" + doc)));
185+
}
186+
187+
db.execSQL(
188+
"INSERT INTO mutations (uid, batch_id, mutations) VALUES (?,?,?)",
189+
new Object[] {uid, batchId, write.build().toByteArray()});
190+
}
191+
126192
private void assertNoResultsForQuery(String query, String[] args) {
127193
Cursor cursor = null;
128194
try {

0 commit comments

Comments
 (0)