Skip to content

Commit 6674fad

Browse files
authored
Add test 'bloom filter should correctly encode special unicode characters' to query.test.ts (#7412)
1 parent aea4a44 commit 6674fad

File tree

1 file changed

+144
-0
lines changed

1 file changed

+144
-0
lines changed

packages/firestore/test/integration/api/query.test.ts

+144
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
doc,
3232
DocumentChange,
3333
DocumentChangeType,
34+
DocumentData,
3435
documentId,
3536
enableNetwork,
3637
endAt,
@@ -2195,6 +2196,149 @@ apiDescribe('Queries', persistence => {
21952196
});
21962197
});
21972198
}).timeout('90s');
2199+
2200+
// TODO(b/270731363): Re-enable this test once the Firestore emulator is fixed
2201+
// to send an existence filter.
2202+
// eslint-disable-next-line no-restricted-properties
2203+
(USE_EMULATOR ? it.skip : it)(
2204+
'bloom filter should correctly encode complex Unicode characters',
2205+
async () => {
2206+
// Firestore does not do any Unicode normalization on the document IDs.
2207+
// Therefore, two document IDs that are canonically-equivalent (i.e. they
2208+
// visually appear identical) but are represented by a different sequence
2209+
// of Unicode code points are treated as distinct document IDs.
2210+
const testDocIds = [
2211+
'DocumentToDelete',
2212+
// The next two strings both end with "e" with an accent: the first uses
2213+
// the dedicated Unicode code point for this character, while the second
2214+
// uses the standard lowercase "e" followed by the accent combining
2215+
// character.
2216+
'LowercaseEWithAcuteAccent_\u00E9',
2217+
'LowercaseEWithAcuteAccent_\u0065\u0301',
2218+
// The next two strings both end with an "e" with two different accents
2219+
// applied via the following two combining characters. The combining
2220+
// characters are specified in a different order and Firestore treats
2221+
// these document IDs as unique, despite the order of the combining
2222+
// characters being irrelevant.
2223+
'LowercaseEWithMultipleAccents_\u0065\u0301\u0327',
2224+
'LowercaseEWithMultipleAccents_\u0065\u0327\u0301',
2225+
// The next string contains a character outside the BMP (the "basic
2226+
// multilingual plane"); that is, its code point is greater than 0xFFFF.
2227+
// In UTF-16 (which JavaScript uses to store Unicode strings) this
2228+
// requires a surrogate pair, two 16-bit code units, to represent this
2229+
// character. Make sure that its presence is correctly tested in the
2230+
// bloom filter, which uses UTF-8 encoding.
2231+
'Smiley_\u{1F600}'
2232+
];
2233+
2234+
// Verify assumptions about the equivalence of strings in `testDocIds`.
2235+
expect(testDocIds[1].normalize()).equals(testDocIds[2].normalize());
2236+
expect(testDocIds[3].normalize()).equals(testDocIds[4].normalize());
2237+
expect(testDocIds[5]).equals('Smiley_\uD83D\uDE00');
2238+
2239+
// Create the mapping from document ID to document data for the document
2240+
// IDs specified in `testDocIds`.
2241+
const testDocs = testDocIds.reduce((map, docId) => {
2242+
map[docId] = { foo: 42 };
2243+
return map;
2244+
}, {} as { [key: string]: DocumentData });
2245+
2246+
// Ensure that the local cache is configured to use LRU garbage
2247+
// collection (rather than eager garbage collection) so that the resume
2248+
// token and document data does not get prematurely evicted.
2249+
const lruPersistence = persistence.toLruGc();
2250+
2251+
return withRetry(async attemptNumber => {
2252+
return withTestCollection(
2253+
lruPersistence,
2254+
testDocs,
2255+
async (coll, db) => {
2256+
// Run a query to populate the local cache with documents that have
2257+
// names with complex Unicode characters.
2258+
const snapshot1 = await getDocs(coll);
2259+
const snapshot1DocumentIds = snapshot1.docs.map(
2260+
documentSnapshot => documentSnapshot.id
2261+
);
2262+
expect(
2263+
snapshot1DocumentIds,
2264+
'snapshot1DocumentIds'
2265+
).to.have.members(testDocIds);
2266+
2267+
// Delete one of the documents so that the next call to getDocs() will
2268+
// experience an existence filter mismatch. Do this deletion in a
2269+
// transaction, rather than using deleteDoc(), to avoid affecting the
2270+
// local cache.
2271+
await runTransaction(db, async txn => {
2272+
const snapshotOfDocumentToDelete = await txn.get(
2273+
doc(coll, 'DocumentToDelete')
2274+
);
2275+
expect(
2276+
snapshotOfDocumentToDelete.exists(),
2277+
'snapshotOfDocumentToDelete.exists()'
2278+
).to.be.true;
2279+
txn.delete(snapshotOfDocumentToDelete.ref);
2280+
});
2281+
2282+
// Wait for 10 seconds, during which Watch will stop tracking the
2283+
// query and will send an existence filter rather than "delete" events
2284+
// when the query is resumed.
2285+
await new Promise(resolve => setTimeout(resolve, 10000));
2286+
2287+
// Resume the query and save the resulting snapshot for verification.
2288+
// Use some internal testing hooks to "capture" the existence filter
2289+
// mismatches.
2290+
const [existenceFilterMismatches, snapshot2] =
2291+
await captureExistenceFilterMismatches(() => getDocs(coll));
2292+
const snapshot2DocumentIds = snapshot2.docs.map(
2293+
documentSnapshot => documentSnapshot.id
2294+
);
2295+
const testDocIdsMinusDeletedDocId = testDocIds.filter(
2296+
documentId => documentId !== 'DocumentToDelete'
2297+
);
2298+
expect(
2299+
snapshot2DocumentIds,
2300+
'snapshot2DocumentIds'
2301+
).to.have.members(testDocIdsMinusDeletedDocId);
2302+
2303+
// Verify that Watch sent an existence filter with the correct counts.
2304+
expect(
2305+
existenceFilterMismatches,
2306+
'existenceFilterMismatches'
2307+
).to.have.length(1);
2308+
const { localCacheCount, existenceFilterCount, bloomFilter } =
2309+
existenceFilterMismatches[0];
2310+
expect(localCacheCount, 'localCacheCount').to.equal(
2311+
testDocIds.length
2312+
);
2313+
expect(existenceFilterCount, 'existenceFilterCount').to.equal(
2314+
testDocIds.length - 1
2315+
);
2316+
2317+
// Verify that Watch sent a valid bloom filter.
2318+
if (!bloomFilter) {
2319+
expect.fail(
2320+
'The existence filter should have specified a bloom filter ' +
2321+
'in its `unchanged_names` field.'
2322+
);
2323+
throw new Error('should never get here');
2324+
}
2325+
2326+
// Verify that the bloom filter was successfully used to avert a full
2327+
// requery. If a false positive occurred, which is statistically rare,
2328+
// but technically possible, then retry the entire test.
2329+
if (attemptNumber === 1 && !bloomFilter.applied) {
2330+
throw new RetryError();
2331+
}
2332+
2333+
expect(
2334+
bloomFilter.applied,
2335+
`bloomFilter.applied with attemptNumber=${attemptNumber}`
2336+
).to.be.true;
2337+
}
2338+
);
2339+
});
2340+
}
2341+
).timeout('90s');
21982342
});
21992343

22002344
function verifyDocumentChange<T>(

0 commit comments

Comments
 (0)