Skip to content

Commit a3fb711

Browse files
authored
Add bloom filter to existence filter and watchFilters spec builder (#6839)
1 parent c17af51 commit a3fb711

10 files changed

+83
-31
lines changed

packages/firestore/src/protos/firestore_proto_api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,7 @@ export declare type BeginTransactionRequest =
456456
firestoreV1ApiClientInterfaces.BeginTransactionRequest;
457457
export declare type BeginTransactionResponse =
458458
firestoreV1ApiClientInterfaces.BeginTransactionResponse;
459+
export declare type BloomFilter = firestoreV1ApiClientInterfaces.BloomFilter;
459460
export declare type CollectionSelector =
460461
firestoreV1ApiClientInterfaces.CollectionSelector;
461462
export declare type CommitRequest =

packages/firestore/src/remote/existence_filter.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
* limitations under the License.
1616
*/
1717

18+
import { BloomFilter as ProtoBloomFilter } from '../protos/firestore_proto_api';
19+
1820
export class ExistenceFilter {
19-
// TODO(b/33078163): just use simplest form of existence filter for now
20-
constructor(public count: number) {}
21+
constructor(public count: number, public unchangedNames?: ProtoBloomFilter) {}
2122
}

packages/firestore/src/remote/serializer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -537,8 +537,8 @@ export function fromWatchChange(
537537
assertPresent(change.filter, 'filter');
538538
const filter = change.filter;
539539
assertPresent(filter.targetId, 'filter.targetId');
540-
const count = filter.count || 0;
541-
const existenceFilter = new ExistenceFilter(count);
540+
const { count = 0, unchangedNames } = filter;
541+
const existenceFilter = new ExistenceFilter(count, unchangedNames);
542542
const targetId = filter.targetId;
543543
watchChange = new ExistenceFilterChange(targetId, existenceFilter);
544544
} else {

packages/firestore/test/unit/specs/existence_filter_spec.test.ts

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,23 @@ describeSpec('Existence Filters:', [], () => {
3131
.userListens(query1)
3232
.watchAcksFull(query1, 1000, doc1)
3333
.expectEvents(query1, { added: [doc1] })
34-
.watchFilters([query1], doc1.key)
34+
.watchFilters([query1], [doc1.key])
35+
.watchSnapshots(2000);
36+
});
37+
38+
// This test is only to make sure watchFilters can accept bloom filter.
39+
// TODO:(mila) update the tests when bloom filter logic is implemented.
40+
specTest('Existence filter with bloom filter match', [], () => {
41+
const query1 = query('collection');
42+
const doc1 = doc('collection/1', 1000, { v: 1 });
43+
return spec()
44+
.userListens(query1)
45+
.watchAcksFull(query1, 1000, doc1)
46+
.expectEvents(query1, { added: [doc1] })
47+
.watchFilters([query1], [doc1.key], {
48+
bits: { bitmap: 'a', padding: 1 },
49+
hashCount: 1
50+
})
3551
.watchSnapshots(2000);
3652
});
3753

@@ -45,7 +61,7 @@ describeSpec('Existence Filters:', [], () => {
4561
.watchSnapshots(2000)
4662
.expectEvents(query1, {})
4763
.watchSends({ affects: [query1] }, doc1)
48-
.watchFilters([query1], doc1.key)
64+
.watchFilters([query1], [doc1.key])
4965
.watchSnapshots(2000)
5066
.expectEvents(query1, { added: [doc1] });
5167
});
@@ -59,7 +75,7 @@ describeSpec('Existence Filters:', [], () => {
5975
.watchCurrents(query1, 'resume-token-1000')
6076
.watchSnapshots(2000)
6177
.expectEvents(query1, {})
62-
.watchFilters([query1], doc1.key)
78+
.watchFilters([query1], [doc1.key])
6379
.watchSnapshots(2000)
6480
.expectEvents(query1, { fromCache: true });
6581
});
@@ -96,7 +112,37 @@ describeSpec('Existence Filters:', [], () => {
96112
.userListens(query1)
97113
.watchAcksFull(query1, 1000, doc1, doc2)
98114
.expectEvents(query1, { added: [doc1, doc2] })
99-
.watchFilters([query1], doc1.key) // in the next sync doc2 was deleted
115+
.watchFilters([query1], [doc1.key]) // in the next sync doc2 was deleted
116+
.watchSnapshots(2000)
117+
// query is now marked as "inconsistent" because of filter mismatch
118+
.expectEvents(query1, { fromCache: true })
119+
.expectActiveTargets({ query: query1, resumeToken: '' })
120+
.watchRemoves(query1) // Acks removal of query
121+
.watchAcksFull(query1, 2000, doc1)
122+
.expectLimboDocs(doc2.key) // doc2 is now in limbo
123+
.ackLimbo(2000, deletedDoc('collection/2', 2000))
124+
.expectLimboDocs() // doc2 is no longer in limbo
125+
.expectEvents(query1, {
126+
removed: [doc2]
127+
})
128+
);
129+
});
130+
131+
// This test is only to make sure watchFilters can accept bloom filter.
132+
// TODO:(mila) update the tests when bloom filter logic is implemented.
133+
specTest('Existence filter mismatch triggers bloom filter', [], () => {
134+
const query1 = query('collection');
135+
const doc1 = doc('collection/1', 1000, { v: 1 });
136+
const doc2 = doc('collection/2', 1000, { v: 2 });
137+
return (
138+
spec()
139+
.userListens(query1)
140+
.watchAcksFull(query1, 1000, doc1, doc2)
141+
.expectEvents(query1, { added: [doc1, doc2] })
142+
.watchFilters([query1], [doc1.key], {
143+
bits: { bitmap: 'a', padding: 1 },
144+
hashCount: 3
145+
}) // in the next sync doc2 was deleted
100146
.watchSnapshots(2000)
101147
// query is now marked as "inconsistent" because of filter mismatch
102148
.expectEvents(query1, { fromCache: true })
@@ -130,7 +176,7 @@ describeSpec('Existence Filters:', [], () => {
130176
resumeToken: 'existence-filter-resume-token'
131177
})
132178
.watchAcks(query1)
133-
.watchFilters([query1], doc1.key) // in the next sync doc2 was deleted
179+
.watchFilters([query1], [doc1.key]) // in the next sync doc2 was deleted
134180
.watchSnapshots(2000)
135181
// query is now marked as "inconsistent" because of filter mismatch
136182
.expectEvents(query1, { fromCache: true })
@@ -159,7 +205,7 @@ describeSpec('Existence Filters:', [], () => {
159205
// Send a mismatching existence filter with two documents, but don't
160206
// send a new global snapshot. We should not see an event until we
161207
// receive the snapshot.
162-
.watchFilters([query1], doc1.key, doc2.key)
208+
.watchFilters([query1], [doc1.key, doc2.key])
163209
.watchSends({ affects: [query1] }, doc3)
164210
.watchSnapshots(2000)
165211
// The query result includes doc3, but is marked as "inconsistent"
@@ -193,7 +239,7 @@ describeSpec('Existence Filters:', [], () => {
193239
.userListens(query1)
194240
.watchAcksFull(query1, 1000, doc1, doc2)
195241
.expectEvents(query1, { added: [doc1, doc2] })
196-
.watchFilters([query1], doc1.key) // in the next sync doc2 was deleted
242+
.watchFilters([query1], [doc1.key]) // in the next sync doc2 was deleted
197243
.watchSnapshots(2000)
198244
// query is now marked as "inconsistent" because of filter mismatch
199245
.expectEvents(query1, { fromCache: true })
@@ -229,7 +275,7 @@ describeSpec('Existence Filters:', [], () => {
229275
.userListens(query1)
230276
.watchAcksFull(query1, 1000, doc1, doc2)
231277
.expectEvents(query1, { added: [doc1, doc2] })
232-
.watchFilters([query1], doc1.key) // doc2 was deleted
278+
.watchFilters([query1], [doc1.key]) // doc2 was deleted
233279
.watchSnapshots(2000)
234280
.expectEvents(query1, { fromCache: true })
235281
// The SDK is unable to re-run the query, and does not remove doc2

packages/firestore/test/unit/specs/limbo_spec.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -880,7 +880,7 @@ describeSpec('Limbo Documents:', [], () => {
880880
// documents that changed since the resume token. This will cause it
881881
// to just send the docBs with an existence filter with a count of 3.
882882
.watchSends({ affects: [query1] }, docB1, docB2, docB3)
883-
.watchFilters([query1], docB1.key, docB2.key, docB3.key)
883+
.watchFilters([query1], [docB1.key, docB2.key, docB3.key])
884884
.watchSnapshots(1001)
885885
.expectEvents(query1, {
886886
added: [docB1, docB2, docB3],

packages/firestore/test/unit/specs/limit_spec.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ describeSpec('Limits:', [], () => {
341341
// we receive an existence filter, which indicates that our view is
342342
// out of sync.
343343
.watchSends({ affects: [limitQuery] }, secondDocument)
344-
.watchFilters([limitQuery], secondDocument.key)
344+
.watchFilters([limitQuery], [secondDocument.key])
345345
.watchSnapshots(1004)
346346
.expectActiveTargets({ query: limitQuery, resumeToken: '' })
347347
.watchRemoves(limitQuery)

packages/firestore/test/unit/specs/spec_builder.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { DocumentKey } from '../../../src/model/document_key';
3333
import { FieldIndex } from '../../../src/model/field_index';
3434
import { JsonObject } from '../../../src/model/object_value';
3535
import { ResourcePath } from '../../../src/model/path';
36+
import { BloomFilter as ProtoBloomFilter } from '../../../src/protos/firestore_proto_api';
3637
import {
3738
isPermanentWriteError,
3839
mapCodeFromRpcCode,
@@ -769,18 +770,19 @@ export class SpecBuilder {
769770
return this;
770771
}
771772

772-
watchFilters(queries: Query[], ...docs: DocumentKey[]): this {
773+
watchFilters(
774+
queries: Query[],
775+
docs: DocumentKey[] = [],
776+
bloomFilter?: ProtoBloomFilter
777+
): this {
773778
this.nextStep();
774779
const targetIds = queries.map(query => {
775780
return this.getTargetId(query);
776781
});
777782
const keys = docs.map(key => {
778783
return key.path.canonicalString();
779784
});
780-
const filter: SpecWatchFilter = [targetIds] as SpecWatchFilter;
781-
for (const key of keys) {
782-
filter.push(key);
783-
}
785+
const filter = { targetIds, keys, bloomFilter } as SpecWatchFilter;
784786
this.currentStep = {
785787
watchFilter: filter
786788
};

packages/firestore/test/unit/specs/spec_test_runner.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -693,13 +693,12 @@ abstract class TestRunner {
693693
}
694694

695695
private doWatchFilter(watchFilter: SpecWatchFilter): Promise<void> {
696-
const targetIds: TargetId[] = watchFilter[0];
696+
const { targetIds, keys, bloomFilter } = watchFilter;
697697
debugAssert(
698698
targetIds.length === 1,
699699
'ExistenceFilters currently support exactly one target only.'
700700
);
701-
const keys = watchFilter.slice(1);
702-
const filter = new ExistenceFilter(keys.length);
701+
const filter = new ExistenceFilter(keys.length, bloomFilter);
703702
const change = new ExistenceFilterChange(targetIds[0], filter);
704703
return this.doWatchEvent(change);
705704
}
@@ -1577,14 +1576,12 @@ export interface SpecClientState {
15771576
}
15781577

15791578
/**
1580-
* [[<target-id>, ...], <key>, ...]
1581-
* Note that the last parameter is really of type ...string (spread operator)
15821579
* The filter is based of a list of keys to match in the existence filter
15831580
*/
1584-
export interface SpecWatchFilter
1585-
extends Array<TargetId[] | string | undefined> {
1586-
'0': TargetId[];
1587-
'1': string | undefined;
1581+
export interface SpecWatchFilter {
1582+
targetIds: TargetId[];
1583+
keys: string[];
1584+
bloomFilter?: api.BloomFilter;
15881585
}
15891586

15901587
export type SpecLimitType = 'LimitToFirst' | 'LimitToLast';

packages/firestore/test/util/helpers.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -429,15 +429,19 @@ export function existenceFilterEvent(
429429
targetId: number,
430430
syncedKeys: DocumentKeySet,
431431
remoteCount: number,
432-
snapshotVersion: number
432+
snapshotVersion: number,
433+
bloomFilter?: api.BloomFilter
433434
): RemoteEvent {
434435
const aggregator = new WatchChangeAggregator({
435436
getRemoteKeysForTarget: () => syncedKeys,
436437
getTargetDataForTarget: targetId =>
437438
targetData(targetId, TargetPurpose.Listen, 'foo')
438439
});
439440
aggregator.handleExistenceFilter(
440-
new ExistenceFilterChange(targetId, new ExistenceFilter(remoteCount))
441+
new ExistenceFilterChange(
442+
targetId,
443+
new ExistenceFilter(remoteCount, bloomFilter)
444+
)
441445
);
442446
return aggregator.createRemoteEvent(version(snapshotVersion));
443447
}

packages/firestore/test/util/spec_test_helpers.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,9 @@ export function encodeWatchChange(
4444
if (watchChange instanceof ExistenceFilterChange) {
4545
return {
4646
filter: {
47+
targetId: watchChange.targetId,
4748
count: watchChange.existenceFilter.count,
48-
targetId: watchChange.targetId
49+
unchangedNames: watchChange.existenceFilter.unchangedNames
4950
}
5051
};
5152
}

0 commit comments

Comments
 (0)