Skip to content

Commit 05dc9d6

Browse files
Fix the ordering of the index-based lookup in getAll(keys) (#6128)
1 parent e9e5f6b commit 05dc9d6

File tree

6 files changed

+120
-29
lines changed

6 files changed

+120
-29
lines changed

.changeset/nasty-hairs-knock.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@firebase/firestore": patch
3+
---
4+
5+
Fixes an issue during multi-document lookup that resulted in the IndexedDB error "The parameter is less than or equal to this cursor's".

packages/firestore/src/local/indexeddb_remote_document_cache.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -611,12 +611,36 @@ function dbCollectionGroupKey(
611611
/* document id */ path.length > 0 ? path[path.length - 1] : ''
612612
];
613613
}
614+
614615
/**
615616
* Comparator that compares document keys according to the primary key sorting
616-
* used by the `DbRemoteDocumentDocument` store (by collection path and then
617-
* document ID).
617+
* used by the `DbRemoteDocumentDocument` store (by prefix path, collection id
618+
* and then document ID).
619+
*
620+
* Visible for testing.
618621
*/
619-
function dbKeyComparator(l: DocumentKey, r: DocumentKey): number {
620-
const cmp = l.path.length - r.path.length;
621-
return cmp !== 0 ? cmp : DocumentKey.comparator(l, r);
622+
export function dbKeyComparator(l: DocumentKey, r: DocumentKey): number {
623+
const left = l.path.toArray();
624+
const right = r.path.toArray();
625+
626+
// The ordering is based on https://chromium.googlesource.com/chromium/blink/+/fe5c21fef94dae71c1c3344775b8d8a7f7e6d9ec/Source/modules/indexeddb/IDBKey.cpp#74
627+
let cmp = 0;
628+
for (let i = 0; i < left.length - 2 && i < right.length - 2; ++i) {
629+
cmp = primitiveComparator(left[i], right[i]);
630+
if (cmp) {
631+
return cmp;
632+
}
633+
}
634+
635+
cmp = primitiveComparator(left.length, right.length);
636+
if (cmp) {
637+
return cmp;
638+
}
639+
640+
cmp = primitiveComparator(left[left.length - 2], right[right.length - 2]);
641+
if (cmp) {
642+
return cmp;
643+
}
644+
645+
return primitiveComparator(left[left.length - 1], right[right.length - 1]);
622646
}

packages/firestore/src/local/simple_db.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,8 @@ export class SimpleDbStore<
656656
loadAll(): PersistencePromise<ValueType[]>;
657657
/** Loads all elements for the index range from the object store. */
658658
loadAll(range: IDBKeyRange): PersistencePromise<ValueType[]>;
659+
/** Loads all elements ordered by the given index. */
660+
loadAll(index: string): PersistencePromise<ValueType[]>;
659661
/**
660662
* Loads all elements from the object store that fall into the provided in the
661663
* index range for the given index.
@@ -845,9 +847,7 @@ export class SimpleDbStore<
845847
cursor.continue(controller.skipToKey);
846848
}
847849
};
848-
}).next(() => {
849-
return PersistencePromise.waitFor(results);
850-
});
850+
}).next(() => PersistencePromise.waitFor(results));
851851
}
852852

853853
private options(

packages/firestore/test/unit/local/remote_document_cache.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,19 @@ function genericRemoteDocumentCacheTests(
406406
});
407407
});
408408

409+
it('can set and read several documents with deeply nested keys', () => {
410+
// This test verifies that the sorting works correctly in IndexedDB,
411+
// which sorts by prefix path first.
412+
// Repro of https://github.com/firebase/firebase-js-sdk/issues/6110
413+
const keys = ['a/a/a/a/a/a/a/a', 'b/b/b/b/a/a', 'c/c/a/a', 'd/d'];
414+
return cache
415+
.addEntries(keys.map(k => doc(k, VERSION, DOC_DATA)))
416+
.then(() => cache.getEntries(documentKeySet(...keys.map(k => key(k)))))
417+
.then(read => {
418+
expect(read.size).to.equal(keys.length);
419+
});
420+
});
421+
409422
it('can set and read several documents including missing document', () => {
410423
const docs = [
411424
doc(DOC_PATH, VERSION, DOC_DATA),

packages/firestore/test/unit/local/simple_db.test.ts

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ import { expect, use } from 'chai';
1919
import chaiAsPromised from 'chai-as-promised';
2020
import { Context } from 'mocha';
2121

22+
import { dbKeyComparator } from '../../../src/local/indexeddb_remote_document_cache';
2223
import { PersistencePromise } from '../../../src/local/persistence_promise';
2324
import {
2425
SimpleDb,
2526
SimpleDbSchemaConverter,
2627
SimpleDbStore,
2728
SimpleDbTransaction
2829
} from '../../../src/local/simple_db';
30+
import { DocumentKey } from '../../../src/model/document_key';
2931
import { fail } from '../../../src/util/assert';
3032
import { Code, FirestoreError } from '../../../src/util/error';
3133

@@ -66,13 +68,16 @@ class TestSchemaConverter implements SimpleDbSchemaConverter {
6668
fromVersion: number,
6769
toVersion: number
6870
): PersistencePromise<void> {
69-
const objectStore = db.createObjectStore('users', { keyPath: 'id' });
70-
objectStore.createIndex('age-name', ['age', 'name'], {
71+
const userStore = db.createObjectStore('users', { keyPath: 'id' });
72+
userStore.createIndex('age-name', ['age', 'name'], {
7173
unique: false
7274
});
7375

7476
// A store that uses arrays as keys.
75-
db.createObjectStore('docs');
77+
const docStore = db.createObjectStore('docs');
78+
docStore.createIndex('path', ['prefixPath', 'collectionId', 'documentId'], {
79+
unique: false
80+
});
7681
return PersistencePromise.resolve();
7782
}
7883
}
@@ -526,6 +531,68 @@ describe('SimpleDb', () => {
526531
);
527532
});
528533

534+
it('correctly sorts keys with nested arrays', async function (this: Context) {
535+
// This test verifies that the sorting in IndexedDb matches
536+
// `dbKeyComparator()`
537+
538+
const keys = [
539+
'a/a/a/a/a/a/a/a/a/a',
540+
'a/b/a/a/a/a/a/a/a/b',
541+
'b/a/a/a/a/a/a/a/a/a',
542+
'b/b/a/a/a/a/a/a/a/b',
543+
'b/b/a/a/a/a/a/a',
544+
'b/b/b/a/a/a/a/b',
545+
'c/c/a/a/a/a',
546+
'd/d/a/a',
547+
'e/e'
548+
].map(k => DocumentKey.fromPath(k));
549+
550+
interface ValueType {
551+
prefixPath: string[];
552+
collectionId: string;
553+
documentId: string;
554+
}
555+
556+
const expectedOrder = [...keys];
557+
expectedOrder.sort(dbKeyComparator);
558+
559+
const actualOrder = await db.runTransaction(
560+
this.test!.fullTitle(),
561+
'readwrite',
562+
['docs'],
563+
txn => {
564+
const store = txn.store<string[], ValueType>('docs');
565+
566+
const writes = keys.map(k => {
567+
const path = k.path.toArray();
568+
return store.put(k.path.toArray(), {
569+
prefixPath: path.slice(0, path.length - 2),
570+
collectionId: path[path.length - 2],
571+
documentId: path[path.length - 1]
572+
});
573+
});
574+
575+
return PersistencePromise.waitFor(writes).next(() =>
576+
store
577+
.loadAll('path')
578+
.next(keys =>
579+
keys.map(k =>
580+
DocumentKey.fromSegments([
581+
...k.prefixPath,
582+
k.collectionId,
583+
k.documentId
584+
])
585+
)
586+
)
587+
);
588+
}
589+
);
590+
591+
expect(actualOrder.map(k => k.toString())).to.deep.equal(
592+
expectedOrder.map(k => k.toString())
593+
);
594+
});
595+
529596
it('retries transactions', async function (this: Context) {
530597
let attemptCount = 0;
531598

packages/firestore/test/unit/local/test_remote_document_cache.ts

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
import { SnapshotVersion } from '../../../src/core/snapshot_version';
1919
import { IndexManager } from '../../../src/local/index_manager';
20-
import { remoteDocumentCacheGetNewDocumentChanges } from '../../../src/local/indexeddb_remote_document_cache';
2120
import { Persistence } from '../../../src/local/persistence';
2221
import { PersistencePromise } from '../../../src/local/persistence_promise';
2322
import { RemoteDocumentCache } from '../../../src/local/remote_document_cache';
@@ -139,23 +138,6 @@ export class TestRemoteDocumentCache {
139138
);
140139
}
141140

142-
getNewDocumentChanges(sinceReadTime: SnapshotVersion): Promise<{
143-
changedDocs: MutableDocumentMap;
144-
readTime: SnapshotVersion;
145-
}> {
146-
return this.persistence.runTransaction(
147-
'getNewDocumentChanges',
148-
'readonly',
149-
txn => {
150-
return remoteDocumentCacheGetNewDocumentChanges(
151-
this.cache,
152-
txn,
153-
sinceReadTime
154-
);
155-
}
156-
);
157-
}
158-
159141
getSize(): Promise<number> {
160142
return this.persistence.runTransaction('get size', 'readonly', txn =>
161143
this.cache.getSize(txn)

0 commit comments

Comments
 (0)