Skip to content

Commit f598475

Browse files
committed
change the compareUtf8Strings to lazy encoding
1 parent 3418ef8 commit f598475

File tree

2 files changed

+279
-0
lines changed

2 files changed

+279
-0
lines changed

packages/firestore/src/util/misc.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717

1818
import { randomBytes } from '../platform/random_bytes';
19+
import { newTextEncoder } from '../platform/text_serializer';
1920

2021
import { debugAssert } from './assert';
2122

@@ -74,6 +75,45 @@ export interface Equatable<T> {
7475
isEqual(other: T): boolean;
7576
}
7677

78+
/** Compare strings in UTF-8 encoded byte order */
79+
export function compareUtf8Strings(left: string, right: string): number {
80+
let i = 0;
81+
while (i < left.length && i < right.length) {
82+
const leftCodePoint = left.codePointAt(i)!;
83+
const rightCodePoint = right.codePointAt(i)!;
84+
85+
if (leftCodePoint !== rightCodePoint) {
86+
if (leftCodePoint < 128 && rightCodePoint < 128) {
87+
// ASCII comparison
88+
return primitiveComparator(leftCodePoint, rightCodePoint);
89+
} else {
90+
// Lazy instantiate TextEncoder
91+
const encoder = newTextEncoder();
92+
93+
// UTF-8 encoded byte comparison, substring 2 indexes to cover surrogate pairs
94+
const leftBytes = encoder.encode(left.substring(i, i + 2));
95+
const rightBytes = encoder.encode(right.substring(i, i + 2));
96+
for (
97+
let j = 0;
98+
j < Math.min(leftBytes.length, rightBytes.length);
99+
j++
100+
) {
101+
const comparison = primitiveComparator(leftBytes[j], rightBytes[j]);
102+
if (comparison !== 0) {
103+
return comparison;
104+
}
105+
}
106+
}
107+
}
108+
109+
// Increment by 2 for surrogate pairs, 1 otherwise
110+
i += leftCodePoint > 0xffff ? 2 : 1;
111+
}
112+
113+
// Compare lengths if all characters are equal
114+
return primitiveComparator(left.length, right.length);
115+
}
116+
77117
export interface Iterable<V> {
78118
forEach: (cb: (v: V) => void) => void;
79119
}

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

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2424,4 +2424,243 @@ apiDescribe('Database', persistence => {
24242424
});
24252425
});
24262426
});
2427+
2428+
describe('Sort unicode strings', () => {
2429+
const expectedDocs = [
2430+
'b',
2431+
'a',
2432+
'h',
2433+
'i',
2434+
'c',
2435+
'f',
2436+
'e',
2437+
'd',
2438+
'g',
2439+
'k',
2440+
'j'
2441+
];
2442+
it('snapshot listener sorts unicode strings the same as server', async () => {
2443+
const testDocs = {
2444+
'a': { value: 'Łukasiewicz' },
2445+
'b': { value: 'Sierpiński' },
2446+
'c': { value: '岩澤' },
2447+
'd': { value: '🄟' },
2448+
'e': { value: 'P' },
2449+
'f': { value: '︒' },
2450+
'g': { value: '🐵' },
2451+
'h': { value: '你好' },
2452+
'i': { value: '你顥' },
2453+
'j': { value: '😁' },
2454+
'k': { value: '😀' },
2455+
};
2456+
2457+
return withTestCollection(persistence, testDocs, async collectionRef => {
2458+
const orderedQuery = query(collectionRef, orderBy('value'));
2459+
2460+
const getSnapshot = await getDocsFromServer(orderedQuery);
2461+
expect(toIds(getSnapshot)).to.deep.equal(expectedDocs);
2462+
2463+
const storeEvent = new EventsAccumulator<QuerySnapshot>();
2464+
const unsubscribe = onSnapshot(orderedQuery, storeEvent.storeEvent);
2465+
const watchSnapshot = await storeEvent.awaitEvent();
2466+
expect(toIds(watchSnapshot)).to.deep.equal(toIds(getSnapshot));
2467+
2468+
unsubscribe();
2469+
2470+
await checkOnlineAndOfflineResultsMatch(orderedQuery, ...expectedDocs);
2471+
});
2472+
});
2473+
2474+
it('snapshot listener sorts unicode strings in array the same as server', async () => {
2475+
const testDocs = {
2476+
'a': { value: ['Łukasiewicz'] },
2477+
'b': { value: ['Sierpiński'] },
2478+
'c': { value: ['岩澤'] },
2479+
'd': { value: ['🄟'] },
2480+
'e': { value: ['P'] },
2481+
'f': { value: ['︒'] },
2482+
'g': { value: ['🐵'] },
2483+
'h': { value: ['你好'] },
2484+
'i': { value: ['你顥'] },
2485+
'j': { value: ['😁'] },
2486+
'k': { value: ['😀'] }
2487+
};
2488+
2489+
return withTestCollection(persistence, testDocs, async collectionRef => {
2490+
const orderedQuery = query(collectionRef, orderBy('value'));
2491+
2492+
const getSnapshot = await getDocsFromServer(orderedQuery);
2493+
expect(toIds(getSnapshot)).to.deep.equal(expectedDocs);
2494+
2495+
const storeEvent = new EventsAccumulator<QuerySnapshot>();
2496+
const unsubscribe = onSnapshot(orderedQuery, storeEvent.storeEvent);
2497+
const watchSnapshot = await storeEvent.awaitEvent();
2498+
expect(toIds(watchSnapshot)).to.deep.equal(toIds(getSnapshot));
2499+
2500+
unsubscribe();
2501+
2502+
await checkOnlineAndOfflineResultsMatch(orderedQuery, ...expectedDocs);
2503+
});
2504+
});
2505+
2506+
it('snapshot listener sorts unicode strings in map the same as server', async () => {
2507+
const testDocs = {
2508+
'a': { value: { foo: 'Łukasiewicz' } },
2509+
'b': { value: { foo: 'Sierpiński' } },
2510+
'c': { value: { foo: '岩澤' } },
2511+
'd': { value: { foo: '🄟' } },
2512+
'e': { value: { foo: 'P' } },
2513+
'f': { value: { foo: '︒' } },
2514+
'g': { value: { foo: '🐵' } },
2515+
'h': { value: { foo: '你好' } },
2516+
'i': { value: { foo: '你顥' } },
2517+
'j': { value: { foo: '😁' } },
2518+
'k': { value: { foo: '😀' } }
2519+
};
2520+
2521+
return withTestCollection(persistence, testDocs, async collectionRef => {
2522+
const orderedQuery = query(collectionRef, orderBy('value'));
2523+
2524+
const getSnapshot = await getDocsFromServer(orderedQuery);
2525+
expect(toIds(getSnapshot)).to.deep.equal(expectedDocs);
2526+
2527+
const storeEvent = new EventsAccumulator<QuerySnapshot>();
2528+
const unsubscribe = onSnapshot(orderedQuery, storeEvent.storeEvent);
2529+
const watchSnapshot = await storeEvent.awaitEvent();
2530+
expect(toIds(watchSnapshot)).to.deep.equal(toIds(getSnapshot));
2531+
2532+
unsubscribe();
2533+
2534+
await checkOnlineAndOfflineResultsMatch(orderedQuery, ...expectedDocs);
2535+
});
2536+
});
2537+
2538+
it('snapshot listener sorts unicode strings in map key the same as server', async () => {
2539+
const testDocs = {
2540+
'a': { value: { 'Łukasiewicz': true } },
2541+
'b': { value: { 'Sierpiński': true } },
2542+
'c': { value: { '岩澤': true } },
2543+
'd': { value: { '🄟': true } },
2544+
'e': { value: { 'P': true } },
2545+
'f': { value: { '︒': true } },
2546+
'g': { value: { '🐵': true } },
2547+
'h': { value: { '你好': true } },
2548+
'i': { value: { '你顥': true } },
2549+
'j': { value: { '😁': true } },
2550+
'k': { value: { '😀': true } }
2551+
};
2552+
2553+
return withTestCollection(persistence, testDocs, async collectionRef => {
2554+
const orderedQuery = query(collectionRef, orderBy('value'));
2555+
2556+
const getSnapshot = await getDocsFromServer(orderedQuery);
2557+
expect(toIds(getSnapshot)).to.deep.equal(expectedDocs);
2558+
2559+
const storeEvent = new EventsAccumulator<QuerySnapshot>();
2560+
const unsubscribe = onSnapshot(orderedQuery, storeEvent.storeEvent);
2561+
const watchSnapshot = await storeEvent.awaitEvent();
2562+
expect(toIds(watchSnapshot)).to.deep.equal(toIds(getSnapshot));
2563+
2564+
unsubscribe();
2565+
2566+
await checkOnlineAndOfflineResultsMatch(orderedQuery, ...expectedDocs);
2567+
});
2568+
});
2569+
2570+
it('snapshot listener sorts unicode strings in document key the same as server', async () => {
2571+
const testDocs = {
2572+
'Łukasiewicz': { value: true },
2573+
'Sierpiński': { value: true },
2574+
'岩澤': { value: true },
2575+
'🄟': { value: true },
2576+
'P': { value: true },
2577+
'︒': { value: true },
2578+
'🐵': { value: true },
2579+
'你好': { value: true },
2580+
'你顥': { value: true },
2581+
'😁': { value: true },
2582+
'😀': { value: true }
2583+
};
2584+
2585+
return withTestCollection(persistence, testDocs, async collectionRef => {
2586+
const orderedQuery = query(collectionRef, orderBy(documentId()));
2587+
2588+
const getSnapshot = await getDocsFromServer(orderedQuery);
2589+
const expectedDocs = [
2590+
'Sierpiński',
2591+
'Łukasiewicz',
2592+
'你好',
2593+
'你顥',
2594+
'岩澤',
2595+
'︒',
2596+
'P',
2597+
'🄟',
2598+
'🐵',
2599+
'😀',
2600+
'😁'
2601+
];
2602+
expect(toIds(getSnapshot)).to.deep.equal(expectedDocs);
2603+
2604+
const storeEvent = new EventsAccumulator<QuerySnapshot>();
2605+
const unsubscribe = onSnapshot(orderedQuery, storeEvent.storeEvent);
2606+
const watchSnapshot = await storeEvent.awaitEvent();
2607+
expect(toIds(watchSnapshot)).to.deep.equal(toIds(getSnapshot));
2608+
2609+
unsubscribe();
2610+
2611+
await checkOnlineAndOfflineResultsMatch(orderedQuery, ...expectedDocs);
2612+
});
2613+
});
2614+
2615+
// eslint-disable-next-line no-restricted-properties
2616+
(persistence.storage === 'indexeddb' ? it.skip : it)(
2617+
'snapshot listener sorts unicode strings in document key the same as server with persistence',
2618+
async () => {
2619+
const testDocs = {
2620+
'Łukasiewicz': { value: true },
2621+
'Sierpiński': { value: true },
2622+
'岩澤': { value: true },
2623+
'🄟': { value: true },
2624+
'P': { value: true },
2625+
'︒': { value: true },
2626+
'🐵': { value: true },
2627+
'你好': { value: true },
2628+
'你顥': { value: true },
2629+
'😁': { value: true },
2630+
'😀': { value: true }
2631+
};
2632+
2633+
return withTestCollection(
2634+
persistence,
2635+
testDocs,
2636+
async collectionRef => {
2637+
const orderedQuery = query(collectionRef, orderBy('value'));
2638+
2639+
const getSnapshot = await getDocsFromServer(orderedQuery);
2640+
expect(toIds(getSnapshot)).to.deep.equal([
2641+
'Sierpiński',
2642+
'Łukasiewicz',
2643+
'你好',
2644+
'你顥',
2645+
'岩澤',
2646+
'︒',
2647+
'P',
2648+
'🄟',
2649+
'🐵',
2650+
'😀',
2651+
'😁'
2652+
]);
2653+
2654+
const storeEvent = new EventsAccumulator<QuerySnapshot>();
2655+
const unsubscribe = onSnapshot(orderedQuery, storeEvent.storeEvent);
2656+
const watchSnapshot = await storeEvent.awaitEvent();
2657+
// TODO: IndexedDB sorts string lexicographically, and misses the document with ID '🄟','🐵'
2658+
expect(toIds(watchSnapshot)).to.deep.equal(toIds(getSnapshot));
2659+
2660+
unsubscribe();
2661+
}
2662+
);
2663+
}
2664+
);
2665+
});
24272666
});

0 commit comments

Comments
 (0)