Skip to content

Commit 6555aa7

Browse files
Port IndexState and IndexOffset (#5916)
* Add readTime to MutableDocument * Cleanup * Port IndexState and IndexOffset * Review * Review
1 parent fad3c47 commit 6555aa7

File tree

5 files changed

+356
-11
lines changed

5 files changed

+356
-11
lines changed

packages/firestore/src/api/index_configuration.ts

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

1818
import { fieldPathFromDotSeparatedString } from '../lite-api/user_data_reader';
19-
import { FieldIndex, Kind, Segment } from '../model/field_index';
19+
import {
20+
FieldIndex,
21+
IndexKind,
22+
IndexSegment,
23+
IndexState
24+
} from '../model/field_index';
2025
import { Code, FirestoreError } from '../util/error';
2126
import { cast } from '../util/input_validation';
2227

@@ -160,7 +165,7 @@ export function setIndexConfiguration(
160165
for (const index of indexConfiguration.indexes) {
161166
const collectionGroup = tryGetString(index, 'collectionGroup');
162167

163-
const segments: Segment[] = [];
168+
const segments: IndexSegment[] = [];
164169
if (Array.isArray(index.fields)) {
165170
for (const field of index.fields) {
166171
const fieldPathString = tryGetString(field, 'fieldPath');
@@ -170,17 +175,22 @@ export function setIndexConfiguration(
170175
);
171176

172177
if (field.arrayConfig === 'CONTAINS') {
173-
segments.push(new Segment(fieldPath, Kind.CONTAINS));
178+
segments.push(new IndexSegment(fieldPath, IndexKind.CONTAINS));
174179
} else if (field.order === 'ASCENDING') {
175-
segments.push(new Segment(fieldPath, Kind.ASCENDING));
180+
segments.push(new IndexSegment(fieldPath, IndexKind.ASCENDING));
176181
} else if (field.order === 'DESCENDING') {
177-
segments.push(new Segment(fieldPath, Kind.DESCENDING));
182+
segments.push(new IndexSegment(fieldPath, IndexKind.DESCENDING));
178183
}
179184
}
180185
}
181186

182187
parsedIndexes.push(
183-
new FieldIndex(FieldIndex.UNKNOWN_ID, collectionGroup, segments)
188+
new FieldIndex(
189+
FieldIndex.UNKNOWN_ID,
190+
collectionGroup,
191+
segments,
192+
IndexState.empty()
193+
)
184194
);
185195
}
186196
}

packages/firestore/src/model/document_key.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ export class DocumentKey {
3939
return new DocumentKey(ResourcePath.fromString(name).popFirst(5));
4040
}
4141

42+
static empty(): DocumentKey {
43+
return new DocumentKey(ResourcePath.emptyPath());
44+
}
45+
4246
/** Returns true if the document is in the specified collectionId. */
4347
hasCollectionId(collectionId: string): boolean {
4448
return (

packages/firestore/src/model/field_index.ts

Lines changed: 164 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,26 @@
1515
* limitations under the License.
1616
*/
1717

18+
import { SnapshotVersion } from '../core/snapshot_version';
19+
import { Timestamp } from '../lite-api/timestamp';
20+
import { primitiveComparator } from '../util/misc';
21+
22+
import { Document } from './document';
23+
import { DocumentKey } from './document_key';
1824
import { FieldPath } from './path';
1925

26+
/**
27+
* The initial mutation batch id for each index. Gets updated during index
28+
* backfill.
29+
*/
30+
const INITIAL_LARGEST_BATCH_ID = -1;
31+
32+
/**
33+
* The initial sequence number for each index. Gets updated during index
34+
* backfill.
35+
*/
36+
export const INITIAL_SEQUENCE_NUMBER = 0;
37+
2038
/**
2139
* An index definition for field indexes in Firestore.
2240
*
@@ -30,7 +48,7 @@ import { FieldPath } from './path';
3048
*/
3149
export class FieldIndex {
3250
/** An ID for an index that has not yet been added to persistence. */
33-
static UNKNOWN_ID: -1;
51+
static UNKNOWN_ID = -1;
3452

3553
constructor(
3654
/**
@@ -41,12 +59,40 @@ export class FieldIndex {
4159
/** The collection ID this index applies to. */
4260
readonly collectionGroup: string,
4361
/** The field segments for this index. */
44-
readonly segments: Segment[]
62+
readonly segments: IndexSegment[],
63+
/** Shows how up-to-date the index is for the current user. */
64+
readonly indexState: IndexState
4565
) {}
4666
}
4767

68+
/**
69+
* Compares indexes by collection group and segments. Ignores update time and
70+
* index ID.
71+
*/
72+
export function fieldIndexSemanticComparator(
73+
left: FieldIndex,
74+
right: FieldIndex
75+
): number {
76+
let cmp = primitiveComparator(left.collectionGroup, right.collectionGroup);
77+
if (cmp !== 0) {
78+
return cmp;
79+
}
80+
81+
for (
82+
let i = 0;
83+
i < Math.min(left.segments.length, right.segments.length);
84+
++i
85+
) {
86+
cmp = indexSegmentComparator(left.segments[i], right.segments[i]);
87+
if (cmp !== 0) {
88+
return cmp;
89+
}
90+
}
91+
return primitiveComparator(left.segments.length, right.segments.length);
92+
}
93+
4894
/** The type of the index, e.g. for which type of query it can be used. */
49-
export const enum Kind {
95+
export const enum IndexKind {
5096
/**
5197
* Ordered index. Can be used for <, <=, ==, >=, >, !=, IN and NOT IN queries.
5298
*/
@@ -60,11 +106,124 @@ export const enum Kind {
60106
}
61107

62108
/** An index component consisting of field path and index type. */
63-
export class Segment {
109+
export class IndexSegment {
64110
constructor(
65111
/** The field path of the component. */
66112
readonly fieldPath: FieldPath,
67113
/** The fields sorting order. */
68-
readonly kind: Kind
114+
readonly kind: IndexKind
69115
) {}
70116
}
117+
118+
function indexSegmentComparator(
119+
left: IndexSegment,
120+
right: IndexSegment
121+
): number {
122+
const cmp = FieldPath.comparator(left.fieldPath, right.fieldPath);
123+
if (cmp !== 0) {
124+
return cmp;
125+
}
126+
return primitiveComparator(left.kind, right.kind);
127+
}
128+
129+
/**
130+
* Stores the "high water mark" that indicates how updated the Index is for the
131+
* current user.
132+
*/
133+
export class IndexState {
134+
constructor(
135+
/**
136+
* Indicates when the index was last updated (relative to other indexes).
137+
*/
138+
readonly sequenceNumber: number,
139+
/** The the latest indexed read time, document and batch id. */
140+
readonly offset: IndexOffset
141+
) {}
142+
143+
/** The state of an index that has not yet been backfilled. */
144+
static empty(): IndexState {
145+
return new IndexState(INITIAL_SEQUENCE_NUMBER, IndexOffset.min());
146+
}
147+
}
148+
149+
/**
150+
* Creates an offset that matches all documents with a read time higher than
151+
* `readTime`.
152+
*/
153+
export function newIndexOffsetSuccessorFromReadTime(
154+
readTime: SnapshotVersion,
155+
largestBatchId: number
156+
): IndexOffset {
157+
// We want to create an offset that matches all documents with a read time
158+
// greater than the provided read time. To do so, we technically need to
159+
// create an offset for `(readTime, MAX_DOCUMENT_KEY)`. While we could use
160+
// Unicode codepoints to generate MAX_DOCUMENT_KEY, it is much easier to use
161+
// `(readTime + 1, DocumentKey.empty())` since `> DocumentKey.empty()` matches
162+
// all valid document IDs.
163+
const successorSeconds = readTime.toTimestamp().seconds;
164+
const successorNanos = readTime.toTimestamp().nanoseconds + 1;
165+
const successor = SnapshotVersion.fromTimestamp(
166+
successorNanos === 1e9
167+
? new Timestamp(successorSeconds + 1, 0)
168+
: new Timestamp(successorSeconds, successorNanos)
169+
);
170+
return new IndexOffset(successor, DocumentKey.empty(), largestBatchId);
171+
}
172+
173+
/** Creates a new offset based on the provided document. */
174+
export function newIndexOffsetFromDocument(document: Document): IndexOffset {
175+
return new IndexOffset(
176+
document.readTime,
177+
document.key,
178+
INITIAL_LARGEST_BATCH_ID
179+
);
180+
}
181+
182+
/**
183+
* Stores the latest read time, document and batch ID that were processed for an
184+
* index.
185+
*/
186+
export class IndexOffset {
187+
constructor(
188+
/**
189+
* The latest read time version that has been indexed by Firestore for this
190+
* field index.
191+
*/
192+
readonly readTime: SnapshotVersion,
193+
194+
/**
195+
* The key of the last document that was indexed for this query. Use
196+
* `DocumentKey.empty()` if no document has been indexed.
197+
*/
198+
readonly documentKey: DocumentKey,
199+
200+
/*
201+
* The largest mutation batch id that's been processed by Firestore.
202+
*/
203+
readonly largestBatchId: number
204+
) {}
205+
206+
/** The state of an index that has not yet been backfilled. */
207+
static min(): IndexOffset {
208+
return new IndexOffset(
209+
SnapshotVersion.min(),
210+
DocumentKey.empty(),
211+
INITIAL_LARGEST_BATCH_ID
212+
);
213+
}
214+
}
215+
216+
export function indexOffsetComparator(
217+
left: IndexOffset,
218+
right: IndexOffset
219+
): number {
220+
let cmp = left.readTime.compareTo(right.readTime);
221+
if (cmp !== 0) {
222+
return cmp;
223+
}
224+
cmp = DocumentKey.comparator(left.documentKey, right.documentKey);
225+
if (cmp !== 0) {
226+
return cmp;
227+
}
228+
return primitiveComparator(left.largestBatchId, right.largestBatchId);
229+
}

0 commit comments

Comments
 (0)