Skip to content

Port IndexState and IndexOffset #5916

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jan 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions packages/firestore/src/api/index_configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@
*/

import { fieldPathFromDotSeparatedString } from '../lite-api/user_data_reader';
import { FieldIndex, Kind, Segment } from '../model/field_index';
import {
FieldIndex,
IndexKind,
IndexSegment,
IndexState
} from '../model/field_index';
import { Code, FirestoreError } from '../util/error';
import { cast } from '../util/input_validation';

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

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

if (field.arrayConfig === 'CONTAINS') {
segments.push(new Segment(fieldPath, Kind.CONTAINS));
segments.push(new IndexSegment(fieldPath, IndexKind.CONTAINS));
} else if (field.order === 'ASCENDING') {
segments.push(new Segment(fieldPath, Kind.ASCENDING));
segments.push(new IndexSegment(fieldPath, IndexKind.ASCENDING));
} else if (field.order === 'DESCENDING') {
segments.push(new Segment(fieldPath, Kind.DESCENDING));
segments.push(new IndexSegment(fieldPath, IndexKind.DESCENDING));
}
}
}

parsedIndexes.push(
new FieldIndex(FieldIndex.UNKNOWN_ID, collectionGroup, segments)
new FieldIndex(
FieldIndex.UNKNOWN_ID,
collectionGroup,
segments,
IndexState.empty()
)
);
}
}
Expand Down
4 changes: 4 additions & 0 deletions packages/firestore/src/model/document_key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ export class DocumentKey {
return new DocumentKey(ResourcePath.fromString(name).popFirst(5));
}

static empty(): DocumentKey {
return new DocumentKey(ResourcePath.emptyPath());
}

