Skip to content

Commit c6ecac8

Browse files
authored
Fix CSI timestamp issue (#8090)
1 parent c8a2568 commit c6ecac8

8 files changed

+307
-10
lines changed

.changeset/friendly-eyes-destroy.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@firebase/firestore': patch
3+
---
4+
5+
Fixed the CSI issue where indexing on timestamp fields leads to incorrect query results.

packages/firestore/src/index/firestore_index_value_writer.ts

+9-6
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616
*/
1717

1818
import { DocumentKey } from '../model/document_key';
19-
import { normalizeByteString, normalizeNumber } from '../model/normalize';
19+
import {
20+
normalizeByteString,
21+
normalizeNumber,
22+
normalizeTimestamp
23+
} from '../model/normalize';
2024
import { isMaxValue } from '../model/values';
2125
import { ArrayValue, MapValue, Value } from '../protos/firestore_proto_api';
2226
import { fail } from '../util/assert';
@@ -93,14 +97,13 @@ export class FirestoreIndexValueWriter {
9397
}
9498
}
9599
} else if ('timestampValue' in indexValue) {
96-
const timestamp = indexValue.timestampValue!;
100+
let timestamp = indexValue.timestampValue!;
97101
this.writeValueTypeLabel(encoder, INDEX_TYPE_TIMESTAMP);
98102
if (typeof timestamp === 'string') {
99-
encoder.writeString(timestamp);
100-
} else {
101-
encoder.writeString(`${timestamp.seconds || ''}`);
102-
encoder.writeNumber(timestamp.nanos || 0);
103+
timestamp = normalizeTimestamp(timestamp);
103104
}
105+
encoder.writeString(`${timestamp.seconds || ''}`);
106+
encoder.writeNumber(timestamp.nanos || 0);
104107
} else if ('stringValue' in indexValue) {
105108
this.writeIndexString(indexValue.stringValue!, encoder);
106109
this.writeTruncationMarker(encoder);

packages/firestore/src/local/indexeddb_schema.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,10 @@ import { DbTimestampKey } from './indexeddb_sentinels';
5151
* document lookup via `getAll()`.
5252
* 14. Add overlays.
5353
* 15. Add indexing support.
54+
* 16. Parse timestamp strings before creating index entries.
5455
*/
5556

56-
export const SCHEMA_VERSION = 15;
57+
export const SCHEMA_VERSION = 16;
5758

5859
/**
5960
* Wrapper class to store timestamps (seconds and nanos) in IndexedDb objects.

packages/firestore/src/local/indexeddb_schema_converter.ts

+13
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,19 @@ export class SchemaConverter implements SimpleDbSchemaConverter {
256256
p = p.next(() => createFieldIndex(db));
257257
}
258258

259+
if (fromVersion < 16 && toVersion >= 16) {
260+
// Clear the object stores to remove possibly corrupted index entries
261+
p = p
262+
.next(() => {
263+
const indexStateStore = txn.objectStore(DbIndexStateStore);
264+
indexStateStore.clear();
265+
})
266+
.next(() => {
267+
const indexEntryStore = txn.objectStore(DbIndexEntryStore);
268+
indexEntryStore.clear();
269+
});
270+
}
271+
259272
return p;
260273
}
261274

packages/firestore/src/local/indexeddb_sentinels.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,7 @@ export const V15_STORES = [
414414
DbIndexStateStore,
415415
DbIndexEntryStore
416416
];
417+
export const V16_STORES = V15_STORES;
417418

418419
/**
419420
* The list of all default IndexedDB stores used throughout the SDK. This is
@@ -424,7 +425,9 @@ export const ALL_STORES = V12_STORES;
424425

425426
/** Returns the object stores for the provided schema. */
426427
export function getObjectStores(schemaVersion: number): string[] {
427-
if (schemaVersion === 15) {
428+
if (schemaVersion === 16) {
429+
return V16_STORES;
430+
} else if (schemaVersion === 15) {
428431
return V15_STORES;
429432
} else if (schemaVersion === 14) {
430433
return V14_STORES;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/**
2+
* @license
3+
* Copyright 2024 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0x00 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0x00
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
import { expect } from 'chai';
18+
19+
import { FirestoreIndexValueWriter } from '../../../src/index/firestore_index_value_writer';
20+
import { IndexByteEncoder } from '../../../src/index/index_byte_encoder';
21+
import { Timestamp } from '../../../src/lite-api/timestamp';
22+
import { IndexKind } from '../../../src/model/field_index';
23+
import type { Value } from '../../../src/protos/firestore_proto_api';
24+
import { toTimestamp } from '../../../src/remote/serializer';
25+
import { JSON_SERIALIZER } from '../local/persistence_test_helpers';
26+
27+
import { compare } from './ordered_code_writer.test';
28+
29+
describe('Firestore Index Value Writer', () => {
30+
function compareIndexEncodedValues(
31+
value1: Value,
32+
value2: Value,
33+
direction: IndexKind
34+
): number {
35+
const encoder1 = new IndexByteEncoder();
36+
const encoder2 = new IndexByteEncoder();
37+
FirestoreIndexValueWriter.INSTANCE.writeIndexValue(
38+
value1,
39+
encoder1.forKind(direction)
40+
);
41+
42+
FirestoreIndexValueWriter.INSTANCE.writeIndexValue(
43+
value2,
44+
encoder2.forKind(direction)
45+
);
46+
47+
return compare(encoder1.encodedBytes(), encoder2.encodedBytes());
48+
}
49+
50+
describe('can gracefully handle different format of timestamp', () => {
51+
it('can handle different format of timestamp', () => {
52+
const value1 = { timestampValue: '2016-01-02T10:20:50.850Z' };
53+
const value2 = { timestampValue: '2016-01-02T10:20:50.850000Z' };
54+
const value3 = { timestampValue: '2016-01-02T10:20:50.850000000Z' };
55+
const value4 = {
56+
timestampValue: { seconds: 1451730050, nanos: 850000000 }
57+
};
58+
const value5 = {
59+
timestampValue: toTimestamp(
60+
JSON_SERIALIZER,
61+
new Timestamp(1451730050, 850000000)
62+
)
63+
};
64+
expect(
65+
compareIndexEncodedValues(value1, value2, IndexKind.ASCENDING)
66+
).to.equal(0);
67+
expect(
68+
compareIndexEncodedValues(value1, value3, IndexKind.ASCENDING)
69+
).to.equal(0);
70+
expect(
71+
compareIndexEncodedValues(value1, value4, IndexKind.ASCENDING)
72+
).to.equal(0);
73+
expect(
74+
compareIndexEncodedValues(value1, value5, IndexKind.ASCENDING)
75+
).to.equal(0);
76+
77+
expect(
78+
compareIndexEncodedValues(value1, value2, IndexKind.DESCENDING)
79+
).to.equal(0);
80+
expect(
81+
compareIndexEncodedValues(value1, value3, IndexKind.DESCENDING)
82+
).to.equal(0);
83+
expect(
84+
compareIndexEncodedValues(value1, value4, IndexKind.DESCENDING)
85+
).to.equal(0);
86+
expect(
87+
compareIndexEncodedValues(value1, value5, IndexKind.DESCENDING)
88+
).to.equal(0);
89+
});
90+
91+
it('can handle timestamps with 0 nanoseconds', () => {
92+
const value1 = { timestampValue: '2016-01-02T10:20:50Z' };
93+
const value2 = { timestampValue: '2016-01-02T10:20:50.000000000Z' };
94+
const value3 = { timestampValue: { seconds: 1451730050, nanos: 0 } };
95+
const value4 = {
96+
timestampValue: toTimestamp(
97+
JSON_SERIALIZER,
98+
new Timestamp(1451730050, 0)
99+
)
100+
};
101+
expect(
102+
compareIndexEncodedValues(value1, value2, IndexKind.ASCENDING)
103+
).to.equal(0);
104+
expect(
105+
compareIndexEncodedValues(value1, value3, IndexKind.ASCENDING)
106+
).to.equal(0);
107+
expect(
108+
compareIndexEncodedValues(value1, value4, IndexKind.ASCENDING)
109+
).to.equal(0);
110+
111+
expect(
112+
compareIndexEncodedValues(value1, value2, IndexKind.DESCENDING)
113+
).to.equal(0);
114+
expect(
115+
compareIndexEncodedValues(value1, value3, IndexKind.DESCENDING)
116+
).to.equal(0);
117+
expect(
118+
compareIndexEncodedValues(value1, value4, IndexKind.DESCENDING)
119+
).to.equal(0);
120+
});
121+
122+
it('can compare timestamps with different formats', () => {
123+
const value1 = { timestampValue: '2016-01-02T10:20:50Z' };
124+
const value2 = { timestampValue: '2016-01-02T10:20:50.000001Z' };
125+
const value3 = {
126+
timestampValue: { seconds: 1451730050, nanos: 999999999 }
127+
};
128+
const value4 = {
129+
timestampValue: toTimestamp(
130+
JSON_SERIALIZER,
131+
new Timestamp(1451730050, 1)
132+
)
133+
};
134+
expect(
135+
compareIndexEncodedValues(value1, value2, IndexKind.ASCENDING)
136+
).to.equal(-1);
137+
expect(
138+
compareIndexEncodedValues(value1, value3, IndexKind.ASCENDING)
139+
).to.equal(-1);
140+
expect(
141+
compareIndexEncodedValues(value1, value4, IndexKind.ASCENDING)
142+
).to.equal(-1);
143+
expect(
144+
compareIndexEncodedValues(value2, value3, IndexKind.ASCENDING)
145+
).to.equal(-1);
146+
expect(
147+
compareIndexEncodedValues(value2, value4, IndexKind.ASCENDING)
148+
).to.equal(1);
149+
expect(
150+
compareIndexEncodedValues(value3, value4, IndexKind.ASCENDING)
151+
).to.equal(1);
152+
153+
expect(
154+
compareIndexEncodedValues(value1, value2, IndexKind.DESCENDING)
155+
).to.equal(1);
156+
expect(
157+
compareIndexEncodedValues(value1, value3, IndexKind.DESCENDING)
158+
).to.equal(1);
159+
expect(
160+
compareIndexEncodedValues(value1, value4, IndexKind.DESCENDING)
161+
).to.equal(1);
162+
expect(
163+
compareIndexEncodedValues(value2, value3, IndexKind.DESCENDING)
164+
).to.equal(1);
165+
expect(
166+
compareIndexEncodedValues(value2, value4, IndexKind.DESCENDING)
167+
).to.equal(-1);
168+
expect(
169+
compareIndexEncodedValues(value3, value4, IndexKind.DESCENDING)
170+
).to.equal(-1);
171+
});
172+
});
173+
});

packages/firestore/test/unit/index/ordered_code_writer.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ function fromHex(hexString: string): Uint8Array {
226226
return bytes;
227227
}
228228

229-
function compare(left: Uint8Array, right: Uint8Array): number {
229+
export function compare(left: Uint8Array, right: Uint8Array): number {
230230
for (let i = 0; i < Math.min(left.length, right.length); ++i) {
231231
if (left[i] < right[i]) {
232232
return -1;

0 commit comments

Comments
 (0)