Skip to content

Commit 3fd0de9

Browse files
authored
Fix: sort document reference by long type id (#2257)
1 parent bef5668 commit 3fd0de9

File tree

2 files changed

+165
-8
lines changed

2 files changed

+165
-8
lines changed

dev/src/path.ts

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,10 @@ abstract class Path<T> {
150150
/**
151151
* Compare the current path against another Path object.
152152
*
153+
* Compare the current path against another Path object. Paths are compared segment by segment,
154+
* prioritizing numeric IDs (e.g., "__id123__") in numeric ascending order, followed by string
155+
* segments in lexicographical order.
156+
*
153157
* @private
154158
* @internal
155159
* @param other The path to compare to.
@@ -158,20 +162,67 @@ abstract class Path<T> {
158162
compareTo(other: Path<T>): number {
159163
const len = Math.min(this.segments.length, other.segments.length);
160164
for (let i = 0; i < len; i++) {
161-
if (this.segments[i] < other.segments[i]) {
162-
return -1;
163-
}
164-
if (this.segments[i] > other.segments[i]) {
165-
return 1;
165+
const comparison = this.compareSegments(
166+
this.segments[i],
167+
other.segments[i]
168+
);
169+
if (comparison !== 0) {
170+
return comparison;
166171
}
167172
}
168-
if (this.segments.length < other.segments.length) {
173+
return Math.sign(this.segments.length - other.segments.length);
174+
}
175+
176+
private compareSegments(lhs: string, rhs: string): number {
177+
const isLhsNumeric = this.isNumericId(lhs);
178+
const isRhsNumeric = this.isNumericId(rhs);
179+
180+
if (isLhsNumeric && !isRhsNumeric) {
181+
// Only lhs is numeric
182+
return -1;
183+
} else if (!isLhsNumeric && isRhsNumeric) {
184+
// Only rhs is numeric
185+
return 1;
186+
} else if (isLhsNumeric && isRhsNumeric) {
187+
// both numeric
188+
return this.compareNumbers(
189+
this.extractNumericId(lhs),
190+
this.extractNumericId(rhs)
191+
);
192+
} else {
193+
// both non-numeric
194+
return this.compareStrings(lhs, rhs);
195+
}
196+
}
197+
198+
// Checks if a segment is a numeric ID (starts with "__id" and ends with "__").
199+
private isNumericId(segment: string): boolean {
200+
return segment.startsWith('__id') && segment.endsWith('__');
201+
}
202+
203+
// Extracts the long number from a numeric ID segment.
204+
private extractNumericId(segment: string): bigint {
205+
return BigInt(segment.substring(4, segment.length - 2));
206+
}
207+
208+
private compareNumbers(lhs: bigint, rhs: bigint): number {
209+
if (lhs < rhs) {
169210
return -1;
211+
} else if (lhs > rhs) {
212+
return 1;
213+
} else {
214+
return 0;
170215
}
171-
if (this.segments.length > other.segments.length) {
216+
}
217+
218+
private compareStrings(lhs: string, rhs: string): number {
219+
if (lhs < rhs) {
220+
return -1;
221+
} else if (lhs > rhs) {
172222
return 1;
223+
} else {
224+
return 0;
173225
}
174-
return 0;
175226
}
176227

177228
/**

dev/system-test/firestore.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3913,6 +3913,112 @@ describe('Query class', () => {
39133913
unsubscribe();
39143914
});
39153915

3916+
it('snapshot listener sorts query by DocumentId same way as server', async () => {
3917+
const batch = firestore.batch();
3918+
batch.set(randomCol.doc('A'), {a: 1});
3919+
batch.set(randomCol.doc('a'), {a: 1});
3920+
batch.set(randomCol.doc('Aa'), {a: 1});
3921+
batch.set(randomCol.doc('7'), {a: 1});
3922+
batch.set(randomCol.doc('12'), {a: 1});
3923+
batch.set(randomCol.doc('__id7__'), {a: 1});
3924+
batch.set(randomCol.doc('__id12__'), {a: 1});
3925+
batch.set(randomCol.doc('__id-2__'), {a: 1});
3926+
batch.set(randomCol.doc('__id1_'), {a: 1});
3927+
batch.set(randomCol.doc('_id1__'), {a: 1});
3928+
batch.set(randomCol.doc('__id'), {a: 1});
3929+
// largest long number
3930+
batch.set(randomCol.doc('__id9223372036854775807__'), {a: 1});
3931+
batch.set(randomCol.doc('__id9223372036854775806__'), {a: 1});
3932+
// smallest long number
3933+
batch.set(randomCol.doc('__id-9223372036854775808__'), {a: 1});
3934+
batch.set(randomCol.doc('__id-9223372036854775807__'), {a: 1});
3935+
await batch.commit();
3936+
3937+
const query = randomCol.orderBy(FieldPath.documentId());
3938+
const expectedDocs = [
3939+
'__id-9223372036854775808__',
3940+
'__id-9223372036854775807__',
3941+
'__id-2__',
3942+
'__id7__',
3943+
'__id12__',
3944+
'__id9223372036854775806__',
3945+
'__id9223372036854775807__',
3946+
'12',
3947+
'7',
3948+
'A',
3949+
'Aa',
3950+
'__id',
3951+
'__id1_',
3952+
'_id1__',
3953+
'a',
3954+
];
3955+
3956+
const getSnapshot = await query.get();
3957+
expect(getSnapshot.docs.map(d => d.id)).to.deep.equal(expectedDocs);
3958+
3959+
const unsubscribe = query.onSnapshot(snapshot =>
3960+
currentDeferred.resolve(snapshot)
3961+
);
3962+
3963+
const watchSnapshot = await waitForSnapshot();
3964+
// Compare the snapshot (including sort order) of a snapshot
3965+
snapshotsEqual(watchSnapshot, {
3966+
docs: getSnapshot.docs,
3967+
docChanges: getSnapshot.docChanges(),
3968+
});
3969+
unsubscribe();
3970+
});
3971+
3972+
it('snapshot listener sorts filtered query by DocumentId same way as server', async () => {
3973+
const batch = firestore.batch();
3974+
batch.set(randomCol.doc('A'), {a: 1});
3975+
batch.set(randomCol.doc('a'), {a: 1});
3976+
batch.set(randomCol.doc('Aa'), {a: 1});
3977+
batch.set(randomCol.doc('7'), {a: 1});
3978+
batch.set(randomCol.doc('12'), {a: 1});
3979+
batch.set(randomCol.doc('__id7__'), {a: 1});
3980+
batch.set(randomCol.doc('__id12__'), {a: 1});
3981+
batch.set(randomCol.doc('__id-2__'), {a: 1});
3982+
batch.set(randomCol.doc('__id1_'), {a: 1});
3983+
batch.set(randomCol.doc('_id1__'), {a: 1});
3984+
batch.set(randomCol.doc('__id'), {a: 1});
3985+
// largest long number
3986+
batch.set(randomCol.doc('__id9223372036854775807__'), {a: 1});
3987+
batch.set(randomCol.doc('__id9223372036854775806__'), {a: 1});
3988+
// smallest long number
3989+
batch.set(randomCol.doc('__id-9223372036854775808__'), {a: 1});
3990+
batch.set(randomCol.doc('__id-9223372036854775807__'), {a: 1});
3991+
await batch.commit();
3992+
3993+
const query = randomCol
3994+
.where(FieldPath.documentId(), '>', '__id7__')
3995+
.where(FieldPath.documentId(), '<=', 'A')
3996+
.orderBy(FieldPath.documentId());
3997+
const expectedDocs = [
3998+
'__id12__',
3999+
'__id9223372036854775806__',
4000+
'__id9223372036854775807__',
4001+
'12',
4002+
'7',
4003+
'A',
4004+
];
4005+
4006+
const getSnapshot = await query.get();
4007+
expect(getSnapshot.docs.map(d => d.id)).to.deep.equal(expectedDocs);
4008+
4009+
const unsubscribe = query.onSnapshot(snapshot =>
4010+
currentDeferred.resolve(snapshot)
4011+
);
4012+
4013+
const watchSnapshot = await waitForSnapshot();
4014+
// Compare the snapshot (including sort order) of a snapshot
4015+
snapshotsEqual(watchSnapshot, {
4016+
docs: getSnapshot.docs,
4017+
docChanges: getSnapshot.docChanges(),
4018+
});
4019+
unsubscribe();
4020+
});
4021+
39164022
it('SDK orders vector field same way as backend', async () => {
39174023
// We validate that the SDK orders the vector field the same way as the backend
39184024
// by comparing the sort order of vector fields from a Query.get() and

0 commit comments

Comments
 (0)