/** Returns true if the document is in the specified collectionId. */
hasCollectionId(collectionId: string): boolean {
return (
Expand Down
169 changes: 164 additions & 5 deletions packages/firestore/src/model/field_index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,26 @@
* limitations under the License.
*/

import { SnapshotVersion } from '../core/snapshot_version';
import { Timestamp } from '../lite-api/timestamp';
import { primitiveComparator } from '../util/misc';

import { Document } from './document';
import { DocumentKey } from './document_key';
import { FieldPath } from './path';

/**
* The initial mutation batch id for each index. Gets updated during index
* backfill.
*/
const INITIAL_LARGEST_BATCH_ID = -1;

/**
* The initial sequence number for each index. Gets updated during index
* backfill.
*/
export const INITIAL_SEQUENCE_NUMBER = 0;

/**
* An index definition for field indexes in Firestore.
*
Expand All @@ -30,7 +48,7 @@ import { FieldPath } from './path';
*/
export class FieldIndex {
/** An ID for an index that has not yet been added to persistence. */
static UNKNOWN_ID: -1;
static UNKNOWN_ID = -1;

constructor(
/**
Expand All @@ -41,12 +59,40 @@ export class FieldIndex {
/** The collection ID this index applies to. */
readonly collectionGroup: string,
/** The field segments for this index. */
readonly segments: Segment[]
readonly segments: IndexSegment[],
/** Shows how up-to-date the index is for the current user. */
readonly indexState: IndexState
) {}
}

/**
* Compares indexes by collection group and segments. Ignores update time and
* index ID.
*/
export function fieldIndexSemanticComparator(
left: FieldIndex,
right: FieldIndex
): number {
let cmp = primitiveComparator(left.collectionGroup, right.collectionGroup);
if (cmp !== 0) {
return cmp;
}

for (
let i = 0;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional: if the left and right segments don't match in length, we can exit early rather than comparing, but i can also see how this code is less verbose

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That works for equals, but not here since "b,c" sorts after "a".

i < Math.min(left.segments.length, right.segments.length);
++i
) {
cmp = indexSegmentComparator(left.segments[i], right.segments[i]);
if (cmp !== 0) {
return cmp;
}
}
return primitiveComparator(left.segments.length, right.segments.length);
}

/** The type of the index, e.g. for which type of query it can be used. */
export const enum Kind {
export const enum IndexKind {
/**
* Ordered index. Can be used for <, <=, ==, >=, >, !=, IN and NOT IN queries.
*/
Expand All @@ -60,11 +106,124 @@ export const enum Kind {
}

/** An index component consisting of field path and index type. */
export class Segment {
export class IndexSegment {
constructor(
/** The field path of the component. */
readonly fieldPath: FieldPath,
/** The fields sorting order. */
readonly kind: Kind
readonly kind: IndexKind
) {}
}

function indexSegmentComparator(
left: IndexSegment,
right: IndexSegment
): number {
const cmp = FieldPath.comparator(left.fieldPath, right.fieldPath);
if (cmp !== 0) {
return cmp;
}
return primitiveComparator(left.kind, right.kind);
}

/**
* Stores the "high water mark" that indicates how updated the Index is for the
* current user.
*/
export class IndexState {
constructor(
/**
* Indicates when the index was last updated (relative to other indexes).
*/
readonly sequenceNumber: number,
/** The the latest indexed read time, document and batch id. */
readonly offset: IndexOffset
) {}

/** The state of an index that has not yet been backfilled. */
static empty(): IndexState {
return new IndexState(INITIAL_SEQUENCE_NUMBER, IndexOffset.min());
}
}

/**
* Creates an offset that matches all documents with a read time higher than
* `readTime`.
*/
export function newIndexOffsetSuccessorFromReadTime(
readTime: SnapshotVersion,
largestBatchId: number
): IndexOffset {
// We want to create an offset that matches all documents with a read time
// greater than the provided read time. To do so, we technically need to
// create an offset for `(readTime, MAX_DOCUMENT_KEY)`. While we could use
// Unicode codepoints to generate MAX_DOCUMENT_KEY, it is much easier to use
// `(readTime + 1, DocumentKey.empty())` since `> DocumentKey.empty()` matches
// all valid document IDs.
const successorSeconds = readTime.toTimestamp().seconds;
const successorNanos = readTime.toTimestamp().nanoseconds + 1;
const successor = SnapshotVersion.fromTimestamp(
successorNanos === 1e9
? new Timestamp(successorSeconds + 1, 0)
: new Timestamp(successorSeconds, successorNanos)
);
return new IndexOffset(successor, DocumentKey.empty(), largestBatchId);
}

/** Creates a new offset based on the provided document. */
export function newIndexOffsetFromDocument(document: Document): IndexOffset {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just double checking that you plan on adding another method to create a new index offset with a specified largest batch id.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The equivalent for Web is just calling the constructor of IndexOffset.

return new IndexOffset(
document.readTime,
document.key,
INITIAL_LARGEST_BATCH_ID
);
}

/**
* Stores the latest read time, document and batch ID that were processed for an
* index.
*/
export class IndexOffset {
constructor(
/**
* The latest read time version that has been indexed by Firestore for this
* field index.
*/
readonly readTime: SnapshotVersion,

/**
* The key of the last document that was indexed for this query. Use
* `DocumentKey.empty()` if no document has been indexed.
*/
readonly documentKey: DocumentKey,

/*
* The largest mutation batch id that's been processed by Firestore.
*/
readonly largestBatchId: number
) {}

/** The state of an index that has not yet been backfilled. */
static min(): IndexOffset {
return new IndexOffset(
SnapshotVersion.min(),
DocumentKey.empty(),
INITIAL_LARGEST_BATCH_ID
);
}
}

export function indexOffsetComparator(
left: IndexOffset,
right: IndexOffset
): number {
let cmp = left.readTime.compareTo(right.readTime);
if (cmp !== 0) {
return cmp;
}
cmp = DocumentKey.comparator(left.documentKey, right.documentKey);
if (cmp !== 0) {
return cmp;
}
return primitiveComparator(left.largestBatchId, right.largestBatchId);
}
Loading