Skip to content

Commit 3c743b2

Browse files
authored
Implement indexing for bson types (#331)
1 parent 0716bd7 commit 3c743b2

File tree

14 files changed

+1823
-258
lines changed

14 files changed

+1823
-258
lines changed

packages/firestore/src/core/target.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import {
2828
INTERNAL_MAX_VALUE,
2929
INTERNAL_MIN_VALUE,
3030
lowerBoundCompare,
31+
MAX_KEY_VALUE,
32+
MIN_KEY_VALUE,
3133
upperBoundCompare,
3234
valuesGetLowerBound,
3335
valuesGetUpperBound
@@ -387,7 +389,7 @@ function targetGetAscendingBound(
387389
break;
388390
case Operator.NOT_EQUAL:
389391
case Operator.NOT_IN:
390-
filterValue = INTERNAL_MIN_VALUE;
392+
filterValue = MIN_KEY_VALUE;
391393
break;
392394
default:
393395
// Remaining filters cannot be used as lower bounds.
@@ -462,7 +464,7 @@ function targetGetDescendingBound(
462464
break;
463465
case Operator.NOT_EQUAL:
464466
case Operator.NOT_IN:
465-
filterValue = INTERNAL_MAX_VALUE;
467+
filterValue = MAX_KEY_VALUE;
466468
break;
467469
default:
468470
// Remaining filters cannot be used as upper bounds.

packages/firestore/src/index/firestore_index_value_writer.ts

Lines changed: 94 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,32 +22,45 @@ import {
2222
normalizeTimestamp
2323
} from '../model/normalize';
2424
import {
25-
isVectorValue,
2625
VECTOR_MAP_VECTORS_KEY,
27-
isMaxValue
26+
detectSpecialMapType,
27+
RESERVED_BSON_TIMESTAMP_KEY,
28+
RESERVED_REGEX_KEY,
29+
RESERVED_BSON_OBJECT_ID_KEY,
30+
RESERVED_BSON_BINARY_KEY,
31+
SpecialMapValueType,
32+
RESERVED_REGEX_PATTERN_KEY,
33+
RESERVED_REGEX_OPTIONS_KEY,
34+
RESERVED_INT32_KEY
2835
} from '../model/values';
2936
import { ArrayValue, MapValue, Value } from '../protos/firestore_proto_api';
3037
import { fail } from '../util/assert';
3138
import { isNegativeZero } from '../util/types';
3239

3340
import { DirectionalIndexByteEncoder } from './directional_index_byte_encoder';
3441

35-
// Note: This code is copied from the backend. Code that is not used by
36-
// Firestore was removed.
42+
// Note: This file is copied from the backend. Code that is not used by
43+
// Firestore was removed. Code that has different behavior was modified.
3744

3845
const INDEX_TYPE_NULL = 5;
46+
const INDEX_TYPE_MIN_KEY = 7;
3947
const INDEX_TYPE_BOOLEAN = 10;
4048
const INDEX_TYPE_NAN = 13;
4149
const INDEX_TYPE_NUMBER = 15;
4250
const INDEX_TYPE_TIMESTAMP = 20;
51+
const INDEX_TYPE_BSON_TIMESTAMP = 22;
4352
const INDEX_TYPE_STRING = 25;
4453
const INDEX_TYPE_BLOB = 30;
54+
const INDEX_TYPE_BSON_BINARY = 31;
4555
const INDEX_TYPE_REFERENCE = 37;
56+
const INDEX_TYPE_BSON_OBJECT_ID = 43;
4657
const INDEX_TYPE_GEOPOINT = 45;
58+
const INDEX_TYPE_REGEX = 47;
4759
const INDEX_TYPE_ARRAY = 50;
4860
const INDEX_TYPE_VECTOR = 53;
4961
const INDEX_TYPE_MAP = 55;
5062
const INDEX_TYPE_REFERENCE_SEGMENT = 60;
63+
const INDEX_TYPE_MAX_VALUE = 999;
5164

5265
// A terminator that indicates that a truncatable value was not truncated.
5366
// This must be smaller than all other type labels.
@@ -124,11 +137,30 @@ export class FirestoreIndexValueWriter {
124137
encoder.writeNumber(geoPoint.latitude || 0);
125138
encoder.writeNumber(geoPoint.longitude || 0);
126139
} else if ('mapValue' in indexValue) {
127-
// TODO(Mila/BSON): add bson types for indexing
128-
if (isMaxValue(indexValue)) {
140+
const type = detectSpecialMapType(indexValue);
141+
if (type === SpecialMapValueType.INTERNAL_MAX) {
129142
this.writeValueTypeLabel(encoder, Number.MAX_SAFE_INTEGER);
130-
} else if (isVectorValue(indexValue)) {
143+
} else if (type === SpecialMapValueType.VECTOR) {
131144
this.writeIndexVector(indexValue.mapValue!, encoder);
145+
} else if (type === SpecialMapValueType.MAX_KEY) {
146+
this.writeValueTypeLabel(encoder, INDEX_TYPE_MAX_VALUE);
147+
} else if (type === SpecialMapValueType.MIN_KEY) {
148+
this.writeValueTypeLabel(encoder, INDEX_TYPE_MIN_KEY);
149+
} else if (type === SpecialMapValueType.BSON_BINARY) {
150+
this.writeIndexBsonBinaryData(indexValue.mapValue!, encoder);
151+
} else if (type === SpecialMapValueType.REGEX) {
152+
this.writeIndexRegex(indexValue.mapValue!, encoder);
153+
} else if (type === SpecialMapValueType.BSON_TIMESTAMP) {
154+
this.writeIndexBsonTimestamp(indexValue.mapValue!, encoder);
155+
} else if (type === SpecialMapValueType.BSON_OBJECT_ID) {
156+
this.writeIndexBsonObjectId(indexValue.mapValue!, encoder);
157+
} else if (type === SpecialMapValueType.INT32) {
158+
this.writeValueTypeLabel(encoder, INDEX_TYPE_NUMBER);
159+
encoder.writeNumber(
160+
normalizeNumber(
161+
indexValue.mapValue!.fields![RESERVED_INT32_KEY]!.integerValue!
162+
)
163+
);
132164
} else {
133165
this.writeIndexMap(indexValue.mapValue!, encoder);
134166
this.writeTruncationMarker(encoder);
@@ -202,7 +234,10 @@ export class FirestoreIndexValueWriter {
202234
encoder: DirectionalIndexByteEncoder
203235
): void {
204236
this.writeValueTypeLabel(encoder, INDEX_TYPE_REFERENCE);
205-
const path = DocumentKey.fromName(referenceValue).path;
237+
const segments: string[] = referenceValue
238+
.split('/')
239+
.filter(segment => segment.length > 0);
240+
const path = DocumentKey.fromSegments(segments.slice(5)).path;
206241
path.forEach(segment => {
207242
this.writeValueTypeLabel(encoder, INDEX_TYPE_REFERENCE_SEGMENT);
208243
this.writeUnlabeledIndexString(segment, encoder);
@@ -222,4 +257,55 @@ export class FirestoreIndexValueWriter {
222257
// references, arrays and maps).
223258
encoder.writeNumber(NOT_TRUNCATED);
224259
}
260+
261+
private writeIndexBsonTimestamp(
262+
mapValue: MapValue,
263+
encoder: DirectionalIndexByteEncoder
264+
): void {
265+
this.writeValueTypeLabel(encoder, INDEX_TYPE_BSON_TIMESTAMP);
266+
const fields = mapValue.fields || {};
267+
if (fields) {
268+
// The JS SDK encodes BSON timestamps differently than the backend.
269+
// This is due to the limitation of `number` in JS which handles up to 53-bit precision.
270+
this.writeIndexMap(
271+
fields[RESERVED_BSON_TIMESTAMP_KEY].mapValue!,
272+
encoder
273+
);
274+
}
275+
}
276+
277+
private writeIndexBsonObjectId(
278+
mapValue: MapValue,
279+
encoder: DirectionalIndexByteEncoder
280+
): void {
281+
this.writeValueTypeLabel(encoder, INDEX_TYPE_BSON_OBJECT_ID);
282+
const fields = mapValue.fields || {};
283+
const oid = fields[RESERVED_BSON_OBJECT_ID_KEY]?.stringValue || '';
284+
encoder.writeBytes(normalizeByteString(oid));
285+
}
286+
287+
private writeIndexBsonBinaryData(
288+
mapValue: MapValue,
289+
encoder: DirectionalIndexByteEncoder
290+
): void {
291+
this.writeValueTypeLabel(encoder, INDEX_TYPE_BSON_BINARY);
292+
const fields = mapValue.fields || {};
293+
const binary = fields[RESERVED_BSON_BINARY_KEY]?.bytesValue || '';
294+
encoder.writeBytes(normalizeByteString(binary));
295+
this.writeTruncationMarker(encoder);
296+
}
297+
298+
private writeIndexRegex(
299+
mapValue: MapValue,
300+
encoder: DirectionalIndexByteEncoder
301+
): void {
302+
this.writeValueTypeLabel(encoder, INDEX_TYPE_REGEX);
303+
const fields = mapValue.fields || {};
304+
const regex = fields[RESERVED_REGEX_KEY]?.mapValue?.fields || {};
305+
if (regex) {
306+
encoder.writeString(regex[RESERVED_REGEX_PATTERN_KEY]?.stringValue || '');
307+
encoder.writeString(regex[RESERVED_REGEX_OPTIONS_KEY]?.stringValue || '');
308+
}
309+
this.writeTruncationMarker(encoder);
310+
}
225311
}

packages/firestore/src/lite-api/query.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,8 @@ export function newQueryFilter(
811811
value: unknown
812812
): FieldFilter {
813813
let fieldValue: ProtoValue;
814+
validateQueryOperator(value, op);
815+
814816
if (fieldPath.isKeyField()) {
815817
if (op === Operator.ARRAY_CONTAINS || op === Operator.ARRAY_CONTAINS_ANY) {
816818
throw new FirestoreError(
@@ -1064,6 +1066,31 @@ function validateDisjunctiveFilterElements(
10641066
}
10651067
}
10661068

1069+
/**
1070+
* Validates the input string as a field comparison operator.
1071+
*/
1072+
export function validateQueryOperator(
1073+
value: unknown,
1074+
operator: Operator
1075+
): void {
1076+
if (
1077+
typeof value === 'number' &&
1078+
isNaN(value) &&
1079+
operator !== '==' &&
1080+
operator !== '!='
1081+
) {
1082+
throw new Error(
1083+
"Invalid query. You can only perform '==' and '!=' comparisons on NaN."
1084+
);
1085+
}
1086+
1087+
if (value === null && operator !== '==' && operator !== '!=') {
1088+
throw new Error(
1089+
"Invalid query. You can only perform '==' and '!=' comparisons on Null."
1090+
);
1091+
}
1092+
}
1093+
10671094
/**
10681095
* Given an operator, returns the set of operators that cannot be used with it.
10691096
*

packages/firestore/src/lite-api/user_data_writer.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,6 @@ export abstract class AbstractUserDataWriter {
8383
): unknown {
8484
switch (typeOrder(value)) {
8585
case TypeOrder.NullValue:
86-
if ('mapValue' in value) {
87-
return minKey();
88-
}
8986
return null;
9087
case TypeOrder.BooleanValue:
9188
return value.booleanValue!;
@@ -122,6 +119,8 @@ export abstract class AbstractUserDataWriter {
122119
return this.convertToBsonTimestampValue(value.mapValue!);
123120
case TypeOrder.MaxKeyValue:
124121
return maxKey();
122+
case TypeOrder.MinKeyValue:
123+
return minKey();
125124
default:
126125
throw fail('Invalid value type: ' + JSON.stringify(value));
127126
}

packages/firestore/src/model/type_order.ts

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,24 +27,25 @@ export const enum TypeOrder {
2727
// server timestamps and `MAX_VALUE` inside the SDK.
2828
// NULL and MIN_KEY sort the same.
2929
NullValue = 0,
30-
MinKeyValue = 0,
31-
BooleanValue = 1,
32-
NumberValue = 2,
33-
TimestampValue = 3,
34-
// TODO(Mila/BSON): which should come first considering indexes?
35-
BsonTimestampValue = 4,
36-
ServerTimestampValue = 5,
37-
StringValue = 6,
38-
BlobValue = 7,
39-
BsonBinaryValue = 8,
40-
RefValue = 9,
41-
BsonObjectIdValue = 10,
42-
GeoPointValue = 11,
43-
RegexValue = 12,
44-
ArrayValue = 13,
45-
VectorValue = 14,
46-
ObjectValue = 15,
47-
// TODO(Mila/BSON):should MaxKeyValue and MaxValue combined? how would this affect indexes?
48-
MaxKeyValue = 16,
30+
MinKeyValue = 1,
31+
BooleanValue = 2,
32+
// Note: all numbers (32-bit int, 64-bit int, 64-bit double, 128-bit decimal,
33+
// etc.) are sorted together numerically. The `numberEquals` function
34+
// distinguishes between different number types and compares them accordingly.
35+
NumberValue = 3,
36+
TimestampValue = 4,
37+
BsonTimestampValue = 5,
38+
ServerTimestampValue = 6,
39+
StringValue = 7,
40+
BlobValue = 8,
41+
BsonBinaryValue = 9,
42+
RefValue = 10,
43+
BsonObjectIdValue = 11,
44+
GeoPointValue = 12,
45+
RegexValue = 13,
46+
ArrayValue = 14,
47+
VectorValue = 15,
48+
ObjectValue = 16,
49+
MaxKeyValue = 17,
4950
MaxValue = 9007199254740991 // Number.MAX_SAFE_INTEGER
5051
}

0 commit comments

Comments
 (0)