diff --git a/handwritten/firestore/.jsdoc.js b/handwritten/firestore/.jsdoc.js
index 7ba807e2023c..da17b83de683 100644
--- a/handwritten/firestore/.jsdoc.js
+++ b/handwritten/firestore/.jsdoc.js
@@ -18,6 +18,12 @@
'use strict';
+// BigInt JSON serialization.
+// https://github.com/jsdoc/jsdoc/issues/1918
+BigInt.prototype.toJSON = function() {
+ return this.toString() + 'n';
+};
+
module.exports = {
opts: {
readme: './README.md',
diff --git a/handwritten/firestore/dev/src/convert.ts b/handwritten/firestore/dev/src/convert.ts
index c44e08401195..9beebba9dc18 100644
--- a/handwritten/firestore/dev/src/convert.ts
+++ b/handwritten/firestore/dev/src/convert.ts
@@ -20,7 +20,18 @@ import {ApiMapValue, ProtobufJsValue} from './types';
import {validateObject} from './validate';
import api = google.firestore.v1;
-import {RESERVED_MAP_KEY, RESERVED_MAP_KEY_VECTOR_VALUE} from './map-type';
+import {
+ RESERVED_BSON_BINARY_KEY,
+ RESERVED_INT32_KEY,
+ RESERVED_MAP_KEY,
+ RESERVED_MAP_KEY_VECTOR_VALUE,
+ RESERVED_MAX_KEY,
+ RESERVED_MIN_KEY,
+ RESERVED_BSON_OBJECT_ID_KEY,
+ RESERVED_REGEX_KEY,
+ RESERVED_BSON_TIMESTAMP_KEY,
+ RESERVED_DECIMAL128_KEY,
+} from './map-type';
/*!
* @module firestore/convert
@@ -104,6 +115,46 @@ function bytesFromJson(bytesValue: string | Uint8Array): Uint8Array {
}
}
+/**
+ * Detects the 'valueType' for cases where a map value has been used to
+ * represent another type.
+ * @param mapValue The map value to probe.
+ */
+function detectMapRepresentation(
+ mapValue: api.IMapValue | null | undefined
+): string {
+ const fields = mapValue?.fields;
+ if (fields) {
+ const props = Object.keys(fields);
+ if (
+ props.indexOf(RESERVED_MAP_KEY) !== -1 &&
+ detectValueType(fields[RESERVED_MAP_KEY]) === 'stringValue' &&
+ fields[RESERVED_MAP_KEY].stringValue === RESERVED_MAP_KEY_VECTOR_VALUE
+ ) {
+ return 'vectorValue';
+ } else if (props.indexOf(RESERVED_MIN_KEY) !== -1) {
+ return 'minKeyValue';
+ } else if (props.indexOf(RESERVED_MAX_KEY) !== -1) {
+ return 'maxKeyValue';
+ } else if (props.indexOf(RESERVED_REGEX_KEY) !== -1) {
+ return 'regexValue';
+ } else if (props.indexOf(RESERVED_BSON_OBJECT_ID_KEY) !== -1) {
+ return 'bsonObjectIdValue';
+ } else if (props.indexOf(RESERVED_INT32_KEY) !== -1) {
+ return 'int32Value';
+ } else if (props.indexOf(RESERVED_DECIMAL128_KEY) !== -1) {
+ return 'decimal128Value';
+ } else if (props.indexOf(RESERVED_BSON_TIMESTAMP_KEY) !== -1) {
+ return 'bsonTimestampValue';
+ } else if (props.indexOf(RESERVED_BSON_BINARY_KEY) !== -1) {
+ return 'bsonBinaryValue';
+ }
+ }
+
+ // If none of the above cases apply, it's a regular map.
+ return 'mapValue';
+}
+
/**
* Detects 'valueType' from a Proto3 JSON `firestore.v1.Value` proto.
*
@@ -163,19 +214,10 @@ export function detectValueType(proto: ProtobufJsValue): string {
valueType = detectedValues[0];
}
- // Special handling of mapValues used to represent other data types
+ // Special handling of mapValues which may or may not have been
+ // used to represent other data types.
if (valueType === 'mapValue') {
- const fields = proto.mapValue?.fields;
- if (fields) {
- const props = Object.keys(fields);
- if (
- props.indexOf(RESERVED_MAP_KEY) !== -1 &&
- detectValueType(fields[RESERVED_MAP_KEY]) === 'stringValue' &&
- fields[RESERVED_MAP_KEY].stringValue === RESERVED_MAP_KEY_VECTOR_VALUE
- ) {
- valueType = 'vectorValue';
- }
- }
+ valueType = detectMapRepresentation(proto.mapValue);
}
return valueType;
@@ -261,7 +303,15 @@ export function valueFromJson(fieldValue: api.IValue): api.IValue {
};
}
case 'mapValue':
- case 'vectorValue': {
+ case 'vectorValue':
+ case 'regexValue':
+ case 'bsonObjectIdValue':
+ case 'bsonBinaryValue':
+ case 'int32Value':
+ case 'decimal128Value':
+ case 'bsonTimestampValue':
+ case 'minKeyValue':
+ case 'maxKeyValue': {
const mapValue: ApiMapValue = {};
const fields = fieldValue.mapValue!.fields;
if (fields) {
diff --git a/handwritten/firestore/dev/src/field-value.ts b/handwritten/firestore/dev/src/field-value.ts
index a20ab86f8974..398453729804 100644
--- a/handwritten/firestore/dev/src/field-value.ts
+++ b/handwritten/firestore/dev/src/field-value.ts
@@ -30,6 +30,19 @@ import {
} from './validate';
import api = proto.google.firestore.v1;
+import {
+ RESERVED_BSON_BINARY_KEY,
+ RESERVED_INT32_KEY,
+ RESERVED_BSON_OBJECT_ID_KEY,
+ RESERVED_REGEX_KEY,
+ RESERVED_REGEX_OPTIONS_KEY,
+ RESERVED_REGEX_PATTERN_KEY,
+ RESERVED_BSON_TIMESTAMP_INCREMENT_KEY,
+ RESERVED_BSON_TIMESTAMP_KEY,
+ RESERVED_BSON_TIMESTAMP_SECONDS_KEY,
+ RESERVED_DECIMAL128_KEY,
+} from './map-type';
+import {Quadruple} from './quadruple';
/**
* Represent a vector type in Firestore documents.
@@ -83,6 +96,386 @@ export class VectorValue implements firestore.VectorValue {
}
}
+/**
+ * Represents the Firestore "Min Key" data type.
+ *
+ * @class MinKey
+ */
+export class MinKey implements firestore.MinKey {
+ private static MIN_KEY_VALUE_INSTANCE = new MinKey();
+ readonly type = 'MinKey';
+
+ private constructor() {}
+
+ /**
+ * @private
+ * @internal
+ */
+ static instance(): MinKey {
+ return MinKey.MIN_KEY_VALUE_INSTANCE;
+ }
+
+ /**
+ * @private
+ * @internal
+ */
+ _toProto(serializer: Serializer): api.IValue {
+ return serializer.encodeMinKey();
+ }
+}
+
+/**
+ * Represents the Firestore "Max Key" data type.
+ *
+ * @class MaxKey
+ */
+export class MaxKey implements firestore.MaxKey {
+ private static MAX_KEY_VALUE_INSTANCE = new MaxKey();
+ readonly type = 'MaxKey';
+
+ /**
+ * @private
+ * @internal
+ */
+ private constructor() {}
+
+ /**
+ * @private
+ * @internal
+ */
+ static instance(): MaxKey {
+ return MaxKey.MAX_KEY_VALUE_INSTANCE;
+ }
+
+ /**
+ * @private
+ * @internal
+ */
+ _toProto(serializer: Serializer): api.IValue {
+ return serializer.encodeMaxKey();
+ }
+}
+
+/**
+ * Represents a regular expression type in Firestore documents.
+ *
+ * @class RegexValue
+ */
+export class RegexValue implements firestore.RegexValue {
+ /**
+ * Creates a new regular expression value with the given pattern and options.
+ *
+ * @param pattern - The pattern to use for this regex value.
+ * @param options - The options to use for this regex value.
+ *
+ * @private
+ * @internal
+ */
+ constructor(
+ readonly pattern: string,
+ readonly options: string
+ ) {}
+
+ /**
+ * @private
+ * @internal
+ */
+ _toProto(serializer: Serializer): api.IValue {
+ return serializer.encodeRegex(this.pattern, this.options);
+ }
+
+ /**
+ * @private
+ * @internal
+ */
+ static _fromProto(proto: api.IValue): RegexValue {
+ const fields = proto.mapValue!.fields;
+ const pattern =
+ fields?.[RESERVED_REGEX_KEY]?.mapValue?.fields?.[
+ RESERVED_REGEX_PATTERN_KEY
+ ]?.stringValue ?? '';
+ const options =
+ fields?.[RESERVED_REGEX_KEY]?.mapValue?.fields?.[
+ RESERVED_REGEX_OPTIONS_KEY
+ ]?.stringValue ?? '';
+ return new RegexValue(pattern, options);
+ }
+
+ /**
+ * Returns `true` if the two regex values have the same pattern and options, returns `false` otherwise.
+ */
+ isEqual(other: RegexValue): boolean {
+ return this.pattern === other.pattern && this.options === other.options;
+ }
+}
+
+/**
+ * Represents a BSON ObjectId type in Firestore documents.
+ *
+ * @class BsonObjectId
+ */
+export class BsonObjectId implements firestore.BsonObjectId {
+ /**
+ * Creates a new BSON ObjectId value with the given value.
+ *
+ * @param value - The 24-character hex string representing the ObjectId.
+ *
+ * @private
+ * @internal
+ */
+ constructor(readonly value: string) {}
+
+ /**
+ * @private
+ * @internal
+ */
+ _toProto(serializer: Serializer): api.IValue {
+ return serializer.encodeBsonObjectId(this.value);
+ }
+
+ /**
+ * @private
+ * @internal
+ */
+ static _fromProto(proto: api.IValue): BsonObjectId {
+ const oid =
+ proto.mapValue!.fields?.[RESERVED_BSON_OBJECT_ID_KEY]?.stringValue ?? '';
+ return new BsonObjectId(oid);
+ }
+
+ /**
+ * Returns `true` if the two BsonObjectId values have the same pattern and options, returns `false` otherwise.
+ */
+ isEqual(other: BsonObjectId): boolean {
+ return this.value === other.value;
+ }
+}
+
+/** Represents a 32-bit integer type in Firestore documents. */
+export class Int32Value implements firestore.Int32Value {
+ /**
+ * Creates a new 32-bit signed integer value.
+ *
+ * @param value - The number whose 32-bit representation will be used.
+ *
+ * Note: values larger than the largest 32-bit signed integer,
+ * or smaller than the smallest 32-bit signed integer are invalid
+ * and will get rejected.
+ *
+ * @private
+ * @internal
+ */
+ constructor(readonly value: number) {}
+
+ /**
+ * @private
+ * @internal
+ */
+ _toProto(serializer: Serializer): api.IValue {
+ return serializer.encodeInt32(this.value);
+ }
+
+ /**
+ * @private
+ * @internal
+ */
+ static _fromProto(proto: api.IValue): Int32Value {
+ const value = Number(
+ proto.mapValue!.fields?.[RESERVED_INT32_KEY]?.integerValue
+ );
+ return new Int32Value(value);
+ }
+
+ /**
+ * Returns true if this `Int32Value` is equal to the provided one.
+ *
+ * @param other The `Int32Value` to compare against.
+ * @return 'true' if this `Int32Value` is equal to the provided one.
+ */
+ isEqual(other: Int32Value): boolean {
+ return this.value === other.value;
+ }
+}
+
+/** Represents a 128-bit decimal type in Firestore documents. */
+export class Decimal128Value implements firestore.Decimal128Value {
+ /**
+ * Creates a new 128-bit decimal value.
+ *
+ * @param value - The string representation of the 128-bit decimal.
+ *
+ * @private
+ * @internal
+ */
+ constructor(readonly value: string) {}
+
+ /**
+ * @private
+ * @internal
+ */
+ _toProto(serializer: Serializer): api.IValue {
+ return serializer.encodeDecimal128(this.value);
+ }
+
+ /**
+ * @private
+ * @internal
+ */
+ static _fromProto(proto: api.IValue): Decimal128Value {
+ const value =
+ proto.mapValue!.fields?.[RESERVED_DECIMAL128_KEY]?.stringValue || '';
+ return new Decimal128Value(value);
+ }
+
+ /**
+ * Returns true if this `Decimal128Value` is equal to the provided one.
+ *
+ * @param other The `Decimal128Value` to compare against.
+ * @return 'true' if this `Decimal128Value` is equal to the provided one.
+ */
+ isEqual(other: Decimal128Value): boolean {
+ const lhs = Quadruple.fromString(this.value);
+ const rhs = Quadruple.fromString(other.value);
+
+ // Firestore considers -0 and +0 to be equal, but Quadruple does not.
+ if (lhs.isZero() && rhs.isZero()) {
+ return true;
+ }
+
+ return lhs.compareTo(rhs) === 0;
+ }
+}
+
+/** Represents a Request Timestamp type in Firestore documents. */
+export class BsonTimestamp implements firestore.BsonTimestamp {
+ /**
+ * Creates a new BSON Timestamp from the given values.
+ *
+ * @param seconds - The seconds value to be used for this BSON timestamp.
+ * @param increment - The increment value to be used for this BSON timestamp.
+ *
+ * Note: negative values and values larger than the largest 32-bit
+ * unsigned integer are invalid and will get rejected.
+ *
+ * @private
+ * @internal
+ */
+ constructor(
+ readonly seconds: number,
+ readonly increment: number
+ ) {
+ if (seconds < 0 || seconds > 4294967295) {
+ throw new Error(
+ "BsonTimestamp 'seconds' must be in the range of a 32-bit unsigned integer."
+ );
+ }
+ if (increment < 0 || increment > 4294967295) {
+ throw new Error(
+ "BsonTimestamp 'increment' must be in the range of a 32-bit unsigned integer."
+ );
+ }
+ }
+
+ /**
+ * @private
+ * @internal
+ */
+ _toProto(serializer: Serializer): api.IValue {
+ return serializer.encodeBsonTimestamp(this.seconds, this.increment);
+ }
+
+ /**
+ * @private
+ * @internal
+ */
+ static _fromProto(proto: api.IValue): BsonTimestamp {
+ const fields = proto.mapValue!.fields?.[RESERVED_BSON_TIMESTAMP_KEY];
+ const seconds = Number(
+ fields?.mapValue?.fields?.[RESERVED_BSON_TIMESTAMP_SECONDS_KEY]
+ ?.integerValue
+ );
+ const increment = Number(
+ fields?.mapValue?.fields?.[RESERVED_BSON_TIMESTAMP_INCREMENT_KEY]
+ ?.integerValue
+ );
+ return new BsonTimestamp(seconds, increment);
+ }
+
+ /**
+ * Returns true if this `BsonTimestamp` is equal to the provided one.
+ *
+ * @param other The `BsonTimestamp` to compare against.
+ * @return 'true' if this `BsonTimestamp` is equal to the provided one.
+ */
+ isEqual(other: BsonTimestamp): boolean {
+ return this.seconds === other.seconds && this.increment === other.increment;
+ }
+}
+
+/** Represents a BSON Binary Data type in Firestore documents. */
+export class BsonBinaryData implements firestore.BsonBinaryData {
+ /**
+ * Creates a new BSON Binary Data from the given values.
+ *
+ * @param subtype - The subtype of the data.
+ * @param data - The byte array that contains the data.
+ *
+ * @private
+ * @internal
+ */
+ constructor(
+ readonly subtype: number,
+ readonly data: Uint8Array
+ ) {
+ // By definition the subtype should be 1 byte and should therefore
+ // have a value between 0 and 255
+ if (subtype < 0 || subtype > 255) {
+ throw new Error(
+ 'The subtype for BsonBinaryData must be a value in the inclusive [0, 255] range.'
+ );
+ }
+ }
+
+ /**
+ * @private
+ * @internal
+ */
+ _toProto(serializer: Serializer): api.IValue {
+ return serializer.encodeBsonBinaryData(this.subtype, this.data);
+ }
+
+ /**
+ * @private
+ * @internal
+ */
+ static _fromProto(proto: api.IValue): BsonBinaryData {
+ const fields = proto.mapValue!.fields?.[RESERVED_BSON_BINARY_KEY];
+ const subtypeAndData = fields?.bytesValue;
+ if (!subtypeAndData) {
+ throw new Error('Received incorrect bytesValue for BsonBinaryData');
+ }
+ if (subtypeAndData.length === 0) {
+ throw new Error('Received empty bytesValue for BsonBinaryData');
+ }
+ const subtype = subtypeAndData[0];
+ const data = subtypeAndData.slice(1);
+ return new BsonBinaryData(subtype, data);
+ }
+
+ /**
+ * Returns true if this `BsonBinaryData` is equal to the provided one.
+ *
+ * @param other The `BsonBinaryData` to compare against.
+ * @return 'true' if this `BsonBinaryData` is equal to the provided one.
+ */
+ isEqual(other: BsonBinaryData): boolean {
+ return (
+ this.subtype === other.subtype &&
+ Buffer.from(this.data).equals(other.data)
+ );
+ }
+}
+
/**
* Sentinel values that can be used when writing documents with set(), create()
* or update().
diff --git a/handwritten/firestore/dev/src/index.ts b/handwritten/firestore/dev/src/index.ts
index 5218efaf939e..1176ae5fe561 100644
--- a/handwritten/firestore/dev/src/index.ts
+++ b/handwritten/firestore/dev/src/index.ts
@@ -112,7 +112,18 @@ export {BulkWriter} from './bulk-writer';
export type {BulkWriterError} from './bulk-writer';
export type {BundleBuilder} from './bundle';
export {DocumentSnapshot, QueryDocumentSnapshot} from './document';
-export {FieldValue, VectorValue} from './field-value';
+export {
+ FieldValue,
+ VectorValue,
+ MinKey,
+ MaxKey,
+ BsonObjectId,
+ BsonTimestamp,
+ BsonBinaryData,
+ Decimal128Value,
+ RegexValue,
+ Int32Value,
+} from './field-value';
export {Filter} from './filter';
export {WriteBatch, WriteResult} from './write-batch';
export {Transaction} from './transaction';
diff --git a/handwritten/firestore/dev/src/map-type.ts b/handwritten/firestore/dev/src/map-type.ts
index f876220714c6..95ab56f09d87 100644
--- a/handwritten/firestore/dev/src/map-type.ts
+++ b/handwritten/firestore/dev/src/map-type.ts
@@ -17,3 +17,28 @@
export const RESERVED_MAP_KEY = '__type__';
export const RESERVED_MAP_KEY_VECTOR_VALUE = '__vector__';
export const VECTOR_MAP_VECTORS_KEY = 'value';
+
+export const RESERVED_MIN_KEY = '__min__';
+export const RESERVED_MAX_KEY = '__max__';
+
+// For Regex type
+export const RESERVED_REGEX_KEY = '__regex__';
+export const RESERVED_REGEX_PATTERN_KEY = 'pattern';
+export const RESERVED_REGEX_OPTIONS_KEY = 'options';
+
+// For BSON ObjectId type
+export const RESERVED_BSON_OBJECT_ID_KEY = '__oid__';
+
+// For Int32 type
+export const RESERVED_INT32_KEY = '__int__';
+
+// For Decimal128 type
+export const RESERVED_DECIMAL128_KEY = '__decimal128__';
+
+// For BSON Timestamp
+export const RESERVED_BSON_TIMESTAMP_KEY = '__request_timestamp__';
+export const RESERVED_BSON_TIMESTAMP_SECONDS_KEY = 'seconds';
+export const RESERVED_BSON_TIMESTAMP_INCREMENT_KEY = 'increment';
+
+// For BSON Binary Data
+export const RESERVED_BSON_BINARY_KEY = '__binary__';
diff --git a/handwritten/firestore/dev/src/order.ts b/handwritten/firestore/dev/src/order.ts
index cca7d744cd8a..14d47b0fb81d 100644
--- a/handwritten/firestore/dev/src/order.ts
+++ b/handwritten/firestore/dev/src/order.ts
@@ -20,22 +20,45 @@ import {QualifiedResourcePath} from './path';
import {ApiMapValue} from './types';
import api = google.firestore.v1;
+import {
+ RESERVED_BSON_BINARY_KEY,
+ RESERVED_BSON_OBJECT_ID_KEY,
+ RESERVED_BSON_TIMESTAMP_INCREMENT_KEY,
+ RESERVED_BSON_TIMESTAMP_KEY,
+ RESERVED_BSON_TIMESTAMP_SECONDS_KEY,
+ RESERVED_DECIMAL128_KEY,
+ RESERVED_INT32_KEY,
+ RESERVED_REGEX_KEY,
+ RESERVED_REGEX_OPTIONS_KEY,
+ RESERVED_REGEX_PATTERN_KEY,
+} from './map-type';
+import {Quadruple} from './quadruple';
/*!
* The type order as defined by the backend.
*/
enum TypeOrder {
+ // NULL and MIN_KEY sort the same.
NULL = 0,
- BOOLEAN = 1,
- NUMBER = 2,
- TIMESTAMP = 3,
- STRING = 4,
- BLOB = 5,
- REF = 6,
- GEO_POINT = 7,
- ARRAY = 8,
- VECTOR = 9,
- OBJECT = 10,
+ MIN_KEY = 1,
+ BOOLEAN = 2,
+ // Note: all numbers (32-bit int, 64-bit int, 64-bit double, 128-bit decimal,
+ // etc.) are sorted together numerically. The `compareNumberProtos` function
+ // distinguishes between different number types and compares them accordingly.
+ NUMBER = 3,
+ TIMESTAMP = 4,
+ BSON_TIMESTAMP = 5,
+ STRING = 6,
+ BLOB = 7,
+ BSON_BINARY = 8,
+ REF = 9,
+ BSON_OBJECT_ID = 10,
+ GEO_POINT = 11,
+ REGEX = 12,
+ ARRAY = 13,
+ VECTOR = 14,
+ OBJECT = 15,
+ MAX_KEY = 16,
}
/*!
@@ -48,10 +71,15 @@ function typeOrder(val: api.IValue): TypeOrder {
switch (valueType) {
case 'nullValue':
return TypeOrder.NULL;
+ case 'minKeyValue':
+ return TypeOrder.MIN_KEY;
case 'integerValue':
- return TypeOrder.NUMBER;
+ case 'int32Value':
+ case 'decimal128Value':
case 'doubleValue':
return TypeOrder.NUMBER;
+ case 'bsonTimestampValue':
+ return TypeOrder.BSON_TIMESTAMP;
case 'stringValue':
return TypeOrder.STRING;
case 'booleanValue':
@@ -62,14 +90,22 @@ function typeOrder(val: api.IValue): TypeOrder {
return TypeOrder.TIMESTAMP;
case 'geoPointValue':
return TypeOrder.GEO_POINT;
+ case 'regexValue':
+ return TypeOrder.REGEX;
+ case 'bsonObjectIdValue':
+ return TypeOrder.BSON_OBJECT_ID;
case 'bytesValue':
return TypeOrder.BLOB;
+ case 'bsonBinaryValue':
+ return TypeOrder.BSON_BINARY;
case 'referenceValue':
return TypeOrder.REF;
case 'mapValue':
return TypeOrder.OBJECT;
case 'vectorValue':
return TypeOrder.VECTOR;
+ case 'maxKeyValue':
+ return TypeOrder.MAX_KEY;
default:
throw new Error('Unexpected value type: ' + valueType);
}
@@ -119,18 +155,74 @@ function compareNumbers(left: number, right: number): number {
* @internal
*/
function compareNumberProtos(left: api.IValue, right: api.IValue): number {
- let leftValue, rightValue;
- if (left.integerValue !== undefined) {
- leftValue = Number(left.integerValue!);
- } else {
- leftValue = Number(left.doubleValue!);
+ // If either number is Decimal128, we cast both to wider (128-bit)
+ // representation, and compare those.
+ if (
+ detectValueType(left) === 'decimal128Value' ||
+ detectValueType(right) === 'decimal128Value'
+ ) {
+ const lhs = convertNumberToQuadruple(left);
+ const rhs = convertNumberToQuadruple(right);
+
+ // Firestore sorts `NaN`s smaller than all numbers, but Quadruple considers
+ // `NaN`s bigger than all numbers.
+ if (lhs.isNaN()) {
+ return rhs.isNaN() ? 0 : -1;
+ } else if (rhs.isNaN()) {
+ // lhs is not NaN, and rhs is NaN.
+ return 1;
+ }
+
+ // Firestore considers -0 and +0 to be equal, but Quadruple does not.
+ if (lhs.isZero() && rhs.isZero()) {
+ return 0;
+ }
+
+ return lhs.compareTo(rhs);
}
- if (right.integerValue !== undefined) {
- rightValue = Number(right.integerValue);
+
+ return compareNumbers(
+ convertProtoValueToNumber(left),
+ convertProtoValueToNumber(right)
+ );
+}
+
+/*!
+ * Converts the given proto value to a `number`.
+ * Throws an exception if the value is larger than 64-bit value or is not numeric.
+ *
+ * @private
+ * @internal
+ */
+function convertProtoValueToNumber(value: api.IValue): number {
+ if (value.integerValue !== undefined) {
+ return Number(value.integerValue!);
+ } else if (value.doubleValue !== undefined) {
+ return Number(value.doubleValue!);
+ } else if (detectValueType(value) === 'int32Value') {
+ return Number(value.mapValue!.fields?.[RESERVED_INT32_KEY]?.integerValue);
+ }
+
+ throw new Error(
+ 'convertProtoValueToNumber was called on an unsupported type.'
+ );
+}
+
+/*!
+ * Converts the given proto value to a `Quadruple`.
+ * Throws an exception if the value is not numeric.
+ *
+ * @private
+ * @internal
+ */
+function convertNumberToQuadruple(value: api.IValue): Quadruple {
+ if (detectValueType(value) === 'decimal128Value') {
+ return Quadruple.fromString(
+ value.mapValue!.fields![RESERVED_DECIMAL128_KEY].stringValue!
+ );
} else {
- rightValue = Number(right.doubleValue!);
+ return Quadruple.fromNumber(convertProtoValueToNumber(value));
}
- return compareNumbers(leftValue, rightValue);
}
/*!
@@ -148,6 +240,51 @@ function compareTimestamps(
return primitiveComparator(left.nanos || 0, right.nanos || 0);
}
+/*!
+ * @private
+ * @internal
+ */
+function compareBsonTimestamps(left: api.IValue, right: api.IValue): number {
+ // First order by seconds, then order by increment values.
+ const leftFields = left.mapValue!.fields?.[RESERVED_BSON_TIMESTAMP_KEY];
+ const leftSeconds = Number(
+ leftFields?.mapValue?.fields?.[RESERVED_BSON_TIMESTAMP_SECONDS_KEY]
+ ?.integerValue
+ );
+ const leftIncrement = Number(
+ leftFields?.mapValue?.fields?.[RESERVED_BSON_TIMESTAMP_INCREMENT_KEY]
+ ?.integerValue
+ );
+ const rightFields = right.mapValue!.fields?.[RESERVED_BSON_TIMESTAMP_KEY];
+ const rightSeconds = Number(
+ rightFields?.mapValue?.fields?.[RESERVED_BSON_TIMESTAMP_SECONDS_KEY]
+ ?.integerValue
+ );
+ const rightIncrement = Number(
+ rightFields?.mapValue?.fields?.[RESERVED_BSON_TIMESTAMP_INCREMENT_KEY]
+ ?.integerValue
+ );
+ const secondsDiff = compareNumbers(leftSeconds, rightSeconds);
+ return secondsDiff !== 0
+ ? secondsDiff
+ : compareNumbers(leftIncrement, rightIncrement);
+}
+
+/*!
+ * @private
+ * @internal
+ */
+function compareBsonBinaryData(left: api.IValue, right: api.IValue): number {
+ const leftBytes =
+ left.mapValue!.fields?.[RESERVED_BSON_BINARY_KEY]?.bytesValue;
+ const rightBytes =
+ right.mapValue!.fields?.[RESERVED_BSON_BINARY_KEY]?.bytesValue;
+ if (!rightBytes || !leftBytes) {
+ throw new Error('Received incorrect bytesValue for BsonBinaryData');
+ }
+ return Buffer.compare(Buffer.from(leftBytes), Buffer.from(rightBytes));
+}
+
/*!
* @private
* @internal
@@ -248,6 +385,47 @@ function compareVectors(left: ApiMapValue, right: ApiMapValue): number {
return compareArrays(leftArray, rightArray);
}
+/*!
+ * @private
+ * @internal
+ */
+function compareRegex(left: api.IValue, right: api.IValue): number {
+ const lhsPattern =
+ left.mapValue!.fields?.[RESERVED_REGEX_KEY]?.mapValue?.fields?.[
+ RESERVED_REGEX_PATTERN_KEY
+ ]?.stringValue ?? '';
+ const lhsOptions =
+ left.mapValue!.fields?.[RESERVED_REGEX_KEY]?.mapValue?.fields?.[
+ RESERVED_REGEX_OPTIONS_KEY
+ ]?.stringValue ?? '';
+ const rhsPattern =
+ right.mapValue!.fields?.[RESERVED_REGEX_KEY]?.mapValue?.fields?.[
+ RESERVED_REGEX_PATTERN_KEY
+ ]?.stringValue ?? '';
+ const rhsOptions =
+ right.mapValue!.fields?.[RESERVED_REGEX_KEY]?.mapValue?.fields?.[
+ RESERVED_REGEX_OPTIONS_KEY
+ ]?.stringValue ?? '';
+
+ // First order by patterns, and then options.
+ const patternDiff = compareUtf8Strings(lhsPattern, rhsPattern);
+ return patternDiff !== 0
+ ? patternDiff
+ : compareUtf8Strings(lhsOptions, rhsOptions);
+}
+
+/*!
+ * @private
+ * @internal
+ */
+function compareBsonObjectIds(left: api.IValue, right: api.IValue): number {
+ const lhs =
+ left.mapValue!.fields?.[RESERVED_BSON_OBJECT_ID_KEY]?.stringValue ?? '';
+ const rhs =
+ right.mapValue!.fields?.[RESERVED_BSON_OBJECT_ID_KEY]?.stringValue ?? '';
+ return compareUtf8Strings(lhs, rhs);
+}
+
/*!
* Compare strings in UTF-8 encoded byte order
* @private
@@ -330,8 +508,12 @@ export function compare(left: api.IValue, right: api.IValue): number {
// So they are the same type.
switch (leftType) {
+ // All Nulls are all equal.
+ // All MinKeys are all equal.
+ // All MaxKeys are all equal.
case TypeOrder.NULL:
- // Nulls are all equal.
+ case TypeOrder.MIN_KEY:
+ case TypeOrder.MAX_KEY:
return 0;
case TypeOrder.BOOLEAN:
return primitiveComparator(left.booleanValue!, right.booleanValue!);
@@ -341,12 +523,20 @@ export function compare(left: api.IValue, right: api.IValue): number {
return compareNumberProtos(left, right);
case TypeOrder.TIMESTAMP:
return compareTimestamps(left.timestampValue!, right.timestampValue!);
+ case TypeOrder.BSON_TIMESTAMP:
+ return compareBsonTimestamps(left, right);
case TypeOrder.BLOB:
return compareBlobs(left.bytesValue!, right.bytesValue!);
+ case TypeOrder.BSON_BINARY:
+ return compareBsonBinaryData(left, right);
case TypeOrder.REF:
return compareReferenceProtos(left, right);
case TypeOrder.GEO_POINT:
return compareGeoPoints(left.geoPointValue!, right.geoPointValue!);
+ case TypeOrder.REGEX:
+ return compareRegex(left, right);
+ case TypeOrder.BSON_OBJECT_ID:
+ return compareBsonObjectIds(left, right);
case TypeOrder.ARRAY:
return compareArrays(
left.arrayValue!.values || [],
diff --git a/handwritten/firestore/dev/src/quadruple.ts b/handwritten/firestore/dev/src/quadruple.ts
new file mode 100644
index 000000000000..860d0e3f732a
--- /dev/null
+++ b/handwritten/firestore/dev/src/quadruple.ts
@@ -0,0 +1,272 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {QuadrupleBuilder} from './quadruple_builder';
+
+/*
+ * @private
+ * @internal
+ */
+export class Quadruple {
+ constructor(
+ negative: boolean,
+ biasedExponent: number,
+ mantHi: bigint,
+ mantLo: bigint
+ ) {
+ this.negative = negative;
+ this.biasedExponent = biasedExponent;
+ this.mantHi = mantHi;
+ this.mantLo = mantLo;
+ }
+ // The fields containing the value of the instance
+ negative: boolean;
+ biasedExponent: number;
+ mantHi: bigint;
+ mantLo: bigint;
+ static #exponentOfInfinity = Number(QuadrupleBuilder.EXPONENT_OF_INFINITY);
+ static positiveZero: Quadruple = new Quadruple(false, 0, 0n, 0n);
+ static negativeZero: Quadruple = new Quadruple(true, 0, 0n, 0n);
+ static NaN: Quadruple = new Quadruple(
+ false,
+ Quadruple.#exponentOfInfinity,
+ 1n << 63n,
+ 0n
+ );
+ static negativeInfinity: Quadruple = new Quadruple(
+ true,
+ Quadruple.#exponentOfInfinity,
+ 0n,
+ 0n
+ );
+ static positiveInfinity: Quadruple = new Quadruple(
+ false,
+ Quadruple.#exponentOfInfinity,
+ 0n,
+ 0n
+ );
+ static #minLong: Quadruple = new Quadruple(true, Quadruple.#bias(63), 0n, 0n);
+ static #positiveOne: Quadruple = new Quadruple(
+ false,
+ Quadruple.#bias(0),
+ 0n,
+ 0n
+ );
+ static #negativeOne: Quadruple = new Quadruple(
+ true,
+ Quadruple.#bias(0),
+ 0n,
+ 0n
+ );
+ /** Return the (unbiased) exponent of this {@link Quadruple}. */
+ exponent(): number {
+ return this.biasedExponent - QuadrupleBuilder.EXPONENT_BIAS;
+ }
+ /** Return true if this {@link Quadruple} is -0 or +0 */
+ isZero(): boolean {
+ return (
+ this.biasedExponent === 0 && this.mantHi === 0n && this.mantLo === 0n
+ );
+ }
+ /** Return true if this {@link Quadruple} is -infinity or +infinity */
+ isInfinite(): boolean {
+ return (
+ this.biasedExponent === Quadruple.#exponentOfInfinity &&
+ this.mantHi === 0n &&
+ this.mantLo === 0n
+ );
+ }
+ /** Return true if this {@link Quadruple} is a NaN. */
+ isNaN(): boolean {
+ return (
+ this.biasedExponent === Quadruple.#exponentOfInfinity &&
+ !(this.mantHi === 0n && this.mantLo === 0n)
+ );
+ }
+ /** Compare two quadruples, with -0 < 0, and all NaNs equal and larger than all numbers. */
+ compareTo(other: Quadruple): number {
+ if (this.isNaN()) {
+ return other.isNaN() ? 0 : 1;
+ }
+ if (other.isNaN()) {
+ return -1;
+ }
+ let lessThan;
+ let greaterThan;
+ if (this.negative) {
+ if (!other.negative) {
+ return -1;
+ }
+ lessThan = 1;
+ greaterThan = -1;
+ } else {
+ if (other.negative) {
+ return 1;
+ }
+ lessThan = -1;
+ greaterThan = 1;
+ }
+ if (this.biasedExponent < other.biasedExponent) {
+ return lessThan;
+ }
+ if (this.biasedExponent > other.biasedExponent) {
+ return greaterThan;
+ }
+ if (this.mantHi < other.mantHi) {
+ return lessThan;
+ }
+ if (this.mantHi > other.mantHi) {
+ return greaterThan;
+ }
+ if (this.mantLo < other.mantLo) {
+ return lessThan;
+ }
+ if (this.mantLo > other.mantLo) {
+ return greaterThan;
+ }
+ return 0;
+ }
+ debug(): string {
+ return (
+ (this.negative ? '+' : '-') +
+ Quadruple.#hex(this.mantHi) +
+ Quadruple.#hex(this.mantLo) +
+ '*2^' +
+ this.exponent()
+ );
+ }
+ static #hex(n: bigint): string {
+ return n.toString(16).padStart(16, '0');
+ }
+ static fromNumber(value: number): Quadruple {
+ if (isNaN(value)) {
+ return Quadruple.NaN;
+ }
+ if (!isFinite(value)) {
+ return value < 0
+ ? Quadruple.negativeInfinity
+ : Quadruple.positiveInfinity;
+ }
+ if (value === 0) {
+ // -0 === 0 and Math.sign(-0) = -0, so can't be used to distinguish 0 and -0.
+ // But 1/-0=-infinity, and 1/0=infinity, and Math.sign does "work" on infinity.
+ return Math.sign(1 / value) > 0
+ ? Quadruple.positiveZero
+ : Quadruple.negativeZero;
+ }
+ const array = new DataView(new ArrayBuffer(8));
+ array.setFloat64(0, value);
+ const bits = array.getBigUint64(0);
+ let mantHi = BigInt.asUintN(64, bits << 12n);
+ let exponent = Number(bits >> 52n) & 0x7ff;
+ if (exponent === 0) {
+ // subnormal - mantHi cannot be zero as that means value===+/-0
+ const leadingZeros = QuadrupleBuilder.clz64(mantHi);
+ mantHi = leadingZeros < 63 ? mantHi << BigInt(leadingZeros + 1) : 0n;
+ exponent = -leadingZeros;
+ }
+ return new Quadruple(
+ value < 0,
+ Quadruple.#bias(exponent - 1023),
+ mantHi,
+ 0n
+ );
+ }
+ /**
+ * Converts a decimal number to a {@link Quadruple}. The supported format (no whitespace allowed)
+ * is:
+ *
+ *
+ * - NaN for Quadruple.NaN
+ *
- Infinity or +Infinity for Quadruple.POSITIVE_INFINITY
+ *
- -Infinity for Quadruple.NEGATIVE_INFINITY
+ *
- regular expression: [+-]?[0-9]*(.[0-9]*)?([eE][+-]?[0-9]+)? - the exponent cannot be more
+ * than 9 digits, and the whole string cannot be empty
+ *
+ */
+ static fromString(s: string): Quadruple {
+ if (s === 'NaN') {
+ return Quadruple.NaN;
+ }
+ if (s === '-Infinity') {
+ return Quadruple.negativeInfinity;
+ }
+ if (s === 'Infinity' || s === '+Infinity') {
+ return Quadruple.positiveInfinity;
+ }
+ const digits: number[] = new Array(s.length).fill(0);
+ let i = 0;
+ let j = 0;
+ let exponent = 0;
+ let negative = false;
+ if (s[i] === '-') {
+ negative = true;
+ i++;
+ } else if (s[i] === '+') {
+ i++;
+ }
+ while (Quadruple.#isDigit(s, i)) {
+ digits[j++] = Quadruple.#digit(s, i++);
+ }
+ if (s[i] === '.') {
+ const decimal = ++i;
+ while (Quadruple.#isDigit(s, i)) {
+ digits[j++] = Quadruple.#digit(s, i++);
+ }
+ exponent = decimal - i;
+ }
+ if (s[i] === 'e' || s[i] === 'E') {
+ let exponentValue = 0;
+ i++;
+ let exponentSign = 1;
+ if (s[i] === '-') {
+ exponentSign = -1;
+ i++;
+ } else if (s[i] === '+') {
+ i++;
+ }
+ const firstExponent = i;
+ while (Quadruple.#isDigit(s, i)) {
+ exponentValue = exponentValue * 10 + Quadruple.#digit(s, i++);
+ if (i - firstExponent > 9) {
+ throw new Error('Exponent too large ' + s);
+ }
+ }
+ if (i === firstExponent) {
+ throw new Error('Invalid number ' + s);
+ }
+ exponent += exponentValue * exponentSign;
+ }
+ if (j === 0 || i !== s.length) {
+ throw new Error('Invalid number ' + s);
+ }
+ const parsed = QuadrupleBuilder.parseDecimal(digits.slice(0, j), exponent);
+ return new Quadruple(
+ negative,
+ parsed.exponent,
+ parsed.mantHi,
+ parsed.mantLo
+ );
+ }
+ static #isDigit(s: string, i: number): boolean {
+ const cp = s.codePointAt(i);
+ return cp !== undefined && cp >= 48 && cp <= 57;
+ }
+ static #digit(s: string, i: number): number {
+ return s.codePointAt(i)! - 48;
+ }
+ static #bias(exponent: number): number {
+ return exponent + QuadrupleBuilder.EXPONENT_BIAS;
+ }
+}
diff --git a/handwritten/firestore/dev/src/quadruple_builder.ts b/handwritten/firestore/dev/src/quadruple_builder.ts
new file mode 100644
index 000000000000..0ea198bbd3a4
--- /dev/null
+++ b/handwritten/firestore/dev/src/quadruple_builder.ts
@@ -0,0 +1,1062 @@
+// Copyright 2021 M.Vokhmentsev
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/*
+ * @private
+ * @internal
+ */
+export class QuadrupleBuilder {
+ static parseDecimal(digits: number[], exp10: number): QuadrupleBuilder {
+ const q = new QuadrupleBuilder();
+ q.parse(digits, exp10);
+ return q;
+ }
+ // The fields containing the value of the instance
+ exponent = 0;
+ mantHi = 0n;
+ mantLo = 0n;
+ // 2^192 = 6.277e57, so the 58-th digit after point may affect the result
+ static MAX_MANTISSA_LENGTH = 59;
+ // Max value of the decimal exponent, corresponds to EXPONENT_OF_MAX_VALUE
+ static MAX_EXP10 = 646456993;
+ // Min value of the decimal exponent, corresponds to EXPONENT_OF_MIN_NORMAL
+ static MIN_EXP10 = -646457032;
+ // (2^63) / 10 =~ 9.223372e17
+ // eslint-disable-next-line @typescript-eslint/no-loss-of-precision
+ static TWO_POW_63_DIV_10 = 922337203685477580.0;
+ // Just for convenience: 0x8000_0000_0000_0000L
+ static HIGH_BIT = 0x8000_0000_0000_0000n;
+ // Just for convenience: 0x8000_0000L, 2^31
+ static POW_2_31 = 2147483648.0;
+ // Just for convenience: 0x0000_0000_FFFF_FFFFL
+ static LOWER_32_BITS = 0x0000_0000_ffff_ffffn;
+ // Just for convenience: 0xFFFF_FFFF_0000_0000L;
+ static HIGHER_32_BITS = 0xffff_ffff_0000_0000n;
+ // Approximate value of log2(10)
+ static LOG2_10 = Math.log(10) / Math.log(2);
+ // Approximate value of log2(e)
+ static LOG2_E = 1 / Math.log(2.0);
+ // The value of the exponent (biased) corresponding to {@code 1.0 == 2^0}; equals to 2_147_483_647
+ // ({@code 0x7FFF_FFFF}).
+ static EXPONENT_BIAS = 0x7fff_ffff;
+ // The value of the exponent (biased), corresponding to {@code Infinity}, {@code _Infinty}, and
+ // {@code NaN}
+ static EXPONENT_OF_INFINITY = 0xffff_ffffn;
+ // An array of positive powers of two, each value consists of 4 longs: decimal exponent and 3 x 64
+ // bits of mantissa, divided by ten Used to find an arbitrary power of 2 (by powerOfTwo(long exp))
+ static POS_POWERS_OF_2: bigint[][] = [
+ // 0: 2^0 = 1 = 0.1e1
+ [
+ 1n,
+ 0x1999_9999_9999_9999n,
+ 0x9999_9999_9999_9999n,
+ 0x9999_9999_9999_999an,
+ ], // 1: 2^(2^0) = 2^1 = 2 = 0.2e1
+ [
+ 1n,
+ 0x3333_3333_3333_3333n,
+ 0x3333_3333_3333_3333n,
+ 0x3333_3333_3333_3334n,
+ ], // ***
+ // 2: 2^(2^1) = 2^2 = 4 = 0.4e1
+ [
+ 1n,
+ 0x6666_6666_6666_6666n,
+ 0x6666_6666_6666_6666n,
+ 0x6666_6666_6666_6667n,
+ ], // ***
+ // 3: 2^(2^2) = 2^4 = 16 = 0.16e2
+ [
+ 2n,
+ 0x28f5_c28f_5c28_f5c2n,
+ 0x8f5c_28f5_c28f_5c28n,
+ 0xf5c2_8f5c_28f5_c290n,
+ ], // ***
+ // 4: 2^(2^3) = 2^8 = 256 = 0.256e3
+ [
+ 3n,
+ 0x4189_374b_c6a7_ef9dn,
+ 0xb22d_0e56_0418_9374n,
+ 0xbc6a_7ef9_db22_d0e6n,
+ ], // ***
+ // 5: 2^(2^4) = 2^16 = 65536 = 0.65536e5
+ [
+ 5n,
+ 0xa7c5_ac47_1b47_8423n,
+ 0x0fcf_80dc_3372_1d53n,
+ 0xcddd_6e04_c059_2104n,
+ ], // 6: 2^(2^5) = 2^32 = 4294967296 = 0.4294967296e10
+ [
+ 10n,
+ 0x6df3_7f67_5ef6_eadfn,
+ 0x5ab9_a207_2d44_268dn,
+ 0x97df_837e_6748_956en,
+ ], // 7: 2^(2^6) = 2^64 = 18446744073709551616 = 0.18446744073709551616e20
+ [
+ 20n,
+ 0x2f39_4219_2484_46ban,
+ 0xa23d_2ec7_29af_3d61n,
+ 0x0607_aa01_67dd_94cbn,
+ ], // 8: 2^(2^7) = 2^128 = 340282366920938463463374607431768211456 =
+ // 0.340282366920938463463374607431768211456e39
+ [
+ 39n,
+ 0x571c_bec5_54b6_0dbbn,
+ 0xd5f6_4baf_0506_840dn,
+ 0x451d_b70d_5904_029bn,
+ ], // 9: 2^(2^8) = 2^256 =
+ // 1.1579208923731619542357098500868790785326998466564056403945758401E+77 =
+ // 0.11579208923731619542357098500868790785326998466564056403945758401e78
+ [
+ 78n,
+ 0x1da4_8ce4_68e7_c702n,
+ 0x6520_247d_3556_476dn,
+ 0x1469_caf6_db22_4cfan,
+ ], // ***
+ // 10: 2^(2^9) = 2^512 =
+ // 1.3407807929942597099574024998205846127479365820592393377723561444E+154 =
+ // 0.13407807929942597099574024998205846127479365820592393377723561444e155
+ [
+ 155n,
+ 0x2252_f0e5_b397_69dcn,
+ 0x9ae2_eea3_0ca3_ade0n,
+ 0xeeaa_3c08_dfe8_4e30n,
+ ], // 11: 2^(2^10) = 2^1024 =
+ // 1.7976931348623159077293051907890247336179769789423065727343008116E+308 =
+ // 0.17976931348623159077293051907890247336179769789423065727343008116e309
+ [
+ 309n,
+ 0x2e05_5c9a_3f6b_a793n,
+ 0x1658_3a81_6eb6_0a59n,
+ 0x22c4_b082_6cf1_ebf7n,
+ ], // 12: 2^(2^11) = 2^2048 =
+ // 3.2317006071311007300714876688669951960444102669715484032130345428E+616 =
+ // 0.32317006071311007300714876688669951960444102669715484032130345428e617
+ [
+ 617n,
+ 0x52bb_45e9_cf23_f17fn,
+ 0x7688_c076_06e5_0364n,
+ 0xb344_79aa_9d44_9a57n,
+ ], // 13: 2^(2^12) = 2^4096 =
+ // 1.0443888814131525066917527107166243825799642490473837803842334833E+1233 =
+ // 0.10443888814131525066917527107166243825799642490473837803842334833e1234
+ [
+ 1234n,
+ 0x1abc_81c8_ff5f_846cn,
+ 0x8f5e_3c98_53e3_8c97n,
+ 0x4506_0097_f3bf_9296n,
+ ], // 14: 2^(2^13) = 2^8192 =
+ // 1.0907481356194159294629842447337828624482641619962326924318327862E+2466 =
+ // 0.10907481356194159294629842447337828624482641619962326924318327862e2467
+ [
+ 2467n,
+ 0x1bec_53b5_10da_a7b4n,
+ 0x4836_9ed7_7dbb_0eb1n,
+ 0x3b05_587b_2187_b41en,
+ ], // 15: 2^(2^14) = 2^16384 =
+ // 1.1897314953572317650857593266280071307634446870965102374726748212E+4932 =
+ // 0.11897314953572317650857593266280071307634446870965102374726748212e4933
+ [
+ 4933n,
+ 0x1e75_063a_5ba9_1326n,
+ 0x8abf_b8e4_6001_6ae3n,
+ 0x2800_8702_d29e_8a3cn,
+ ], // 16: 2^(2^15) = 2^32768 =
+ // 1.4154610310449547890015530277449516013481307114723881672343857483E+9864 =
+ // 0.14154610310449547890015530277449516013481307114723881672343857483e9865
+ [
+ 9865n,
+ 0x243c_5d8b_b5c5_fa55n,
+ 0x40c6_d248_c588_1915n,
+ 0x4c0f_d99f_d5be_fc22n,
+ ], // 17: 2^(2^16) = 2^65536 =
+ // 2.0035299304068464649790723515602557504478254755697514192650169737E+19728 =
+ // 0.20035299304068464649790723515602557504478254755697514192650169737e19729
+ [
+ 19729n,
+ 0x334a_5570_c3f4_ef3cn,
+ 0xa13c_36c4_3f97_9c90n,
+ 0xda7a_c473_555f_b7a8n,
+ ], // 18: 2^(2^17) = 2^131072 =
+ // 4.0141321820360630391660606060388767343771510270414189955825538065E+39456 =
+ // 0.40141321820360630391660606060388767343771510270414189955825538065e39457
+ [
+ 39457n,
+ 0x66c3_0444_5dd9_8f3bn,
+ 0xa8c2_93a2_0e47_a41bn,
+ 0x4c5b_03dc_1260_4964n,
+ ], // 19: 2^(2^18) = 2^262144 =
+ // 1.6113257174857604736195721184520050106440238745496695174763712505E+78913 =
+ // 0.16113257174857604736195721184520050106440238745496695174763712505e78914
+ [
+ 78914n,
+ 0x293f_fbf5_fb02_8cc4n,
+ 0x89d3_e5ff_4423_8406n,
+ 0x369a_339e_1bfe_8c9bn,
+ ], // 20: 2^(2^19) = 2^524288 =
+ // 2.5963705678310007761265964957268828277447343763484560463573654868E+157826 =
+ // 0.25963705678310007761265964957268828277447343763484560463573654868e157827
+ [
+ 157827n,
+ 0x4277_92fb_b68e_5d20n,
+ 0x7b29_7cd9_fc15_4b62n,
+ 0xf091_4211_4aa9_a20cn,
+ ], // 21: 2^(2^20) = 2^1048576 =
+ // 6.7411401254990734022690651047042454376201859485326882846944915676E+315652 =
+ // 0.67411401254990734022690651047042454376201859485326882846944915676e315653
+ [
+ 315653n,
+ 0xac92_bc65_ad5c_08fcn,
+ 0x00be_eb11_5a56_6c19n,
+ 0x4ba8_82d8_a462_2437n,
+ ], // 22: 2^(2^21) = 2^2097152 =
+ // 4.5442970191613663099961595907970650433180103994591456270882095573E+631305 =
+ // 0.45442970191613663099961595907970650433180103994591456270882095573e631306
+ [
+ 631306n,
+ 0x7455_8144_0f92_e80en,
+ 0x4da8_22cf_7f89_6f41n,
+ 0x509d_5986_7816_4ecdn,
+ ], // 23: 2^(2^22) = 2^4194304 =
+ // 2.0650635398358879243991194945816501695274360493029670347841664177E+1262611 =
+ // 0.20650635398358879243991194945816501695274360493029670347841664177e1262612
+ [
+ 1262612n,
+ 0x34dd_99b4_c695_23a5n,
+ 0x64bc_2e8f_0d8b_1044n,
+ 0xb03b_1c96_da5d_d349n,
+ ], // 24: 2^(2^23) = 2^8388608 =
+ // 4.2644874235595278724327289260856157547554200794957122157246170406E+2525222 =
+ // 0.42644874235595278724327289260856157547554200794957122157246170406e2525223
+ [
+ 2525223n,
+ 0x6d2b_bea9_d6d2_5a08n,
+ 0xa0a4_606a_88e9_6b70n,
+ 0x1820_63bb_c2fe_8520n,
+ ], // 25: 2^(2^24) = 2^16777216 =
+ // 1.8185852985697380078927713277749906189248596809789408311078112486E+5050445 =
+ // 0.18185852985697380078927713277749906189248596809789408311078112486e5050446
+ [
+ 5050446n,
+ 0x2e8e_47d6_3bfd_d6e3n,
+ 0x2b55_fa89_76ea_a3e9n,
+ 0x1a6b_9d30_8641_2a73n,
+ ], // 26: 2^(2^25) = 2^33554432 =
+ // 3.3072524881739831340558051919726975471129152081195558970611353362E+10100890 =
+ // 0.33072524881739831340558051919726975471129152081195558970611353362e10100891
+ [
+ 10100891n,
+ 0x54aa_68ef_a1d7_19dfn,
+ 0xd850_5806_612c_5c8fn,
+ 0xad06_8837_fee8_b43an,
+ ], // 27: 2^(2^26) = 2^67108864 =
+ // 1.0937919020533002449982468634925923461910249420785622990340704603E+20201781 =
+ // 0.10937919020533002449982468634925923461910249420785622990340704603e20201782
+ [
+ 20201782n,
+ 0x1c00_464c_cb7b_ae77n,
+ 0x9e38_7778_4c77_982cn,
+ 0xd94a_f3b6_1717_404fn,
+ ], // 28: 2^(2^27) = 2^134217728 =
+ // 1.1963807249973763567102377630870670302911237824129274789063323723E+40403562 =
+ // 0.11963807249973763567102377630870670302911237824129274789063323723e40403563
+ [
+ 40403563n,
+ 0x1ea0_99c8_be2b_6cd0n,
+ 0x8bfb_6d53_9fa5_0466n,
+ 0x6d3b_c37e_69a8_4218n,
+ ], // 29: 2^(2^28) = 2^268435456 =
+ // 1.4313268391452478724777126233530788980596273340675193575004129517E+80807124 =
+ // 0.14313268391452478724777126233530788980596273340675193575004129517e80807125
+ [
+ 80807125n,
+ 0x24a4_57f4_66ce_8d18n,
+ 0xf2c8_f3b8_1bc6_bb59n,
+ 0xa78c_7576_92e0_2d49n,
+ ], // 30: 2^(2^29) = 2^536870912 =
+ // 2.0486965204575262773910959587280218683219330308711312100181276813E+161614248 =
+ // 0.20486965204575262773910959587280218683219330308711312100181276813e161614249
+ [
+ 161614249n,
+ 0x3472_5667_7aba_6b53n,
+ 0x3fbf_90d3_0611_a67cn,
+ 0x1e03_9d87_e0bd_b32bn,
+ ], // 31: 2^(2^30) = 2^1073741824 =
+ // 4.1971574329347753848087162337676781412761959309467052555732924370E+323228496 =
+ // 0.41971574329347753848087162337676781412761959309467052555732924370e323228497
+ [
+ 323228497n,
+ 0x6b72_7daf_0fd3_432an,
+ 0x71f7_1121_f9e4_200fn,
+ 0x8fcd_9942_d486_c10cn,
+ ], // 32: 2^(2^31) = 2^2147483648 =
+ // 1.7616130516839633532074931497918402856671115581881347960233679023E+646456993 =
+ // 0.17616130516839633532074931497918402856671115581881347960233679023e646456994
+ [
+ 646456994n,
+ 0x2d18_e844_84d9_1f78n,
+ 0x4079_bfe7_829d_ec6fn,
+ 0x2155_1643_e365_abc6n,
+ ],
+ ];
+ // An array of negative powers of two, each value consists of 4 longs: decimal exponent and 3 x 64
+ // bits of mantissa, divided by ten. Used to find an arbitrary power of 2 (by powerOfTwo(long exp))
+ static NEG_POWERS_OF_2: bigint[][] = [
+ // v18
+ // 0: 2^0 = 1 = 0.1e1
+ [
+ 1n,
+ 0x1999_9999_9999_9999n,
+ 0x9999_9999_9999_9999n,
+ 0x9999_9999_9999_999an,
+ ], // 1: 2^-(2^0) = 2^-1 = 0.5 = 0.5e0
+ [
+ 0n,
+ 0x8000_0000_0000_0000n,
+ 0x0000_0000_0000_0000n,
+ 0x0000_0000_0000_0000n,
+ ], // 2: 2^-(2^1) = 2^-2 = 0.25 = 0.25e0
+ // {0, 0x4000_0000_0000_0000L, 0x0000_0000_0000_0000L, 0x0000_0000_0000_0000L},
+ [
+ 0n,
+ 0x4000_0000_0000_0000n,
+ 0x0000_0000_0000_0000n,
+ 0x0000_0000_0000_0001n,
+ ], // ***
+ // 3: 2^-(2^2) = 2^-4 = 0.0625 = 0.625e-1
+ [
+ -1n,
+ 0xa000_0000_0000_0000n,
+ 0x0000_0000_0000_0000n,
+ 0x0000_0000_0000_0000n,
+ ], // 4: 2^-(2^3) = 2^-8 = 0.00390625 = 0.390625e-2
+ [
+ -2n,
+ 0x6400_0000_0000_0000n,
+ 0x0000_0000_0000_0000n,
+ 0x0000_0000_0000_0000n,
+ ], // 5: 2^-(2^4) = 2^-16 = 0.0000152587890625 = 0.152587890625e-4
+ [
+ -4n,
+ 0x2710_0000_0000_0000n,
+ 0x0000_0000_0000_0000n,
+ 0x0000_0000_0000_0001n,
+ ], // ***
+ // 6: 2^-(2^5) = 2^-32 = 2.3283064365386962890625E-10 = 0.23283064365386962890625e-9
+ [
+ -9n,
+ 0x3b9a_ca00_0000_0000n,
+ 0x0000_0000_0000_0000n,
+ 0x0000_0000_0000_0001n,
+ ], // ***
+ // 7: 2^-(2^6) = 2^-64 = 5.42101086242752217003726400434970855712890625E-20 =
+ // 0.542101086242752217003726400434970855712890625e-19
+ [
+ -19n,
+ 0x8ac7_2304_89e8_0000n,
+ 0x0000_0000_0000_0000n,
+ 0x0000_0000_0000_0000n,
+ ], // 8: 2^-(2^7) = 2^-128 =
+ // 2.9387358770557187699218413430556141945466638919302188037718792657E-39 =
+ // 0.29387358770557187699218413430556141945466638919302188037718792657e-38
+ [
+ -38n,
+ 0x4b3b_4ca8_5a86_c47an,
+ 0x098a_2240_0000_0000n,
+ 0x0000_0000_0000_0001n,
+ ], // ***
+ // 9: 2^-(2^8) = 2^-256 =
+ // 8.6361685550944446253863518628003995711160003644362813850237034700E-78 =
+ // 0.86361685550944446253863518628003995711160003644362813850237034700e-77
+ [
+ -77n,
+ 0xdd15_fe86_affa_d912n,
+ 0x49ef_0eb7_13f3_9eben,
+ 0xaa98_7b6e_6fd2_a002n,
+ ], // 10: 2^-(2^9) = 2^-512 =
+ // 7.4583407312002067432909653154629338373764715346004068942715183331E-155 =
+ // 0.74583407312002067432909653154629338373764715346004068942715183331e-154
+ [
+ -154n,
+ 0xbeee_fb58_4aff_8603n,
+ 0xaafb_550f_facf_d8fan,
+ 0x5ca4_7e4f_88d4_5371n,
+ ], // 11: 2^-(2^10) = 2^-1024 =
+ // 5.5626846462680034577255817933310101605480399511558295763833185421E-309 =
+ // 0.55626846462680034577255817933310101605480399511558295763833185421e-308
+ [
+ -308n,
+ 0x8e67_9c2f_5e44_ff8fn,
+ 0x570f_09ea_a7ea_7648n,
+ 0x5961_db50_c6d2_b888n,
+ ], // ***
+ // 12: 2^-(2^11) = 2^-2048 =
+ // 3.0943460473825782754801833699711978538925563038849690459540984582E-617 =
+ // 0.30943460473825782754801833699711978538925563038849690459540984582e-616
+ [
+ -616n,
+ 0x4f37_1b33_99fc_2ab0n,
+ 0x8170_041c_9feb_05aan,
+ 0xc7c3_4344_7c75_bcf6n,
+ ], // 13: 2^-(2^12) = 2^-4096 =
+ // 9.5749774609521853579467310122804202420597417413514981491308464986E-1234 =
+ // 0.95749774609521853579467310122804202420597417413514981491308464986e-1233
+ [
+ -1233n,
+ 0xf51e_9281_7901_3fd3n,
+ 0xde4b_d12c_de4d_985cn,
+ 0x4a57_3ca6_f94b_ff14n,
+ ], // 14: 2^-(2^13) = 2^-8192 =
+ // 9.1680193377742358281070619602424158297818248567928361864131947526E-2467 =
+ // 0.91680193377742358281070619602424158297818248567928361864131947526e-2466
+ [
+ -2466n,
+ 0xeab3_8812_7bcc_aff7n,
+ 0x1667_6391_42b9_fbaen,
+ 0x775e_c999_5e10_39fbn,
+ ], // 15: 2^-(2^14) = 2^-16384 =
+ // 8.4052578577802337656566945433043815064951983621161781002720680748E-4933 =
+ // 0.84052578577802337656566945433043815064951983621161781002720680748e-4932
+ [
+ -4932n,
+ 0xd72c_b2a9_5c7e_f6ccn,
+ 0xe81b_f1e8_25ba_7515n,
+ 0xc2fe_b521_d6cb_5dcdn,
+ ], // 16: 2^-(2^15) = 2^-32768 =
+ // 7.0648359655776364427774021878587184537374439102725065590941425796E-9865 =
+ // 0.70648359655776364427774021878587184537374439102725065590941425796e-9864
+ [
+ -9864n,
+ 0xb4dc_1be6_6045_02dcn,
+ 0xd491_079b_8eef_6535n,
+ 0x578d_3965_d24d_e84dn,
+ ], // ***
+ // 17: 2^-(2^16) = 2^-65536 =
+ // 4.9911907220519294656590574792132451973746770423207674161425040336E-19729 =
+ // 0.49911907220519294656590574792132451973746770423207674161425040336e-19728
+ [
+ -19728n,
+ 0x7fc6_447b_ee60_ea43n,
+ 0x2548_da5c_8b12_5b27n,
+ 0x5f42_d114_2f41_d349n,
+ ], // ***
+ // 18: 2^-(2^17) = 2^-131072 =
+ // 2.4911984823897261018394507280431349807329035271689521242878455599E-39457 =
+ // 0.24911984823897261018394507280431349807329035271689521242878455599e-39456
+ [
+ -39456n,
+ 0x3fc6_5180_f88a_f8fbn,
+ 0x6a69_15f3_8334_9413n,
+ 0x063c_3708_b6ce_b291n,
+ ], // ***
+ // 19: 2^-(2^18) = 2^-262144 =
+ // 6.2060698786608744707483205572846793091942192651991171731773832448E-78914 =
+ // 0.62060698786608744707483205572846793091942192651991171731773832448e-78913
+ [
+ -78913n,
+ 0x9ee0_197c_8dcd_55bfn,
+ 0x2b2b_9b94_2c38_f4a2n,
+ 0x0f8b_a634_e9c7_06aen,
+ ], // 20: 2^-(2^19) = 2^-524288 =
+ // 3.8515303338821801176537443725392116267291403078581314096728076497E-157827 =
+ // 0.38515303338821801176537443725392116267291403078581314096728076497e-157826
+ [
+ -157826n,
+ 0x6299_63a2_5b8b_2d79n,
+ 0xd00b_9d22_86f7_0876n,
+ 0xe970_0470_0c36_44fcn,
+ ], // ***
+ // 21: 2^-(2^20) = 2^-1048576 =
+ // 1.4834285912814577854404052243709225888043963245995136935174170977E-315653 =
+ // 0.14834285912814577854404052243709225888043963245995136935174170977e-315652
+ [
+ -315652n,
+ 0x25f9_cc30_8cee_f4f3n,
+ 0x40f1_9543_911a_4546n,
+ 0xa2cd_3894_52cf_c366n,
+ ], // 22: 2^-(2^21) = 2^-2097152 =
+ // 2.2005603854312903332428997579002102976620485709683755186430397089E-631306 =
+ // 0.22005603854312903332428997579002102976620485709683755186430397089e-631305
+ [
+ -631305n,
+ 0x3855_97b0_d47e_76b8n,
+ 0x1b9f_67e1_03bf_2329n,
+ 0xc311_9848_5959_85f7n,
+ ], // 23: 2^-(2^22) = 2^-4194304 =
+ // 4.8424660099295090687215589310713586524081268589231053824420510106E-1262612 =
+ // 0.48424660099295090687215589310713586524081268589231053824420510106e-1262611
+ [
+ -1262611n,
+ 0x7bf7_95d2_76c1_2f66n,
+ 0x66a6_1d62_a446_659an,
+ 0xa1a4_d73b_ebf0_93d5n,
+ ], // ***
+ // 24: 2^-(2^23) = 2^-8388608 =
+ // 2.3449477057322620222546775527242476219043877555386221929831430440E-2525223 =
+ // 0.23449477057322620222546775527242476219043877555386221929831430440e-2525222
+ [
+ -2525222n,
+ 0x3c07_d96a_b1ed_7799n,
+ 0xcb73_55c2_2cc0_5ac0n,
+ 0x4ffc_0ab7_3b1f_6a49n,
+ ], // ***
+ // 25: 2^-(2^24) = 2^-16777216 =
+ // 5.4987797426189993226257377747879918011694025935111951649826798628E-5050446 =
+ // 0.54987797426189993226257377747879918011694025935111951649826798628e-5050445
+ [
+ -5050445n,
+ 0x8cc4_cd8c_3ede_fb9an,
+ 0x6c8f_f86a_90a9_7e0cn,
+ 0x166c_fddb_f98b_71bfn,
+ ], // ***
+ // 26: 2^-(2^25) = 2^-33554432 =
+ // 3.0236578657837068435515418409027857523343464783010706819696074665E-10100891 =
+ // 0.30236578657837068435515418409027857523343464783010706819696074665e-10100890
+ [
+ -10100890n,
+ 0x4d67_d81c_c88e_1228n,
+ 0x1d7c_fb06_666b_79b3n,
+ 0x7b91_6728_aaa4_e70dn,
+ ], // ***
+ // 27: 2^-(2^26) = 2^-67108864 =
+ // 9.1425068893156809483320844568740945600482370635012633596231964471E-20201782 =
+ // 0.91425068893156809483320844568740945600482370635012633596231964471e-20201781
+ [
+ -20201781n,
+ 0xea0c_5549_4e7a_552dn,
+ 0xb88c_b948_4bb8_6c61n,
+ 0x8d44_893c_610b_b7dfn,
+ ], // ***
+ // 28: 2^-(2^27) = 2^-134217728 =
+ // 8.3585432221184688810803924874542310018191301711943564624682743545E-40403563 =
+ // 0.83585432221184688810803924874542310018191301711943564624682743545e-40403562
+ [
+ -40403562n,
+ 0xd5fa_8c82_1ec0_c24an,
+ 0xa80e_46e7_64e0_f8b0n,
+ 0xa727_6bfa_432f_ac7en,
+ ], // 29: 2^-(2^28) = 2^-268435456 =
+ // 6.9865244796022595809958912202005005328020601847785697028605460277E-80807125 =
+ // 0.69865244796022595809958912202005005328020601847785697028605460277e-80807124
+ [
+ -80807124n,
+ 0xb2da_e307_426f_6791n,
+ 0xc970_b82f_58b1_2918n,
+ 0x0472_592f_7f39_190en,
+ ], // 30: 2^-(2^29) = 2^-536870912 =
+ // 4.8811524304081624052042871019605298977947353140996212667810837790E-161614249 =
+ // 0.48811524304081624052042871019605298977947353140996212667810837790e-161614248
+ // {-161614248, 0x7cf5_1edd_8a15_f1c9L, 0x656d_ab34_98f8_e697L, 0x12da_a2a8_0e53_c809L},
+ [
+ -161614248n,
+ 0x7cf5_1edd_8a15_f1c9n,
+ 0x656d_ab34_98f8_e697n,
+ 0x12da_a2a8_0e53_c807n,
+ ], // 31: 2^-(2^30) = 2^-1073741824 =
+ // 2.3825649048879510732161697817326745204151961255592397879550237608E-323228497 =
+ // 0.23825649048879510732161697817326745204151961255592397879550237608e-323228496
+ [
+ -323228496n,
+ 0x3cfe_609a_b588_3c50n,
+ 0xbec8_b5d2_2b19_8871n,
+ 0xe184_7770_3b46_22b4n,
+ ], // 32: 2^-(2^31) = 2^-2147483648 =
+ // 5.6766155260037313438164181629489689531186932477276639365773003794E-646456994 =
+ // 0.56766155260037313438164181629489689531186932477276639365773003794e-646456993
+ [
+ -646456993n,
+ 0x9152_447b_9d7c_da9an,
+ 0x3b4d_3f61_10d7_7aadn,
+ 0xfa81_bad1_c394_adb4n,
+ ],
+ ];
+ // Buffers used internally
+ // The order of words in the arrays is big-endian: the highest part is in buff[0] (in buff[1] for
+ // buffers of 10 words)
+
+ buffer4x64B: bigint[] = new Array(4).fill(0n);
+ buffer6x32A: bigint[] = new Array(6).fill(0n);
+ buffer6x32B: bigint[] = new Array(6).fill(0n);
+ buffer6x32C: bigint[] = new Array(6).fill(0n);
+ buffer12x32: bigint[] = new Array(12).fill(0n);
+ parse(digits: number[], exp10: number): void {
+ exp10 += digits.length - 1; // digits is viewed as x.yyy below.
+ this.exponent = 0;
+ this.mantHi = 0n;
+ this.mantLo = 0n;
+ // Finds numeric value of the decimal mantissa
+ const mantissa: bigint[] = this.buffer6x32C;
+ const exp10Corr: number = this.parseMantissa(digits, mantissa);
+ if (exp10Corr === 0 && this.isEmpty(mantissa)) {
+ // Mantissa == 0
+ return;
+ }
+ // takes account of the point position in the mant string and possible carry as a result of
+ // round-up (like 9.99e1 -> 1.0e2)
+ exp10 += exp10Corr;
+ if (exp10 < QuadrupleBuilder.MIN_EXP10) {
+ return;
+ }
+ if (exp10 > QuadrupleBuilder.MAX_EXP10) {
+ this.exponent = Number(QuadrupleBuilder.EXPONENT_OF_INFINITY);
+ return;
+ }
+ const exp2: number = this.findBinaryExponent(exp10, mantissa);
+ // Finds binary mantissa and possible exponent correction. Fills the fields.
+ this.findBinaryMantissa(exp10, exp2, mantissa);
+ }
+ parseMantissa(digits: number[], mantissa: bigint[]): number {
+ for (let i = 0; i < 6; i++) {
+ mantissa[i] = 0n;
+ }
+ // Skip leading zeroes
+ let firstDigit = 0;
+ while (firstDigit < digits.length && digits[firstDigit] === 0) {
+ firstDigit += 1;
+ }
+ if (firstDigit === digits.length) {
+ return 0; // All zeroes
+ }
+ let expCorr: number = -firstDigit;
+ // Limit the string length to avoid unnecessary fuss
+ if (digits.length - firstDigit > QuadrupleBuilder.MAX_MANTISSA_LENGTH) {
+ const carry: boolean = digits[QuadrupleBuilder.MAX_MANTISSA_LENGTH] >= 5; // The highest digit to be truncated
+ const truncated: number[] = new Array(
+ QuadrupleBuilder.MAX_MANTISSA_LENGTH
+ ).fill(0);
+ for (let i = 0; i < QuadrupleBuilder.MAX_MANTISSA_LENGTH; i++) {
+ truncated[i] = digits[i + firstDigit];
+ }
+ if (carry) {
+ // Round-up: add carry
+ expCorr += this.addCarry(truncated); // May add an extra digit in front of it (99..99 -> 100)
+ }
+ digits = truncated;
+ firstDigit = 0;
+ }
+ for (let i = digits.length - 1; i >= firstDigit; i--) {
+ // digits, starting from the last
+ mantissa[0] |= BigInt(digits[i]) << 32n;
+ this.divBuffBy10(mantissa);
+ }
+ return expCorr;
+ }
+ // Divides the unpacked value stored in the given buffer by 10
+ // @param buffer contains the unpacked value to divide (32 least significant bits are used)
+ divBuffBy10(buffer: bigint[]): void {
+ const maxIdx: number = buffer.length;
+ // big/endian
+ for (let i = 0; i < maxIdx; i++) {
+ const r: bigint = buffer[i] % 10n;
+ buffer[i] = buffer[i] / 10n;
+ if (i + 1 < maxIdx) {
+ buffer[i + 1] += r << 32n;
+ }
+ }
+ }
+ // Checks if the buffer is empty (contains nothing but zeros)
+ // @param buffer the buffer to check
+ // @return {@code true} if the buffer is empty, {@code false} otherwise
+ isEmpty(buffer: bigint[]): boolean {
+ for (let i = 0; i < buffer.length; i++) {
+ if (buffer[i] !== 0n) {
+ return false;
+ }
+ }
+ return true;
+ }
+ // Adds one to a decimal number represented as a sequence of decimal digits. propagates carry as
+ // needed, so that {@code addCarryTo("6789") = "6790", addCarryTo("9999") = "10000"} etc.
+ // @return 1 if an additional higher "1" was added in front of the number as a result of
+ // rounding-up, 0 otherwise
+ addCarry(digits: number[]): number {
+ for (let i = digits.length - 1; i >= 0; i--) {
+ // starting with the lowest digit
+ const c: number = digits[i];
+ if (c === 9) {
+ digits[i] = 0;
+ } else {
+ digits[i] = digits[i] + 1;
+ return 0;
+ }
+ }
+ digits[0] = 1;
+ return 1;
+ }
+ // Finds binary exponent, using decimal exponent and mantissa.
+ // exp2 = exp10 * log2(10) + log2(mant)
+ // @param exp10 decimal exponent
+ // @param mantissa array of longs containing decimal mantissa (divided by 10)
+ // @return found value of binary exponent
+ findBinaryExponent(exp10: number, mantissa: bigint[]): number {
+ const mant10: bigint = (mantissa[0] << 31n) | (mantissa[1] >> 1n); // Higher 63 bits of the mantissa, in range
+ // 0x0CC..CCC -- 0x7FF..FFF (2^63/10 -- 2^63-1)
+ // decimal value of the mantissa in range 1.0..9.9999...
+ const mant10d: number = Number(mant10) / QuadrupleBuilder.TWO_POW_63_DIV_10;
+ return Math.floor(
+ Number(exp10) * QuadrupleBuilder.LOG2_10 + this.log2(mant10d)
+ ); // Binary exponent
+ }
+ // Calculates log2 of the given x
+ // @param x argument that can't be 0
+ // @return the value of log2(x)
+ log2(x: number): number {
+ // x can't be 0
+ return QuadrupleBuilder.LOG2_E * Math.log(x);
+ }
+ findBinaryMantissa(exp10: number, exp2: number, mantissa: bigint[]): void {
+ // pow(2, -exp2): division by 2^exp2 is multiplication by 2^(-exp2) actually
+ const powerOf2: bigint[] = this.buffer4x64B;
+ this.powerOfTwo(-exp2, powerOf2);
+ const product: bigint[] = this.buffer12x32; // use it for the product (M * 10^E / 2^e)
+ this.multUnpacked6x32byPacked(mantissa, powerOf2, product); // product in buff_12x32
+ this.multBuffBy10(product); // "Quasidecimals" are numbers divided by 10
+ // The powerOf2[0] is stored as an unsigned value
+ if (BigInt(powerOf2[0]) !== BigInt(-exp10)) {
+ // For some combinations of exp2 and exp10, additional multiplication needed
+ // (see mant2_from_M_E_e.xls)
+ this.multBuffBy10(product);
+ }
+ // compensate possible inaccuracy of logarithms used to compute exp2
+ exp2 += this.normalizeMant(product);
+ exp2 += QuadrupleBuilder.EXPONENT_BIAS; // add bias
+ // For subnormal values, exp2 <= 0. We just return 0 for them, as they are
+ // far from any range we are interested in.
+ if (exp2 <= 0) {
+ return;
+ }
+ exp2 += this.roundUp(product); // round up, may require exponent correction
+ if (BigInt(exp2) >= QuadrupleBuilder.EXPONENT_OF_INFINITY) {
+ this.exponent = Number(QuadrupleBuilder.EXPONENT_OF_INFINITY);
+ } else {
+ this.exponent = Number(exp2);
+ this.mantHi = ((product[0] << 32n) + product[1]) & 0xffffffffffffffffn;
+ this.mantLo = ((product[2] << 32n) + product[3]) & 0xffffffffffffffffn;
+ }
+ }
+ // Calculates the required power and returns the result in the quasidecimal format (an array of
+ // longs, where result[0] is the decimal exponent of the resulting value, and result[1] --
+ // result[3] contain 192 bits of the mantissa divided by ten (so that 8 looks like
+ // {@code {1, 0xCCCC_.._CCCCL, 0xCCCC_.._CCCCL, 0xCCCC_.._CCCDL}}}
+ // uses arrays buffer4x64B, buffer6x32A, buffer6x32B, buffer12x32,
+ // @param exp the power to raise 2 to
+ // @param power (result) the value of {@code2^exp}
+ powerOfTwo(exp: number, power: bigint[]): void {
+ if (exp === 0) {
+ this.array_copy(QuadrupleBuilder.POS_POWERS_OF_2[0], power);
+ return;
+ }
+ // positive powers of 2 (2^0, 2^1, 2^2, 2^4, 2^8 ... 2^(2^31) )
+ let powers: bigint[][] = QuadrupleBuilder.POS_POWERS_OF_2;
+ if (exp < 0) {
+ exp = -exp;
+ powers = QuadrupleBuilder.NEG_POWERS_OF_2; // positive powers of 2 (2^0, 2^-1, 2^-2, 2^-4, 2^-8 ... 2^30)
+ }
+ // 2^31 = 0x8000_0000L; a single bit that will be shifted right at every iteration
+ let currPowOf2: number = QuadrupleBuilder.POW_2_31;
+ let idx = 32; // Index in the table of powers
+ let first_power = true;
+ // if exp = b31 * 2^31 + b30 * 2^30 + .. + b0 * 2^0, where b0..b31 are the values of the bits in
+ // exp, then 2^exp = 2^b31 * 2^b30 ... * 2^b0. Find the product, using a table of powers of 2.
+ while (exp > 0) {
+ if (exp >= currPowOf2) {
+ // the current bit in the exponent is 1
+ if (first_power) {
+ // 4 longs, power[0] -- decimal (?) exponent, power[1..3] -- 192 bits of mantissa
+ this.array_copy(powers[idx], power);
+ first_power = false;
+ } else {
+ // Multiply by the corresponding power of 2
+ this.multPacked3x64_AndAdjustExponent(power, powers[idx], power);
+ }
+ exp -= currPowOf2;
+ }
+ idx -= 1;
+ currPowOf2 = currPowOf2 * 0.5; // Note: this is exact
+ }
+ }
+ // Copies from into to.
+ array_copy(source: bigint[], dest: bigint[]): void {
+ for (let i = 0; i < dest.length; i++) {
+ dest[i] = source[i];
+ }
+ }
+ // Multiplies two quasidecimal numbers contained in buffers of 3 x 64 bits with exponents, puts
+ // the product to buffer4x64B
+ // and returns it. Both each of the buffers and the product contain 4 longs - exponent and 3 x 64
+ // bits of mantissa. If the higher word of mantissa of the product is less than
+ // 0x1999_9999_9999_9999L (i.e. mantissa is less than 0.1) multiplies mantissa by 10 and adjusts
+ // the exponent respectively.
+ multPacked3x64_AndAdjustExponent(
+ factor1: bigint[],
+ factor2: bigint[],
+ result: bigint[]
+ ): void {
+ this.multPacked3x64_simply(factor1, factor2, this.buffer12x32);
+ const expCorr: number = this.correctPossibleUnderflow(this.buffer12x32);
+ this.pack_6x32_to_3x64(this.buffer12x32, result);
+ // result[0] is a signed int64 value stored in an uint64
+ result[0] = factor1[0] + factor2[0] + BigInt(expCorr); // product.exp = f1.exp + f2.exp
+ }
+ // Multiplies mantissas of two packed quasidecimal values (each is an array of 4 longs, exponent +
+ // 3 x 64 bits of mantissa) Returns the product as unpacked buffer of 12 x 32 (12 x 32 bits of
+ // product)
+ // uses arrays buffer6x32A, buffer6x32B
+ // @param factor1 an array of longs containing factor 1 as packed quasidecimal
+ // @param factor2 an array of longs containing factor 2 as packed quasidecimal
+ // @param result an array of 12 longs filled with the product of mantissas
+ multPacked3x64_simply(
+ factor1: bigint[],
+ factor2: bigint[],
+ result: bigint[]
+ ): void {
+ for (let i = 0; i < result.length; i++) {
+ result[i] = 0n;
+ }
+ // TODO2 19.01.16 21:23:06 for the next version -- rebuild the table of powers to make the
+ // numbers unpacked, to avoid packing/unpacking
+ this.unpack_3x64_to_6x32(factor1, this.buffer6x32A);
+ this.unpack_3x64_to_6x32(factor2, this.buffer6x32B);
+ for (let i = 6 - 1; i >= 0; i--) {
+ // compute partial 32-bit products
+ for (let j = 6 - 1; j >= 0; j--) {
+ const part: bigint = this.buffer6x32A[i] * this.buffer6x32B[j];
+ result[j + i + 1] =
+ (result[j + i + 1] + (part & QuadrupleBuilder.LOWER_32_BITS)) &
+ 0xffffffffffffffffn;
+ result[j + i] = (result[j + i] + (part >> 32n)) & 0xffffffffffffffffn;
+ }
+ }
+ // Carry higher bits of the product to the lower bits of the next word
+ for (let i = 12 - 1; i >= 1; i--) {
+ result[i - 1] =
+ (result[i - 1] + (result[i] >> 32n)) & 0xffffffffffffffffn;
+ result[i] &= QuadrupleBuilder.LOWER_32_BITS;
+ }
+ }
+ // Corrects possible underflow of the decimal mantissa, passed in in the {@code mantissa}, by
+ // multiplying it by a power of ten. The corresponding value to adjust the decimal exponent is
+ // returned as the result
+ // @param mantissa a buffer containing the mantissa to be corrected
+ // @return a corrective (addition) that is needed to adjust the decimal exponent of the number
+ correctPossibleUnderflow(mantissa: bigint[]): number {
+ let expCorr = 0;
+ while (this.isLessThanOne(mantissa)) {
+ // Underflow
+ this.multBuffBy10(mantissa);
+ expCorr -= 1;
+ }
+ return expCorr;
+ }
+ // Checks if the unpacked quasidecimal value held in the given buffer is less than one (in this
+ // format, one is represented as { 0x1999_9999L, 0x9999_9999L, 0x9999_9999L,...}
+ // @param buffer a buffer containing the value to check
+ // @return {@code true}, if the value is less than one
+ isLessThanOne(buffer: bigint[]): boolean {
+ if (buffer[0] < 0x1999_9999n) {
+ return true;
+ }
+ if (buffer[0] > 0x1999_9999n) {
+ return false;
+ }
+ // A note regarding the coverage:
+ // Multiplying a 128-bit number by another 192-bit number,
+ // as well as multiplying of two 192-bit numbers,
+ // can never produce 320 (or 384 bits, respectively) of 0x1999_9999L, 0x9999_9999L,
+ for (let i = 1; i < buffer.length; i++) {
+ // so this loop can't be covered entirely
+ if (buffer[i] < 0x9999_9999n) {
+ return true;
+ }
+ if (buffer[i] > 0x9999_9999n) {
+ return false;
+ }
+ }
+ // and it can never reach this point in real life.
+ return false; // Still Java requires the return statement here.
+ }
+ // Multiplies unpacked 192-bit value by a packed 192-bit factor
+ // uses static arrays buffer6x32B
+ // @param factor1 a buffer containing unpacked quasidecimal mantissa (6 x 32 bits)
+ // @param factor2 an array of 4 longs containing packed quasidecimal power of two
+ // @param product a buffer of at least 12 longs to hold the product
+ multUnpacked6x32byPacked(
+ factor1: bigint[],
+ factor2: bigint[],
+ product: bigint[]
+ ): void {
+ for (let i = 0; i < product.length; i++) {
+ product[i] = 0n;
+ }
+ const unpacked2: bigint[] = this.buffer6x32B;
+ this.unpack_3x64_to_6x32(factor2, unpacked2); // It's the powerOf2, with exponent in 0'th word
+ const maxFactIdx: number = factor1.length;
+ for (let i = maxFactIdx - 1; i >= 0; i--) {
+ // compute partial 32-bit products
+ for (let j = maxFactIdx - 1; j >= 0; j--) {
+ const part: bigint = factor1[i] * unpacked2[j];
+ product[j + i + 1] =
+ (product[j + i + 1] + (part & QuadrupleBuilder.LOWER_32_BITS)) &
+ 0xffffffffffffffffn;
+ product[j + i] = (product[j + i] + (part >> 32n)) & 0xffffffffffffffffn;
+ }
+ }
+ // Carry higher bits of the product to the lower bits of the next word
+ for (let i = 12 - 1; i >= 1; i--) {
+ product[i - 1] =
+ (product[i - 1] + (product[i] >> 32n)) & 0xffffffffffffffffn;
+ product[i] &= QuadrupleBuilder.LOWER_32_BITS;
+ }
+ }
+ // Multiplies the unpacked value stored in the given buffer by 10
+ // @param buffer contains the unpacked value to multiply (32 least significant bits are used)
+ multBuffBy10(buffer: bigint[]): void {
+ const maxIdx: number = buffer.length - 1;
+ buffer[0] &= QuadrupleBuilder.LOWER_32_BITS;
+ buffer[maxIdx] *= 10n;
+ for (let i = maxIdx - 1; i >= 0; i--) {
+ buffer[i] =
+ (buffer[i] * 10n + (buffer[i + 1] >> 32n)) & 0xffffffffffffffffn;
+ buffer[i + 1] &= QuadrupleBuilder.LOWER_32_BITS;
+ }
+ }
+ // Makes sure that the (unpacked) mantissa is normalized,
+ // i.e. buff[0] contains 1 in bit 32 (the implied integer part) and higher 32 of mantissa in bits 31..0,
+ // and buff[1]..buff[4] contain other 96 bits of mantissa in their lower halves:
+ // 0x0000_0001_XXXX_XXXXL, 0x0000_0000_XXXX_XXXXL...
+ // If necessary, divides the mantissa by appropriate power of 2 to make it normal.
+ // @param mantissa a buffer containing unpacked mantissa
+ // @return if the mantissa was not normal initially, a correction that should be added to the result's exponent, or 0 otherwise
+ normalizeMant(mantissa: bigint[]): number {
+ const expCorr: number = 31 - QuadrupleBuilder.clz64(mantissa[0]);
+ if (expCorr !== 0) {
+ this.divBuffByPower2(mantissa, expCorr);
+ }
+ return expCorr;
+ }
+ // Rounds up the contents of the unpacked buffer to 128 bits by adding unity one bit lower than
+ // the lowest of these 128 bits. If carry propagates up to bit 33 of buff[0], shifts the buffer
+ // rightwards to keep it normalized.
+ // @param mantissa the buffer to get rounded
+ // @return 1 if the buffer was shifted, 0 otherwise
+ roundUp(mantissa: bigint[]): number {
+ // due to the limited precision of the power of 2, a number with exactly half LSB in its
+ // mantissa
+ // (i.e that would have 0x8000_0000_0000_0000L in bits 128..191 if it were computed precisely),
+ // after multiplication by this power of 2, may get erroneous bits 185..191 (counting from the
+ // MSB),
+ // taking a value from
+ // 0xXXXX_XXXX_XXXX_XXXXL 0xXXXX_XXXX_XXXX_XXXXL 0x7FFF_FFFF_FFFF_FFD8L.
+ // to
+ // 0xXXXX_XXXX_XXXX_XXXXL 0xXXXX_XXXX_XXXX_XXXXL 0x8000_0000_0000_0014L, or something alike.
+ // To round it up, we first add
+ // 0x0000_0000_0000_0000L 0x0000_0000_0000_0000L 0x0000_0000_0000_0028L, to turn it into
+ // 0xXXXX_XXXX_XXXX_XXXXL 0xXXXX_XXXX_XXXX_XXXXL 0x8000_0000_0000_00XXL,
+ // and then add
+ // 0x0000_0000_0000_0000L 0x0000_0000_0000_0000L 0x8000_0000_0000_0000L, to provide carry to
+ // higher bits.
+ this.addToBuff(mantissa, 5, 100n); // to compensate possible inaccuracy
+ this.addToBuff(mantissa, 4, 0x8000_0000n); // round-up, if bits 128..159 >= 0x8000_0000L
+ if ((mantissa[0] & (QuadrupleBuilder.HIGHER_32_BITS << 1n)) !== 0n) {
+ // carry's got propagated beyond the highest bit
+ this.divBuffByPower2(mantissa, 1);
+ return 1;
+ }
+ return 0;
+ }
+ // converts 192 most significant bits of the mantissa of a number from an unpacked quasidecimal
+ // form (where 32 least significant bits only used) to a packed quasidecimal form (where buff[0]
+ // contains the exponent and buff[1]..buff[3] contain 3 x 64 = 192 bits of mantissa)
+ // @param unpackedMant a buffer of at least 6 longs containing an unpacked value
+ // @param result a buffer of at least 4 long to hold the packed value
+ // @return packedQD192 with words 1..3 filled with the packed mantissa. packedQD192[0] is not
+ // affected.
+ pack_6x32_to_3x64(unpackedMant: bigint[], result: bigint[]): void {
+ result[1] = (unpackedMant[0] << 32n) + unpackedMant[1];
+ result[2] = (unpackedMant[2] << 32n) + unpackedMant[3];
+ result[3] = (unpackedMant[4] << 32n) + unpackedMant[5];
+ }
+ // Unpacks the mantissa of a 192-bit quasidecimal (4 longs: exp10, mantHi, mantMid, mantLo) to a
+ // buffer of 6 longs, where the least significant 32 bits of each long contains respective 32 bits
+ // of the mantissa
+ // @param qd192 array of 4 longs containing the number to unpack
+ // @param buff_6x32 buffer of 6 long to hold the unpacked mantissa
+ unpack_3x64_to_6x32(qd192: bigint[], buff_6x32: bigint[]): void {
+ buff_6x32[0] = qd192[1] >> 32n;
+ buff_6x32[1] = qd192[1] & QuadrupleBuilder.LOWER_32_BITS;
+ buff_6x32[2] = qd192[2] >> 32n;
+ buff_6x32[3] = qd192[2] & QuadrupleBuilder.LOWER_32_BITS;
+ buff_6x32[4] = qd192[3] >> 32n;
+ buff_6x32[5] = qd192[3] & QuadrupleBuilder.LOWER_32_BITS;
+ }
+ // Divides the contents of the buffer by 2^exp2
+ // (shifts the buffer rightwards by exp2 if the exp2 is positive, and leftwards if it's negative),
+ // keeping it unpacked (only lower 32 bits of each element are used, except the buff[0] whose
+ // higher half is intended to contain integer part)
+ // @param buffer the buffer to divide
+ // @param exp2 the exponent of the power of two to divide by, expected to be
+ divBuffByPower2(buffer: bigint[], exp2: number): void {
+ const maxIdx: number = buffer.length - 1;
+ const backShift = BigInt(32 - Math.abs(exp2));
+ if (exp2 > 0) {
+ // Shift to the right
+ const exp2Shift = BigInt(exp2);
+ for (let i = maxIdx + 1 - 1; i >= 1; i--) {
+ buffer[i] =
+ (buffer[i] >> exp2Shift) |
+ ((buffer[i - 1] << backShift) & QuadrupleBuilder.LOWER_32_BITS);
+ }
+ buffer[0] = buffer[0] >> exp2Shift; // Preserve the high half of buff[0]
+ } else if (exp2 < 0) {
+ // Shift to the left
+ const exp2Shift = BigInt(-exp2);
+ buffer[0] =
+ ((buffer[0] << exp2Shift) | (buffer[1] >> backShift)) &
+ 0xffffffffffffffffn; // Preserve the high half of buff[0]
+ for (let i = 1; i < maxIdx; i++) {
+ buffer[i] =
+ (((buffer[i] << exp2Shift) & QuadrupleBuilder.LOWER_32_BITS) |
+ (buffer[i + 1] >> backShift)) &
+ 0xffffffffffffffffn;
+ }
+ buffer[maxIdx] =
+ (buffer[maxIdx] << exp2Shift) & QuadrupleBuilder.LOWER_32_BITS;
+ }
+ }
+ // Adds the summand to the idx'th word of the unpacked value stored in the buffer
+ // and propagates carry as necessary
+ // @param buff the buffer to add the summand to
+ // @param idx the index of the element to which the summand is to be added
+ // @param summand the summand to add to the idx'th element of the buffer
+ addToBuff(buff: bigint[], idx: number, summand: bigint): void {
+ const maxIdx: number = idx;
+ buff[maxIdx] = (buff[maxIdx] + summand) & 0xffffffffffffffffn; // Big-endian, the lowest word
+ for (let i = maxIdx + 1 - 1; i >= 1; i--) {
+ // from the lowest word upwards, except the highest
+ if ((buff[i] & QuadrupleBuilder.HIGHER_32_BITS) !== 0n) {
+ buff[i] &= QuadrupleBuilder.LOWER_32_BITS;
+ buff[i - 1] += 1n;
+ } else {
+ break;
+ }
+ }
+ }
+ static clz64(x: bigint): number {
+ const high = Number(x >> 32n);
+ return high === 0
+ ? 32 + Math.clz32(Number(BigInt.asUintN(32, x)))
+ : Math.clz32(high);
+ }
+}
diff --git a/handwritten/firestore/dev/src/reference/field-filter-internal.ts b/handwritten/firestore/dev/src/reference/field-filter-internal.ts
index 3b08c4919814..c79e9baf400c 100644
--- a/handwritten/firestore/dev/src/reference/field-filter-internal.ts
+++ b/handwritten/firestore/dev/src/reference/field-filter-internal.ts
@@ -21,6 +21,7 @@ import api = protos.google.firestore.v1;
import {FilterInternal} from './filter-internal';
import {Serializer} from '../serializer';
import {FieldPath} from '../path';
+import {Decimal128Value} from '../field-value';
/**
* A field constraint for a Query where clause.
@@ -79,7 +80,15 @@ export class FieldFilterInternal extends FilterInternal {
* @internal
*/
isNanChecking(): boolean {
- return typeof this.value === 'number' && isNaN(this.value);
+ if (typeof this.value === 'number' && isNaN(this.value)) {
+ return true;
+ }
+
+ if (this.value instanceof Decimal128Value) {
+ return this.value.value === 'NaN';
+ }
+
+ return false;
}
/**
diff --git a/handwritten/firestore/dev/src/serializer.ts b/handwritten/firestore/dev/src/serializer.ts
index d5164a8d5718..0b4811f75ffd 100644
--- a/handwritten/firestore/dev/src/serializer.ts
+++ b/handwritten/firestore/dev/src/serializer.ts
@@ -18,7 +18,19 @@ import * as firestore from '@google-cloud/firestore';
import * as proto from '../protos/firestore_v1_proto_api';
-import {DeleteTransform, FieldTransform, VectorValue} from './field-value';
+import {
+ BsonBinaryData,
+ BsonTimestamp,
+ DeleteTransform,
+ FieldTransform,
+ Int32Value,
+ MaxKey,
+ MinKey,
+ BsonObjectId,
+ RegexValue,
+ VectorValue,
+ Decimal128Value,
+} from './field-value';
import {detectGoogleProtobufValueType, detectValueType} from './convert';
import {GeoPoint} from './geo-point';
import {DocumentReference, Firestore} from './index';
@@ -31,9 +43,21 @@ import {Pipeline} from './pipelines';
import api = proto.google.firestore.v1;
import {
+ RESERVED_BSON_BINARY_KEY,
+ RESERVED_INT32_KEY,
RESERVED_MAP_KEY,
RESERVED_MAP_KEY_VECTOR_VALUE,
+ RESERVED_MAX_KEY,
+ RESERVED_MIN_KEY,
+ RESERVED_BSON_OBJECT_ID_KEY,
+ RESERVED_REGEX_KEY,
+ RESERVED_REGEX_OPTIONS_KEY,
+ RESERVED_REGEX_PATTERN_KEY,
+ RESERVED_BSON_TIMESTAMP_INCREMENT_KEY,
+ RESERVED_BSON_TIMESTAMP_KEY,
+ RESERVED_BSON_TIMESTAMP_SECONDS_KEY,
VECTOR_MAP_VECTORS_KEY,
+ RESERVED_DECIMAL128_KEY,
} from './map-type';
import {google} from '../protos/firestore_v1_proto_api';
import IMapValue = google.firestore.v1.IMapValue;
@@ -195,7 +219,17 @@ export class Serializer {
};
}
- if (val instanceof VectorValue) {
+ if (
+ val instanceof VectorValue ||
+ val instanceof RegexValue ||
+ val instanceof Int32Value ||
+ val instanceof Decimal128Value ||
+ val instanceof BsonTimestamp ||
+ val instanceof BsonBinaryData ||
+ val instanceof BsonObjectId ||
+ val instanceof MinKey ||
+ val instanceof MaxKey
+ ) {
return val._toProto(this);
}
@@ -309,6 +343,152 @@ export class Serializer {
};
}
+ /**
+ * @private
+ */
+ encodeMinKey(): api.IValue {
+ // A Firestore MinKey is a map with reserved key/value pairs.
+ return {
+ mapValue: {
+ fields: {
+ [RESERVED_MIN_KEY]: {
+ nullValue: 'NULL_VALUE',
+ },
+ },
+ },
+ };
+ }
+
+ /**
+ * @private
+ */
+ encodeMaxKey(): api.IValue {
+ // A Firestore MaxKey is a map with reserved key/value pairs.
+ return {
+ mapValue: {
+ fields: {
+ [RESERVED_MAX_KEY]: {
+ nullValue: 'NULL_VALUE',
+ },
+ },
+ },
+ };
+ }
+
+ /**
+ * @private
+ */
+ encodeRegex(pattern: string, options: string): api.IValue {
+ // A Firestore Regex is a map with reserved key/value pairs.
+ return {
+ mapValue: {
+ fields: {
+ [RESERVED_REGEX_KEY]: {
+ mapValue: {
+ fields: {
+ [RESERVED_REGEX_PATTERN_KEY]: {
+ stringValue: pattern,
+ },
+ [RESERVED_REGEX_OPTIONS_KEY]: {
+ stringValue: options,
+ },
+ },
+ },
+ },
+ },
+ },
+ };
+ }
+
+ /**
+ * @private
+ */
+ encodeBsonObjectId(value: string): api.IValue {
+ return {
+ mapValue: {
+ fields: {
+ [RESERVED_BSON_OBJECT_ID_KEY]: {
+ stringValue: value,
+ },
+ },
+ },
+ };
+ }
+
+ /**
+ * @private
+ */
+ encodeInt32(value: number): api.IValue {
+ return {
+ mapValue: {
+ fields: {
+ [RESERVED_INT32_KEY]: {
+ integerValue: value,
+ },
+ },
+ },
+ };
+ }
+
+ /**
+ * @private
+ */
+ encodeDecimal128(value: string): api.IValue {
+ return {
+ mapValue: {
+ fields: {
+ [RESERVED_DECIMAL128_KEY]: {
+ stringValue: value,
+ },
+ },
+ },
+ };
+ }
+
+ /**
+ * @private
+ */
+ encodeBsonTimestamp(seconds: number, increment: number): api.IValue {
+ return {
+ mapValue: {
+ fields: {
+ [RESERVED_BSON_TIMESTAMP_KEY]: {
+ mapValue: {
+ fields: {
+ [RESERVED_BSON_TIMESTAMP_SECONDS_KEY]: {
+ integerValue: seconds,
+ },
+ [RESERVED_BSON_TIMESTAMP_INCREMENT_KEY]: {
+ integerValue: increment,
+ },
+ },
+ },
+ },
+ },
+ },
+ };
+ }
+
+ /**
+ * @private
+ */
+ encodeBsonBinaryData(subtype: number, data: Uint8Array): api.IValue {
+ const subtypeAndData = new Uint8Array(data.length + 1);
+ // This converts the subtype from `number` to a byte.
+ subtypeAndData[0] = subtype;
+ // Concatenate the rest of the data starting at index 1.
+ subtypeAndData.set(data, /* offset */ 1);
+ return {
+ mapValue: {
+ fields: {
+ [RESERVED_BSON_BINARY_KEY]: {
+ bytesValue: subtypeAndData,
+ },
+ },
+ },
+ };
+ }
+
/**
* Decodes a single Firestore 'Value' Protobuf.
*
@@ -376,6 +556,30 @@ export class Serializer {
const fields = proto.mapValue!.fields!;
return VectorValue._fromProto(fields[VECTOR_MAP_VECTORS_KEY]);
}
+ case 'minKeyValue': {
+ return MinKey.instance();
+ }
+ case 'maxKeyValue': {
+ return MaxKey.instance();
+ }
+ case 'regexValue': {
+ return RegexValue._fromProto(proto);
+ }
+ case 'bsonObjectIdValue': {
+ return BsonObjectId._fromProto(proto);
+ }
+ case 'int32Value': {
+ return Int32Value._fromProto(proto);
+ }
+ case 'decimal128Value': {
+ return Decimal128Value._fromProto(proto);
+ }
+ case 'bsonTimestampValue': {
+ return BsonTimestamp._fromProto(proto);
+ }
+ case 'bsonBinaryValue': {
+ return BsonBinaryData._fromProto(proto);
+ }
case 'geoPointValue': {
return GeoPoint.fromProto(proto.geoPointValue!);
}
@@ -547,7 +751,17 @@ export function validateUserInput(
'If you want to ignore undefined values, enable `ignoreUndefinedProperties`.',
);
}
- } else if (value instanceof VectorValue) {
+ } else if (
+ value instanceof VectorValue ||
+ value instanceof RegexValue ||
+ value instanceof BsonObjectId ||
+ value instanceof Int32Value ||
+ value instanceof Decimal128Value ||
+ value instanceof BsonTimestamp ||
+ value instanceof BsonBinaryData ||
+ value instanceof MinKey ||
+ value instanceof MaxKey
+ ) {
// OK
} else if (value instanceof DeleteTransform) {
if (inArray) {
diff --git a/handwritten/firestore/dev/system-test/euqality_matcher.ts b/handwritten/firestore/dev/system-test/euqality_matcher.ts
new file mode 100644
index 000000000000..bf0bd7b3b106
--- /dev/null
+++ b/handwritten/firestore/dev/system-test/euqality_matcher.ts
@@ -0,0 +1,165 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {use} from 'chai';
+
+/**
+ * Duck-typed interface for objects that have an isEqual() method.
+ *
+ * Note: This is copied from src/util/misc.ts to avoid importing private types.
+ */
+export interface Equatable {
+ isEqual(other: T): boolean;
+}
+
+/**
+ * Custom equals override for types that have a free-standing equals functions
+ * (such as `queryEquals()`).
+ */
+export interface CustomMatcher {
+ equalsFn: (left: T, right: T) => boolean;
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ forType: Function;
+}
+
+/**
+ * @file This file provides a helper function to add a matcher that matches
+ * based on an objects isEqual method. If the isEqual method is present one
+ * either object it is used to determine equality, else mocha's default isEqual
+ * implementation is used.
+ */
+
+function customDeepEqual(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ customMatchers: Array>,
+ left: unknown,
+ right: unknown
+): boolean {
+ for (const customMatcher of customMatchers) {
+ if (
+ left instanceof customMatcher.forType &&
+ right instanceof customMatcher.forType
+ ) {
+ return customMatcher.equalsFn(left, right);
+ }
+ }
+ if (left && typeof left === 'object' && right && typeof right === 'object') {
+ // The `isEqual` check below returns true if firestore-exp types are
+ // compared with API types from Firestore classic. We do want to
+ // differentiate between these types in our tests to ensure that the we do
+ // not return firestore-exp types in the classic SDK.
+ const leftObj = left as Record;
+ const rightObj = right as Record;
+ if (
+ leftObj.constructor.name === rightObj.constructor.name &&
+ leftObj.constructor !== rightObj.constructor
+ ) {
+ return false;
+ }
+ }
+ if (typeof left === 'object' && left && 'isEqual' in left) {
+ return (left as Equatable).isEqual(right);
+ }
+ if (typeof right === 'object' && right && 'isEqual' in right) {
+ return (right as Equatable).isEqual(left);
+ }
+ if (left === right) {
+ return true;
+ }
+ if (
+ typeof left === 'number' &&
+ typeof right === 'number' &&
+ isNaN(left) &&
+ isNaN(right)
+ ) {
+ return true;
+ }
+ if (typeof left !== typeof right) {
+ return false;
+ } // needed for structurally different objects
+ if (Object(left) !== left) {
+ return false;
+ } // primitive values
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const keys = Object.keys(left as any);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ if (keys.length !== Object.keys(right as any).length) {
+ return false;
+ }
+ for (let i = 0; i < keys.length; i++) {
+ const key = keys[i];
+ if (!Object.prototype.hasOwnProperty.call(right, key)) {
+ return false;
+ }
+ if (
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ !customDeepEqual(customMatchers, (left as any)[key], (right as any)[key])
+ ) {
+ return false;
+ }
+ }
+ return true;
+}
+
+/** The original equality function passed in by chai(). */
+let originalFunction: ((expected: unknown) => void) | null = null;
+
+export function addEqualityMatcher(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ ...customMatchers: Array>
+): void {
+ let isActive = true;
+
+ before(() => {
+ use((chai, utils) => {
+ const Assertion = chai.Assertion;
+
+ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
+ const assertEql = (_super: (expected: unknown) => void) => {
+ originalFunction = originalFunction || _super;
+ return function (this: Chai.Assertion, expected?: unknown): void {
+ if (isActive) {
+ const actual = utils.flag(this, 'object');
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const assertion = new (chai.Assertion as any)();
+ utils.transferFlags(this, assertion, /*includeAll=*/ true);
+ // NOTE: Unlike the top-level chai assert() method, Assertion.assert()
+ // takes the expected value before the actual value.
+ assertion.assert(
+ customDeepEqual(customMatchers, actual, expected),
+ 'expected #{act} to roughly deeply equal #{exp}',
+ 'expected #{act} to not roughly deeply equal #{exp}',
+ expected,
+ actual,
+ /*showDiff=*/ true
+ );
+ } else if (originalFunction) {
+ originalFunction.call(this, expected);
+ }
+ };
+ };
+
+ Assertion.overwriteMethod('eql', assertEql);
+ Assertion.overwriteMethod('eqls', assertEql);
+ });
+ });
+
+ after(() => {
+ isActive = false;
+ });
+}
diff --git a/handwritten/firestore/dev/system-test/firestore.ts b/handwritten/firestore/dev/system-test/firestore.ts
index 779133460f94..8a37b7265c68 100644
--- a/handwritten/firestore/dev/system-test/firestore.ts
+++ b/handwritten/firestore/dev/system-test/firestore.ts
@@ -39,6 +39,14 @@ import {
FieldValue,
Firestore,
GeoPoint,
+ MinKey,
+ MaxKey,
+ Int32Value,
+ Decimal128Value,
+ BsonTimestamp,
+ BsonBinaryData,
+ BsonObjectId,
+ RegexValue,
Query,
QueryDocumentSnapshot,
setLogFunction,
@@ -62,6 +70,7 @@ import {CollectionGroup} from '../src/collection-group';
import IBundleElement = firestore.IBundleElement;
import {Filter} from '../src/filter';
import {IndexTestHelper} from './index_test_helper';
+import {addEqualityMatcher} from './euqality_matcher';
use(chaiAsPromised);
@@ -8702,3 +8711,647 @@ describe('Types test', () => {
});
});
});
+
+describe('non-native Firestore types', () => {
+ addEqualityMatcher();
+ let firestore: Firestore;
+ let randomCol: CollectionReference;
+ let doc: DocumentReference;
+
+ beforeEach(async () => {
+ randomCol = getTestRoot();
+ firestore = randomCol.firestore;
+ doc = randomCol.doc();
+ });
+
+ afterEach(() => verifyInstance(firestore));
+
+ async function getFirstSnapshot(query: Query): Promise {
+ const deferred = new DeferredPromise();
+ deferred.promise = new Promise((resolve, reject) => {
+ deferred.resolve = resolve;
+ deferred.reject = reject;
+ });
+
+ const unsubscribe = query.onSnapshot(
+ snapshot => {
+ deferred.resolve(snapshot);
+ },
+ err => {
+ deferred.reject(err);
+ }
+ );
+
+ const snapshot_1 = await deferred.promise!;
+ unsubscribe();
+ return snapshot_1 as QuerySnapshot;
+ }
+
+ interface TypeWithEquality {
+ isEqual(other: TypeWithEquality): boolean;
+ }
+
+ async function addDocs(docs: {
+ [key: string]: DocumentData;
+ }): Promise {
+ const futures = [];
+ for (const key of Object.keys(docs)) {
+ futures.push(randomCol.doc(key).set(docs[key]));
+ }
+ return Promise.all(futures);
+ }
+
+ function toDataArray(docSet: QuerySnapshot): DocumentData[] {
+ return docSet.docs.map(d => d.data());
+ }
+
+ function toIds(docSet: QuerySnapshot): string[] {
+ return docSet.docs.map(d => d.id);
+ }
+
+ async function checkRoundTrip(originalValue: T) {
+ await doc.set({key: originalValue});
+ const getResult = await doc.get();
+ expect(getResult.data()).to.not.be.undefined;
+ const roundTripValue: T = getResult.data()!['key'];
+ expect(roundTripValue.isEqual(originalValue)).to.be.true;
+ }
+
+ it('round trip a min key value', async () => {
+ await doc.set({key: MinKey.instance()});
+ const getResult = await doc.get();
+ expect(getResult.data()).to.not.be.undefined;
+ const roundTripValue = getResult.data()!['key'];
+ expect(roundTripValue === MinKey.instance()).to.be.true;
+ expect(roundTripValue === MaxKey.instance()).to.be.false;
+ });
+
+ it('round trip a max key value', async () => {
+ await doc.set({key: MaxKey.instance()});
+ const getResult = await doc.get();
+ expect(getResult.data()).to.not.be.undefined;
+ const roundTripValue = getResult.data()!['key'];
+ expect(roundTripValue === MinKey.instance()).to.be.false;
+ expect(roundTripValue === MaxKey.instance()).to.be.true;
+ });
+
+ it('round trip an object id value', async () => {
+ await checkRoundTrip(new BsonObjectId('507f191e810c19729de860ea'));
+ });
+
+ it('round trip a regex value', async () => {
+ await checkRoundTrip(new RegexValue('^foo', 'i'));
+ });
+
+ it('round trip a 32-bit integer', async () => {
+ await checkRoundTrip(new Int32Value(-57));
+ await checkRoundTrip(new Int32Value(0));
+ await checkRoundTrip(new Int32Value(57));
+ });
+
+ it('round trip a 128-bit decimal', async () => {
+ await checkRoundTrip(new Decimal128Value('NaN'));
+ await checkRoundTrip(new Decimal128Value('-Infinity'));
+ await checkRoundTrip(new Decimal128Value('-1.2e3'));
+ await checkRoundTrip(new Decimal128Value('-4.2e+3'));
+ await checkRoundTrip(new Decimal128Value('-1.2e-3'));
+ await checkRoundTrip(new Decimal128Value('-4.2e-3'));
+ await checkRoundTrip(new Decimal128Value('-1'));
+ await checkRoundTrip(new Decimal128Value('-0'));
+ await checkRoundTrip(new Decimal128Value('0'));
+ await checkRoundTrip(new Decimal128Value('-0.0'));
+ await checkRoundTrip(new Decimal128Value('0.0'));
+ await checkRoundTrip(new Decimal128Value('1'));
+ await checkRoundTrip(new Decimal128Value('1.2e3'));
+ await checkRoundTrip(new Decimal128Value('4.2e+3'));
+ await checkRoundTrip(new Decimal128Value('1.2e-3'));
+ await checkRoundTrip(new Decimal128Value('4.2e-3'));
+ await checkRoundTrip(new Decimal128Value('Infinity'));
+ await checkRoundTrip(
+ new Decimal128Value('0.1234567890123456789012345678901234')
+ );
+ await checkRoundTrip(
+ new Decimal128Value('1234567890123456789012345678901234')
+ );
+ await checkRoundTrip(
+ new Decimal128Value('-0.1234567890123456789012345678901234')
+ );
+ await checkRoundTrip(
+ new Decimal128Value('-1234567890123456789012345678901234')
+ );
+ });
+
+ it('invalid decimal128 gets rejected', async () => {
+ const docRef = randomCol.doc();
+ let errorMessage = '';
+ try {
+ await docRef.set({key: new Decimal128Value('')});
+ } catch (err) {
+ errorMessage = err?.message;
+ }
+ expect(errorMessage).to.contains('Invalid decimal128 string');
+
+ try {
+ errorMessage = '';
+ await docRef.set({key: new Decimal128Value('1 23. 4')});
+ } catch (err) {
+ errorMessage = err?.message;
+ }
+ expect(errorMessage).to.contains('Invalid decimal128 string');
+
+ try {
+ errorMessage = '';
+ await docRef.set({key: new Decimal128Value('abc')});
+ } catch (err) {
+ errorMessage = err?.message;
+ }
+ expect(errorMessage).to.contains('Invalid decimal128 string');
+ });
+
+ it('round trip a BSON timestamp', async () => {
+ await checkRoundTrip(new BsonTimestamp(57, 1));
+ });
+
+ it('round trip BSON binary data', async () => {
+ await checkRoundTrip(new BsonBinaryData(128, Buffer.from([5, 6, 7])));
+ });
+
+ it('invalid 32-bit integer gets rejected', async () => {
+ let error1: Error | null = null;
+ try {
+ await doc.set({key: new Int32Value(2147483648)});
+ } catch (e) {
+ error1 = e;
+ }
+
+ expect(error1).to.not.be.null;
+ expect(error1!.message).to.contain(
+ "The field '__int__' value (2,147,483,648) is too large to be converted to a 32-bit integer"
+ );
+
+ let error2: Error | null = null;
+ try {
+ await doc.set({key: new Int32Value(-2147483650)});
+ } catch (e) {
+ error2 = e;
+ }
+ expect(error2).to.not.be.null;
+ expect(error2!.message).to.contain(
+ "The field '__int__' value (-2,147,483,650) is too large to be converted to a 32-bit integer."
+ );
+ });
+
+ it('BSON timestamp larger than 32-bit integer gets rejected', async () => {
+ let error: Error | null = null;
+ try {
+ await doc.set({key: new BsonTimestamp(4294967296, 2)});
+ } catch (e) {
+ error = e;
+ }
+
+ expect(error).to.not.be.null;
+ expect(error!.message).to.contain(
+ "BsonTimestamp 'seconds' must be in the range of a 32-bit unsigned integer."
+ );
+
+ error = null;
+ try {
+ await doc.set({key: new BsonTimestamp(2, 4294967296)});
+ } catch (e) {
+ error = e;
+ }
+
+ expect(error).to.not.be.null;
+ expect(error!.message).to.contain(
+ "BsonTimestamp 'increment' must be in the range of a 32-bit unsigned integer."
+ );
+ });
+
+ it('negative BSON timestamp gets rejected', async () => {
+ let error: Error | null = null;
+ try {
+ await doc.set({key: new BsonTimestamp(-1, 2)});
+ } catch (e) {
+ error = e;
+ }
+
+ expect(error).to.not.be.null;
+ expect(error!.message).to.contain(
+ "BsonTimestamp 'seconds' must be in the range of a 32-bit unsigned integer."
+ );
+
+ error = null;
+ try {
+ await doc.set({key: new BsonTimestamp(1, -2)});
+ } catch (e) {
+ error = e;
+ }
+
+ expect(error).to.not.be.null;
+ expect(error!.message).to.contain(
+ "BsonTimestamp 'increment' must be in the range of a 32-bit unsigned integer."
+ );
+ });
+
+ it('invalid regex value gets rejected', async () => {
+ let error: Error | null = null;
+ try {
+ await doc.set({key: new RegexValue('foo', 'a')});
+ } catch (e) {
+ error = e;
+ }
+
+ expect(error).to.not.be.null;
+ expect(error!.message).to.contain(
+ "Invalid regex option 'a'. Supported options are 'i', 'm', 's', 'u', and 'x'."
+ );
+ });
+
+ it('invalid bsonObjectId value gets rejected', async () => {
+ let error: Error | null = null;
+ try {
+ await doc.set({key: new BsonObjectId('foo')});
+ } catch (e) {
+ error = e;
+ }
+
+ expect(error).to.not.be.null;
+ expect(error!.message).to.contain(
+ 'Object ID hex string has incorrect length.'
+ );
+ });
+
+ it('invalid bsonBinaryData value gets rejected', async () => {
+ let error: Error | null = null;
+ try {
+ await doc.set({
+ key: new BsonBinaryData(1234, new Uint8Array([1, 2, 3])),
+ });
+ } catch (e) {
+ error = e;
+ }
+
+ expect(error).to.not.be.null;
+ expect(error!.message).to.contain(
+ 'The subtype for BsonBinaryData must be a value in the inclusive [0, 255] range.'
+ );
+ });
+
+ it('can filter and order objectIds', async () => {
+ const testDocs = {
+ a: {key: new BsonObjectId('507f191e810c19729de860ea')},
+ b: {key: new BsonObjectId('507f191e810c19729de860eb')},
+ c: {key: new BsonObjectId('507f191e810c19729de860ec')},
+ };
+
+ await addDocs(testDocs);
+ let orderedQuery = randomCol
+ .where('key', '>', new BsonObjectId('507f191e810c19729de860ea'))
+ .orderBy('key', 'desc');
+
+ let snapshot = await getFirstSnapshot(orderedQuery);
+ expect(toDataArray(snapshot)).to.deep.equal([testDocs['c'], testDocs['b']]);
+
+ orderedQuery = randomCol
+ .where('key', 'in', [
+ new BsonObjectId('507f191e810c19729de860ea'),
+ new BsonObjectId('507f191e810c19729de860eb'),
+ ])
+ .orderBy('key', 'desc');
+
+ snapshot = await getFirstSnapshot(orderedQuery);
+ expect(toDataArray(snapshot)).to.deep.equal([testDocs['b'], testDocs['a']]);
+ });
+
+ it('can filter and order Int32 values', async () => {
+ const testDocs = {
+ a: {key: new Int32Value(-1)},
+ b: {key: new Int32Value(1)},
+ c: {key: new Int32Value(2)},
+ };
+ await addDocs(testDocs);
+ let orderedQuery = randomCol
+ .where('key', '>=', new Int32Value(1))
+ .orderBy('key', 'desc');
+
+ let snapshot = await getFirstSnapshot(orderedQuery);
+ expect(toDataArray(snapshot)).to.deep.equal([testDocs['c'], testDocs['b']]);
+
+ orderedQuery = randomCol
+ .where('key', 'not-in', [new Int32Value(1)])
+ .orderBy('key', 'desc');
+
+ snapshot = await getFirstSnapshot(orderedQuery);
+ expect(toDataArray(snapshot)).to.deep.equal([testDocs['c'], testDocs['a']]);
+ });
+
+ it('can filter and order Decimal128 values', async () => {
+ const testDocs = {
+ a: {key: new Decimal128Value('-1.2e3')},
+ b: {key: new Decimal128Value('0')},
+ c: {key: new Decimal128Value('1.2e-3')},
+ d: {key: new Decimal128Value('NaN')},
+ e: {key: new Decimal128Value('-Infinity')},
+ f: {key: new Decimal128Value('Infinity')},
+ };
+ await addDocs(testDocs);
+ let orderedQuery = randomCol
+ .where('key', '>=', new Decimal128Value('-1.1'))
+ .orderBy('key', 'desc');
+
+ let snapshot = await getFirstSnapshot(orderedQuery);
+ expect(toDataArray(snapshot)).to.deep.equal([
+ testDocs['f'],
+ testDocs['c'],
+ testDocs['b'],
+ ]);
+
+ orderedQuery = randomCol
+ .where('key', '!=', new Decimal128Value('0.0'))
+ .orderBy('key', 'desc');
+ snapshot = await getFirstSnapshot(orderedQuery);
+ expect(toDataArray(snapshot)).to.deep.equal([
+ testDocs['f'],
+ testDocs['c'],
+ testDocs['a'],
+ testDocs['e'],
+ testDocs['d'],
+ ]);
+
+ orderedQuery = randomCol
+ .where('key', '>', new Decimal128Value('-1.2e-3'))
+ .orderBy('key', 'desc');
+ snapshot = await getFirstSnapshot(orderedQuery);
+ expect(toDataArray(snapshot)).to.deep.equal([
+ testDocs['f'],
+ testDocs['c'],
+ testDocs['b'],
+ ]);
+
+ orderedQuery = randomCol
+ .where('key', '!=', new Decimal128Value('NaN'))
+ .orderBy('key');
+ snapshot = await getFirstSnapshot(orderedQuery);
+ expect(toDataArray(snapshot)).to.deep.equal([
+ testDocs['e'],
+ testDocs['a'],
+ testDocs['b'],
+ testDocs['c'],
+ testDocs['f'],
+ ]);
+
+ orderedQuery = randomCol
+ .where('key', 'not-in', [
+ new Decimal128Value('1.2e-3'),
+ new Decimal128Value('Infinity'),
+ ])
+ .orderBy('key', 'desc');
+ snapshot = await getFirstSnapshot(orderedQuery);
+ expect(toDataArray(snapshot)).to.deep.equal([
+ testDocs['b'],
+ testDocs['a'],
+ testDocs['e'],
+ testDocs['d'],
+ ]);
+ });
+
+ it('can filter and order numerical values ', async () => {
+ const testDocs = {
+ a: {key: new Decimal128Value('-1.2e3')}, // -1200
+ b: {key: new Int32Value(0)},
+ c: {key: new Decimal128Value('1')},
+ d: {key: new Int32Value(1)},
+ e: {key: 1},
+ f: {key: 1.0},
+ g: {key: new Decimal128Value('1.2e-3')}, // 0.0012
+ h: {key: new Int32Value(2)},
+ i: {key: new Decimal128Value('NaN')},
+ j: {key: new Decimal128Value('-Infinity')},
+ k: {key: NaN},
+ l: {key: Infinity},
+ };
+
+ await addDocs(testDocs);
+ let orderedQuery = randomCol.orderBy('key', 'desc');
+ let snapshot = await getFirstSnapshot(orderedQuery);
+ expect(toIds(snapshot)).to.deep.equal([
+ 'l', // Infinity
+ 'h', // 2
+ 'f', // 1.0
+ 'e', // 1
+ 'd', // 1
+ 'c', // 1
+ 'g', // 0.0012
+ 'b', // 0
+ 'a', // -1200
+ 'j', // -Infinity
+ 'k', // NaN
+ 'i', // NaN
+ ]);
+
+ orderedQuery = randomCol
+ .where('key', '!=', new Decimal128Value('1.0'))
+ .orderBy('key', 'desc');
+ snapshot = await getFirstSnapshot(orderedQuery);
+ expect(toIds(snapshot)).to.deep.equal([
+ 'l',
+ 'h',
+ 'g',
+ 'b',
+ 'a',
+ 'j',
+ 'k',
+ 'i',
+ ]);
+
+ orderedQuery = randomCol.where('key', '==', 1).orderBy('key', 'desc');
+ snapshot = await getFirstSnapshot(orderedQuery);
+ expect(toIds(snapshot)).to.deep.equal(['f', 'e', 'd', 'c']);
+ });
+
+ it('decimal128 values with no 2s complement representation', async () => {
+ const testDocs = {
+ a: {key: new Decimal128Value('-1.1e-3')}, // -0.0011
+ b: {key: new Decimal128Value('1.1')},
+ c: {key: 1.1},
+ d: {key: 1.0},
+ e: {key: new Decimal128Value('1.1e-3')}, // 0.0011
+ };
+
+ await addDocs(testDocs);
+ let orderedQuery = randomCol.where('key', '==', new Decimal128Value('1.1'));
+ let snapshot = await getFirstSnapshot(orderedQuery);
+ expect(toIds(snapshot)).to.deep.equal(['b']);
+
+ orderedQuery = randomCol
+ .where('key', '!=', new Decimal128Value('1.1'))
+ .orderBy('key');
+ snapshot = await getFirstSnapshot(orderedQuery);
+ expect(toIds(snapshot)).to.deep.equal(['a', 'e', 'd', 'c']);
+
+ orderedQuery = randomCol.where('key', '==', 1.1);
+ snapshot = await getFirstSnapshot(orderedQuery);
+ expect(toIds(snapshot)).to.deep.equal(['c']);
+
+ orderedQuery = randomCol.where('key', '!=', 1.1).orderBy('key');
+ snapshot = await getFirstSnapshot(orderedQuery);
+ expect(toIds(snapshot)).to.deep.equal(['a', 'e', 'd', 'b']);
+ });
+
+ it('can filter and order Timestamp values', async () => {
+ const testDocs = {
+ a: {key: new BsonTimestamp(1, 1)},
+ b: {key: new BsonTimestamp(1, 2)},
+ c: {key: new BsonTimestamp(2, 1)},
+ };
+ await addDocs(testDocs);
+
+ let orderedQuery = randomCol
+ .where('key', '>', new BsonTimestamp(1, 1))
+ .orderBy('key', 'desc');
+
+ let snapshot = await getFirstSnapshot(orderedQuery);
+ expect(toDataArray(snapshot)).to.deep.equal([testDocs['c'], testDocs['b']]);
+
+ orderedQuery = randomCol
+ .where('key', '!=', new BsonTimestamp(1, 1))
+ .orderBy('key', 'desc');
+
+ snapshot = await getFirstSnapshot(orderedQuery);
+ expect(toDataArray(snapshot)).to.deep.equal([testDocs['c'], testDocs['b']]);
+ });
+
+ it('can filter and order Binary values', async () => {
+ const testDocs = {
+ a: {key: new BsonBinaryData(1, new Uint8Array([1, 2, 3]))},
+ b: {key: new BsonBinaryData(1, new Uint8Array([1, 2, 4]))},
+ c: {key: new BsonBinaryData(2, new Uint8Array([1, 2, 3]))},
+ };
+ await addDocs(testDocs);
+
+ let orderedQuery = randomCol
+ .where('key', '>', new BsonBinaryData(1, new Uint8Array([1, 2, 3])))
+ .orderBy('key', 'desc');
+
+ let snapshot = await getFirstSnapshot(orderedQuery);
+ expect(toDataArray(snapshot)).to.deep.equal([testDocs['c'], testDocs['b']]);
+
+ orderedQuery = randomCol
+ .where('key', '>=', new BsonBinaryData(1, new Uint8Array([1, 2, 3])))
+ .where('key', '<', new BsonBinaryData(2, new Uint8Array([1, 2, 3])))
+ .orderBy('key', 'desc');
+
+ snapshot = await getFirstSnapshot(orderedQuery);
+ expect(toDataArray(snapshot)).to.deep.equal([testDocs['b'], testDocs['a']]);
+ });
+
+ it('can filter and order Regex values', async () => {
+ const testDocs = {
+ a: {key: new RegexValue('^bar', 'i')},
+ b: {key: new RegexValue('^bar', 'x')},
+ c: {key: new RegexValue('^baz', 'i')},
+ };
+ await addDocs(testDocs);
+
+ const orderedQuery = randomCol
+ .where(
+ Filter.or(
+ Filter.where('key', '>', new RegexValue('^bar', 'x')),
+ Filter.where('key', '!=', new RegexValue('^bar', 'x'))
+ )
+ )
+ .orderBy('key', 'desc');
+
+ const snapshot = await getFirstSnapshot(orderedQuery);
+ expect(toDataArray(snapshot)).to.deep.equal([testDocs['c'], testDocs['a']]);
+ });
+
+ it('can filter and order minKey values', async () => {
+ const testDocs = {
+ a: {key: MinKey.instance()},
+ b: {key: MinKey.instance()},
+ c: {key: MaxKey.instance()},
+ d: {key: null},
+ };
+ await addDocs(testDocs);
+
+ const orderedQuery = randomCol
+ .where('key', '==', MinKey.instance())
+ .orderBy('key', 'desc'); // minKeys are equal, would sort by documentId as secondary order
+
+ const snapshot = await getFirstSnapshot(orderedQuery);
+ expect(toDataArray(snapshot)).to.deep.equal([testDocs['b'], testDocs['a']]);
+ });
+
+ it('can filter and order maxKey values', async () => {
+ const testDocs = {
+ a: {key: MinKey.instance()},
+ b: {key: MaxKey.instance()},
+ c: {key: MaxKey.instance()},
+ };
+ await addDocs(testDocs);
+ const orderedQuery = randomCol
+ .where('key', '==', MaxKey.instance())
+ .orderBy('key', 'desc'); // maxKeys are equal, would sort by documentId as secondary order
+ const snapshot = await getFirstSnapshot(orderedQuery);
+ expect(toDataArray(snapshot)).to.deep.equal([testDocs['c'], testDocs['b']]);
+ });
+
+ it('cross-type order', async () => {
+ await addDocs({
+ t: {key: null},
+ u: {key: MinKey.instance()},
+ c: {key: true},
+ d: {key: NaN},
+ e: {key: new Int32Value(1)},
+ f: {key: 2.0},
+ g: {key: new Decimal128Value('2.01e-5')},
+ h: {key: 3},
+ i: {key: new Timestamp(100, 123456000)},
+ j: {key: new BsonTimestamp(1, 2)},
+ k: {key: 'string'},
+ l: {key: new Uint8Array([0, 1, 255])},
+ m: {key: new BsonBinaryData(1, new Uint8Array([1, 2, 3]))},
+ n: {key: randomCol.firestore.collection('c1').doc('doc')},
+ o: {key: new BsonObjectId('507f191e810c19729de860ea')},
+ p: {key: new GeoPoint(0, 0)},
+ q: {key: new RegexValue('^foo', 'i')},
+ r: {key: [1, 2]},
+ s: {key: FieldValue.vector([1, 2])},
+ a: {key: {a: 1}},
+ b: {key: MaxKey.instance()},
+ });
+
+ const expectedResult = [
+ 'b',
+ 'a',
+ 's',
+ 'r',
+ 'q',
+ 'p',
+ 'o',
+ 'n',
+ 'm',
+ 'l',
+ 'k',
+ 'j',
+ 'i',
+ 'h',
+ 'f',
+ 'e',
+ 'g',
+ 'd',
+ 'c',
+ 'u',
+ 't',
+ ];
+
+ const result = await getFirstSnapshot(randomCol.orderBy('key', 'desc'));
+ expect(result.docs.map(e => e.id)).to.deep.equal(expectedResult);
+
+ const listenerResult = await getFirstSnapshot(
+ randomCol.orderBy('key', 'desc')
+ );
+ expect(listenerResult.docs.map(e => e.id)).to.deep.equal(expectedResult);
+ });
+});
diff --git a/handwritten/firestore/dev/test/document.ts b/handwritten/firestore/dev/test/document.ts
index 32bb38dcec13..f572f60b4001 100644
--- a/handwritten/firestore/dev/test/document.ts
+++ b/handwritten/firestore/dev/test/document.ts
@@ -25,6 +25,14 @@ import {
GeoPoint,
setLogFunction,
Timestamp,
+ BsonBinaryData,
+ BsonObjectId,
+ BsonTimestamp,
+ Decimal128Value,
+ Int32Value,
+ MaxKey,
+ MinKey,
+ RegexValue,
} from '../src';
import {
ApiOverride,
@@ -498,6 +506,240 @@ describe('serialize document', () => {
embedding1: FieldValue.vector([0, 1, 2]),
});
});
+
+ it('is able to translate MinKey to internal representation', async () => {
+ const overrides: ApiOverride = {
+ commit: request => {
+ requestEquals(
+ request,
+ set({
+ document: document('documentId', 'myMinKey', {
+ mapValue: {
+ fields: {
+ __min__: {
+ nullValue: 'NULL_VALUE',
+ },
+ },
+ },
+ }),
+ })
+ );
+ return response(writeResult(1));
+ },
+ };
+
+ const firestore = await createInstance(overrides);
+ await firestore.doc('collectionId/documentId').set({
+ myMinKey: MinKey.instance(),
+ });
+ });
+
+ it('is able to translate MaxKey to internal representation', async () => {
+ const overrides: ApiOverride = {
+ commit: request => {
+ requestEquals(
+ request,
+ set({
+ document: document('documentId', 'myMaxKey', {
+ mapValue: {
+ fields: {
+ __max__: {
+ nullValue: 'NULL_VALUE',
+ },
+ },
+ },
+ }),
+ })
+ );
+ return response(writeResult(1));
+ },
+ };
+
+ const firestore = await createInstance(overrides);
+ await firestore.doc('collectionId/documentId').set({
+ myMaxKey: MaxKey.instance(),
+ });
+ });
+
+ it('is able to translate regex to internal representation', async () => {
+ const overrides: ApiOverride = {
+ commit: request => {
+ requestEquals(
+ request,
+ set({
+ document: document('documentId', 'myRegexValue', {
+ mapValue: {
+ fields: {
+ __regex__: {
+ mapValue: {
+ fields: {
+ pattern: {
+ stringValue: 'foo',
+ },
+ options: {
+ stringValue: 'bar',
+ },
+ },
+ },
+ },
+ },
+ },
+ }),
+ })
+ );
+ return response(writeResult(1));
+ },
+ };
+
+ const firestore = await createInstance(overrides);
+ await firestore.doc('collectionId/documentId').set({
+ myRegexValue: new RegexValue('foo', 'bar'),
+ });
+ });
+
+ it('is able to translate objectId to internal representation', async () => {
+ const overrides: ApiOverride = {
+ commit: request => {
+ requestEquals(
+ request,
+ set({
+ document: document('documentId', 'myObjectIdValue', {
+ mapValue: {
+ fields: {
+ __oid__: {
+ stringValue: 'foo',
+ },
+ },
+ },
+ }),
+ })
+ );
+ return response(writeResult(1));
+ },
+ };
+
+ const firestore = await createInstance(overrides);
+ await firestore.doc('collectionId/documentId').set({
+ myObjectIdValue: new BsonObjectId('foo'),
+ });
+ });
+
+ it('is able to translate int32 to internal representation', async () => {
+ const overrides: ApiOverride = {
+ commit: request => {
+ requestEquals(
+ request,
+ set({
+ document: document('documentId', 'myInt32', {
+ mapValue: {
+ fields: {
+ __int__: {
+ integerValue: 12345,
+ },
+ },
+ },
+ }),
+ })
+ );
+ return response(writeResult(1));
+ },
+ };
+
+ const firestore = await createInstance(overrides);
+ await firestore.doc('collectionId/documentId').set({
+ myInt32: new Int32Value(12345),
+ });
+ });
+
+ it('is able to translate decimal128 to internal representation', async () => {
+ const overrides: ApiOverride = {
+ commit: request => {
+ requestEquals(
+ request,
+ set({
+ document: document('documentId', 'myDecimal128', {
+ mapValue: {
+ fields: {
+ __decimal128__: {
+ stringValue: '1.2e-3',
+ },
+ },
+ },
+ }),
+ })
+ );
+ return response(writeResult(1));
+ },
+ };
+
+ const firestore = await createInstance(overrides);
+ await firestore.doc('collectionId/documentId').set({
+ myDecimal128: new Decimal128Value('1.2e-3'),
+ });
+ });
+
+ it('is able to translate request timestamp to internal representation', async () => {
+ const overrides: ApiOverride = {
+ commit: request => {
+ requestEquals(
+ request,
+ set({
+ document: document('documentId', 'myBsonTimestamp', {
+ mapValue: {
+ fields: {
+ __request_timestamp__: {
+ mapValue: {
+ fields: {
+ seconds: {
+ integerValue: 12345,
+ },
+ increment: {
+ integerValue: 67,
+ },
+ },
+ },
+ },
+ },
+ },
+ }),
+ })
+ );
+ return response(writeResult(1));
+ },
+ };
+
+ const firestore = await createInstance(overrides);
+ await firestore.doc('collectionId/documentId').set({
+ myBsonTimestamp: new BsonTimestamp(12345, 67),
+ });
+ });
+
+ it('is able to translate bson binary data to internal representation', async () => {
+ const overrides: ApiOverride = {
+ commit: request => {
+ requestEquals(
+ request,
+ set({
+ document: document('documentId', 'myBsonBinaryData', {
+ mapValue: {
+ fields: {
+ __binary__: {
+ bytesValue: new Uint8Array([250, 1, 2, 3]),
+ },
+ },
+ },
+ }),
+ })
+ );
+ return response(writeResult(1));
+ },
+ };
+
+ const firestore = await createInstance(overrides);
+ await firestore.doc('collectionId/documentId').set({
+ myBsonBinaryData: new BsonBinaryData(250, Buffer.from([1, 2, 3])),
+ });
+ });
});
describe('deserialize document', () => {
diff --git a/handwritten/firestore/dev/test/field-value.ts b/handwritten/firestore/dev/test/field-value.ts
index bc8dd76d4cde..5932fa66b133 100644
--- a/handwritten/firestore/dev/test/field-value.ts
+++ b/handwritten/firestore/dev/test/field-value.ts
@@ -15,7 +15,17 @@
import {describe, it} from 'mocha';
import {expect} from 'chai';
-import {FieldValue} from '../src';
+import {
+ MaxKey,
+ MinKey,
+ FieldValue,
+ BsonBinaryData,
+ BsonObjectId,
+ BsonTimestamp,
+ Decimal128Value,
+ Int32Value,
+ RegexValue,
+} from '../src';
import {
ApiOverride,
arrayTransform,
@@ -31,6 +41,8 @@ import {
set,
writeResult,
} from './util/helpers';
+import {compare} from '../src/order';
+import {RESERVED_BSON_BINARY_KEY, RESERVED_MIN_KEY} from '../src/map-type';
function genericFieldValueTests(methodName: string, sentinel: FieldValue) {
it("can't be used inside arrays", () => {
@@ -398,3 +410,230 @@ describe('FieldValue.serverTimestamp()', () => {
FieldValue.serverTimestamp(),
);
});
+
+describe('non-native types', () => {
+ it('BSON timestamp members', () => {
+ const value = new BsonTimestamp(57, 4);
+ expect(value.seconds).to.equal(57);
+ expect(value.increment).to.equal(4);
+ });
+
+ it('BSON object id', () => {
+ const bsonObjectId = new BsonObjectId('foobar');
+ expect(bsonObjectId.value).to.equal('foobar');
+ });
+
+ it('regular expression', () => {
+ const regex = new RegexValue('^foo', 'i');
+ expect(regex.pattern).to.equal('^foo');
+ expect(regex.options).to.equal('i');
+ });
+
+ it('32-bit int', () => {
+ const intValue = new Int32Value(255);
+ expect(intValue.value).to.equal(255);
+ });
+
+ it('128-bit decimal', () => {
+ const decimal = new Decimal128Value('-1.2e-3');
+ expect(decimal.value).to.equal('-1.2e-3');
+ });
+
+ it('min key', () => {
+ const value1 = MinKey.instance();
+ const value2 = MinKey.instance();
+ const other = MaxKey.instance();
+ // All MinKeys are equal.
+ expect(value1).to.equal(value2);
+
+ // MinKey and MaxKey are not equal.
+ expect(value1).to.not.equal(other);
+
+ // Two MinKey values are equal.
+ expect(
+ compare(
+ {
+ mapValue: {
+ fields: {
+ [RESERVED_MIN_KEY]: {
+ nullValue: 'NULL_VALUE',
+ },
+ },
+ },
+ },
+ {
+ mapValue: {
+ fields: {
+ [RESERVED_MIN_KEY]: {
+ nullValue: 'NULL_VALUE',
+ },
+ },
+ },
+ }
+ )
+ ).to.equal(0);
+
+ // Null comes before MinKey.
+ expect(
+ compare(
+ {
+ nullValue: null,
+ },
+ {
+ mapValue: {
+ fields: {
+ [RESERVED_MIN_KEY]: {
+ nullValue: 'NULL_VALUE',
+ },
+ },
+ },
+ }
+ )
+ ).to.equal(-1);
+ });
+
+ it('max key', () => {
+ const value1 = MaxKey.instance();
+ const value2 = MaxKey.instance();
+ const other = MinKey.instance();
+ expect(value1).to.equal(value2);
+ expect(value1).to.not.equal(other);
+ });
+
+ it('BSON binary data', () => {
+ const value = new BsonBinaryData(128, Uint8Array.from([7, 8, 9]));
+ expect(value.subtype).to.equal(128);
+ expect(value.data).to.deep.equal(Uint8Array.from([7, 8, 9]));
+ });
+
+ it('BSON binary data can have empty data', () => {
+ const value = BsonBinaryData._fromProto({
+ mapValue: {
+ fields: {
+ [RESERVED_BSON_BINARY_KEY]: {
+ bytesValue: new Uint8Array([128]),
+ },
+ },
+ },
+ });
+ expect(value.subtype).to.equal(128);
+ expect(value.data).to.deep.equal(Uint8Array.from([]));
+ expect(value.isEqual(new BsonBinaryData(128, Uint8Array.from([])))).to.be
+ .true;
+ });
+
+ it('can create BSON timestamp using new', () => {
+ const value1 = new BsonTimestamp(57, 4);
+ const value2 = new BsonTimestamp(57, 4);
+ expect(value1.isEqual(value2)).to.be.true;
+ expect(value2.isEqual(value1)).to.be.true;
+ });
+
+ it('cannot create BSON timestamp with out-of-range values', () => {
+ // Negative seconds
+ let error1: Error | null = null;
+ try {
+ new BsonTimestamp(-1, 1);
+ } catch (e) {
+ error1 = e as Error;
+ }
+ expect(error1).to.not.be.null;
+ expect(error1!.message!).to.equal(
+ "BsonTimestamp 'seconds' must be in the range of a 32-bit unsigned integer."
+ );
+
+ // Larger than 2^32-1 seconds
+ let error2: Error | null = null;
+ try {
+ new BsonTimestamp(4294967296, 1);
+ } catch (e) {
+ error2 = e as Error;
+ }
+ expect(error2).to.not.be.null;
+ expect(error2!.message!).to.equal(
+ "BsonTimestamp 'seconds' must be in the range of a 32-bit unsigned integer."
+ );
+
+ // Negative increment
+ let error3: Error | null = null;
+ try {
+ new BsonTimestamp(1, -1);
+ } catch (e) {
+ error3 = e as Error;
+ }
+ expect(error3).to.not.be.null;
+ expect(error3!.message!).to.equal(
+ "BsonTimestamp 'increment' must be in the range of a 32-bit unsigned integer."
+ );
+
+ // Larger than 2^32-1 increment
+ let error4: Error | null = null;
+ try {
+ new BsonTimestamp(1, 4294967296);
+ } catch (e) {
+ error4 = e as Error;
+ }
+ expect(error4).to.not.be.null;
+ expect(error4!.message!).to.equal(
+ "BsonTimestamp 'increment' must be in the range of a 32-bit unsigned integer."
+ );
+ });
+
+ it('can create BSON object id using new', () => {
+ const bsonObjectId1 = new BsonObjectId('foobar');
+ const bsonObjectId2 = new BsonObjectId('foobar');
+ expect(bsonObjectId1.isEqual(bsonObjectId2)).to.be.true;
+ expect(bsonObjectId2.isEqual(bsonObjectId1)).to.be.true;
+ });
+
+ it('can create regular expression using new', () => {
+ const regex1 = new RegexValue('^foo', 'i');
+ const regex2 = new RegexValue('^foo', 'i');
+ expect(regex1.isEqual(regex2)).to.be.true;
+ expect(regex2.isEqual(regex1)).to.be.true;
+ });
+
+ it('can create 32-bit int using new', () => {
+ const intValue1 = new Int32Value(255);
+ const intValue2 = new Int32Value(255);
+ expect(intValue1.isEqual(intValue2)).to.be.true;
+ expect(intValue2.isEqual(intValue1)).to.be.true;
+ });
+
+ it('can create 128-bit decimal using new', () => {
+ const v1 = new Decimal128Value('1.2e3');
+ const v2 = new Decimal128Value('12e2');
+ const v3 = new Decimal128Value('0.12e4');
+ const v4 = new Decimal128Value('12000e-1');
+ const v5 = new Decimal128Value('1.2');
+ const v6 = new Decimal128Value('NaN');
+ const v7 = new Decimal128Value('NaN');
+ const v8 = new Decimal128Value('Infinity');
+ const v9 = new Decimal128Value('-Infinity');
+ const v10 = new Decimal128Value('-0');
+ const v11 = new Decimal128Value('-0.0');
+ const v12 = new Decimal128Value('0.0');
+ const v13 = new Decimal128Value('0');
+
+ expect(v1.isEqual(v2)).to.be.true;
+ expect(v1.isEqual(v3)).to.be.true;
+ expect(v1.isEqual(v4)).to.be.true;
+ expect(v1.isEqual(v5)).to.be.false;
+ expect(v1.isEqual(v6)).to.be.false;
+ expect(v1.isEqual(v7)).to.be.false;
+ expect(v1.isEqual(v8)).to.be.false;
+ expect(v1.isEqual(v9)).to.be.false;
+
+ expect(v6.isEqual(v7)).to.be.true;
+ expect(v10.isEqual(v11)).to.be.true;
+ expect(v10.isEqual(v12)).to.be.true;
+ expect(v10.isEqual(v13)).to.be.true;
+ });
+
+ it('can create BSON binary data using new', () => {
+ const value1 = new BsonBinaryData(128, Uint8Array.from([7, 8, 9]));
+ const value2 = new BsonBinaryData(128, Uint8Array.from([7, 8, 9]));
+ expect(value1.isEqual(value2)).to.be.true;
+ expect(value2.isEqual(value1)).to.be.true;
+ });
+});
diff --git a/handwritten/firestore/dev/test/index.ts b/handwritten/firestore/dev/test/index.ts
index b0971528683f..696f827a795c 100644
--- a/handwritten/firestore/dev/test/index.ts
+++ b/handwritten/firestore/dev/test/index.ts
@@ -21,7 +21,19 @@ import {GoogleError, GrpcClient, Status} from 'google-gax';
import {google} from '../protos/firestore_v1_proto_api';
import * as Firestore from '../src';
-import {DocumentSnapshot, FieldPath, FieldValue} from '../src';
+import {
+ DocumentSnapshot,
+ FieldPath,
+ FieldValue,
+ BsonBinaryData,
+ BsonObjectId,
+ BsonTimestamp,
+ Decimal128Value,
+ Int32Value,
+ MaxKey,
+ MinKey,
+ RegexValue,
+} from '../src';
import {setTimeoutHandler} from '../src/backoff';
import {QualifiedResourcePath} from '../src/path';
import {
@@ -150,6 +162,104 @@ const allSupportedTypesProtobufJs = document(
},
},
},
+ 'minKeyValue',
+ {
+ mapValue: {
+ fields: {
+ __min__: {
+ nullValue: 'NULL_VALUE',
+ },
+ },
+ },
+ },
+ 'maxKeyValue',
+ {
+ mapValue: {
+ fields: {
+ __max__: {
+ nullValue: 'NULL_VALUE',
+ },
+ },
+ },
+ },
+ 'regexValue',
+ {
+ mapValue: {
+ fields: {
+ __regex__: {
+ mapValue: {
+ fields: {
+ pattern: {
+ stringValue: 'myRegexPattern',
+ },
+ options: {
+ stringValue: 'myRegexOptions',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ 'bsonObjectIdValue',
+ {
+ mapValue: {
+ fields: {
+ __oid__: {
+ stringValue: 'my24CharacterHexString',
+ },
+ },
+ },
+ },
+ 'int32Value',
+ {
+ mapValue: {
+ fields: {
+ __int__: {
+ integerValue: 789,
+ },
+ },
+ },
+ },
+ 'decimal128Value',
+ {
+ mapValue: {
+ fields: {
+ __decimal128__: {
+ stringValue: '1.2e-3',
+ },
+ },
+ },
+ },
+ 'bsonTimestampValue',
+ {
+ mapValue: {
+ fields: {
+ __request_timestamp__: {
+ mapValue: {
+ fields: {
+ seconds: {
+ integerValue: 123,
+ },
+ increment: {
+ integerValue: 456,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ 'bsonBinaryValue',
+ {
+ mapValue: {
+ fields: {
+ __binary__: {
+ bytesValue: Buffer.from([155, 7, 90, 250]),
+ },
+ },
+ },
+ },
'emptyObject',
{
mapValue: {},
@@ -260,6 +370,96 @@ const allSupportedTypesJson = {
},
},
},
+ minKeyValue: {
+ mapValue: {
+ fields: {
+ __min__: {
+ nullValue: 'NULL_VALUE',
+ },
+ },
+ },
+ },
+ maxKeyValue: {
+ mapValue: {
+ fields: {
+ __max__: {
+ nullValue: 'NULL_VALUE',
+ },
+ },
+ },
+ },
+ regexValue: {
+ mapValue: {
+ fields: {
+ __regex__: {
+ mapValue: {
+ fields: {
+ pattern: {
+ stringValue: 'myRegexPattern',
+ },
+ options: {
+ stringValue: 'myRegexOptions',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ bsonObjectIdValue: {
+ mapValue: {
+ fields: {
+ __oid__: {
+ stringValue: 'my24CharacterHexString',
+ },
+ },
+ },
+ },
+ int32Value: {
+ mapValue: {
+ fields: {
+ __int__: {
+ integerValue: 789,
+ },
+ },
+ },
+ },
+ decimal128Value: {
+ mapValue: {
+ fields: {
+ __decimal128__: {
+ stringValue: '1.2e-3',
+ },
+ },
+ },
+ },
+ bsonTimestampValue: {
+ mapValue: {
+ fields: {
+ __request_timestamp__: {
+ mapValue: {
+ fields: {
+ seconds: {
+ integerValue: 123,
+ },
+ increment: {
+ integerValue: 456,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ bsonBinaryValue: {
+ mapValue: {
+ fields: {
+ __binary__: {
+ bytesValue: Buffer.from([155, 7, 90, 250]),
+ },
+ },
+ },
+ },
pathValue: {
referenceValue: `${DATABASE_ROOT}/documents/collection/document`,
},
@@ -289,11 +489,19 @@ const allSupportedTypesInput = {
falseValue: false,
integerValue: 0,
doubleValue: 0.1,
+ int32Value: new Int32Value(789),
+ decimal128Value: new Decimal128Value('1.2e-3'),
infinityValue: Infinity,
negativeInfinityValue: -Infinity,
objectValue: {foo: 'bar'},
emptyObject: {},
vectorValue: FieldValue.vector([0.1, 0.2, 0.3]),
+ minKeyValue: MinKey.instance(),
+ maxKeyValue: MaxKey.instance(),
+ regexValue: new RegexValue('myRegexPattern', 'myRegexOptions'),
+ bsonTimestampValue: new BsonTimestamp(123, 456),
+ bsonBinaryValue: new BsonBinaryData(155, Buffer.from([7, 90, 250])),
+ bsonObjectIdValue: new BsonObjectId('my24CharacterHexString'),
dateValue: new Date('Mar 18, 1985 08:20:00.123 GMT+0100 (CET)'),
timestampValue: Firestore.Timestamp.fromDate(
new Date('Mar 18, 1985 08:20:00.123 GMT+0100 (CET)'),
@@ -323,11 +531,19 @@ const allSupportedTypesOutput: {[field: string]: unknown} = {
falseValue: false,
integerValue: 0,
doubleValue: 0.1,
+ int32Value: new Int32Value(789),
+ decimal128Value: new Decimal128Value('1.2e-3'),
infinityValue: Infinity,
negativeInfinityValue: -Infinity,
objectValue: {foo: 'bar'},
emptyObject: {},
vectorValue: FieldValue.vector([0.1, 0.2, 0.3]),
+ minKeyValue: MinKey.instance(),
+ maxKeyValue: MaxKey.instance(),
+ regexValue: new RegexValue('myRegexPattern', 'myRegexOptions'),
+ bsonTimestampValue: new BsonTimestamp(123, 456),
+ bsonBinaryValue: new BsonBinaryData(155, Buffer.from([7, 90, 250])),
+ bsonObjectIdValue: new BsonObjectId('my24CharacterHexString'),
dateValue: Firestore.Timestamp.fromDate(
new Date('Mar 18, 1985 08:20:00.123 GMT+0100 (CET)'),
),
diff --git a/handwritten/firestore/dev/test/order.ts b/handwritten/firestore/dev/test/order.ts
index 17abcf79fb63..9e52f658bf4b 100644
--- a/handwritten/firestore/dev/test/order.ts
+++ b/handwritten/firestore/dev/test/order.ts
@@ -22,9 +22,17 @@ import {
QueryDocumentSnapshot,
setLogFunction,
Timestamp,
+ GeoPoint,
+ DocumentReference,
+ BsonBinaryData,
+ BsonObjectId,
+ BsonTimestamp,
+ Decimal128Value,
+ Int32Value,
+ MaxKey,
+ MinKey,
+ RegexValue,
} from '../src';
-import {GeoPoint} from '../src';
-import {DocumentReference} from '../src';
import * as order from '../src/order';
import {QualifiedResourcePath} from '../src/path';
import {createInstance, InvalidApiUsage, verifyInstance} from './util/helpers';
@@ -146,7 +154,10 @@ describe('Order', () => {
it('is correct', () => {
const groups = [
// null first
- [wrap(null)],
+ [wrap(null), wrap(null)],
+
+ // MinKey is after null
+ [wrap(MinKey.instance()), wrap(MinKey.instance())],
// booleans
[wrap(false)],
@@ -154,29 +165,89 @@ describe('Order', () => {
// numbers
[double(NaN), double(NaN)],
- [double(-Infinity)],
+ [double(-Infinity), wrap(new Decimal128Value('-Infinity'))],
[double(-Number.MAX_VALUE)],
- [int(Number.MIN_SAFE_INTEGER - 1)],
- [int(Number.MIN_SAFE_INTEGER)],
- [double(-1.1)],
- // Integers and Doubles order the same.
- [int(-1), double(-1.0)],
+ [
+ int(Number.MIN_SAFE_INTEGER - 1),
+ wrap(new Decimal128Value('-9007199254740992')),
+ ],
+ [
+ int(Number.MIN_SAFE_INTEGER),
+ wrap(new Decimal128Value('-9007199254740991')),
+ ],
+ // 64-bit and 32-bit integers order together numerically.
+ [
+ int(-2147483648),
+ wrap(new Int32Value(-2147483648)),
+ wrap(new Decimal128Value('-2147483648')),
+ ],
+ [double(-1.5), wrap(new Decimal128Value('-1.5'))],
+ // Integers and Doubles and int32 order together numerically.
+ [
+ int(-1),
+ double(-1.0),
+ wrap(new Int32Value(-1)),
+ wrap(new Decimal128Value('-1')),
+ wrap(new Decimal128Value('-1.0')),
+ ],
[double(-Number.MIN_VALUE)],
// zeros all compare the same.
- [int(0), double(0.0), double(-0)],
+ [
+ int(0),
+ double(0.0),
+ double(-0),
+ wrap(new Int32Value(0)),
+ wrap(new Decimal128Value('0')),
+ wrap(new Decimal128Value('-0')),
+ ],
[double(Number.MIN_VALUE)],
- [int(1), double(1.0)],
- [double(1.1)],
- [int(2)],
- [int(10)],
- [int(Number.MAX_SAFE_INTEGER)],
- [int(Number.MAX_SAFE_INTEGER + 1)],
- [double(Infinity)],
+ [
+ int(1),
+ double(1.0),
+ wrap(new Int32Value(1)),
+ wrap(new Decimal128Value('1')),
+ wrap(new Decimal128Value('1.0')),
+ ],
+ [double(1.5), wrap(new Decimal128Value('1.5'))],
+ [
+ int(2),
+ wrap(new Decimal128Value('2')),
+ wrap(new Decimal128Value('2.0')),
+ ],
+ [
+ int(10),
+ wrap(new Decimal128Value('10')),
+ wrap(new Decimal128Value('10.0')),
+ ],
+ [
+ wrap(new Int32Value(11)),
+ wrap(new Decimal128Value('11')),
+ wrap(new Decimal128Value('11.0')),
+ ],
+ [wrap(new Int32Value(12)), wrap(new Decimal128Value('12.0'))],
+ [
+ wrap(new Int32Value(2147483647)),
+ wrap(new Decimal128Value('2147483647')),
+ ],
+ [
+ int(Number.MAX_SAFE_INTEGER),
+ wrap(new Decimal128Value('9007199254740991')),
+ ],
+ [
+ int(Number.MAX_SAFE_INTEGER + 1),
+ wrap(new Decimal128Value('9007199254740992')),
+ ],
+ [double(Infinity), wrap(new Decimal128Value('Infinity'))],
// timestamps
[wrap(new Date(2016, 5, 20, 10, 20))],
[wrap(new Date(2016, 10, 21, 15, 32))],
+ // request timestamp
+ [wrap(new BsonTimestamp(123, 4))],
+ [wrap(new BsonTimestamp(123, 5))],
+ [wrap(new BsonTimestamp(124, 0))],
+
// strings
[wrap('')],
[wrap('\u0000\ud7ff\ue000\uffff')],
@@ -196,6 +267,13 @@ describe('Order', () => {
[blob([0, 1, 2, 4, 3])],
[blob([255])],
+ [
+ wrap(new BsonBinaryData(5, Buffer.from([1, 2, 3]))),
+ wrap(new BsonBinaryData(5, new Uint8Array([1, 2, 3]))),
+ ],
+ [wrap(new BsonBinaryData(7, Buffer.from([1])))],
+ [wrap(new BsonBinaryData(7, Buffer.from([2])))],
+
// resource names
[resource('projects/p1/databases/d1/documents/c1/doc1')],
[resource('projects/p1/databases/d1/documents/c1/doc2')],
@@ -207,6 +285,12 @@ describe('Order', () => {
[resource('projects/p2/databases/d2/documents/c1-/doc1')],
[resource('projects/p2/databases/d3/documents/c1-/doc1')],
+ // ObjectId
+ [wrap(new BsonObjectId('foo')), wrap(new BsonObjectId('foo'))],
+ [wrap(new BsonObjectId('foo\u0301'))], // with combining acute accent
+ [wrap(new BsonObjectId('xyz'))],
+ [wrap(new BsonObjectId('Ḟoo'))], // with latin capital letter f with dot above
+
// geo points
[geopoint(-90, -180)],
[geopoint(-90, 0)],
@@ -221,6 +305,12 @@ describe('Order', () => {
[geopoint(90, 0)],
[geopoint(90, 180)],
+ // regular expressions
+ [wrap(new RegexValue('a', 'bar1'))],
+ [wrap(new RegexValue('foo', 'bar1'))],
+ [wrap(new RegexValue('foo', 'bar2'))],
+ [wrap(new RegexValue('go', 'bar1'))],
+
// arrays
[wrap([])],
[wrap(['bar'])],
@@ -235,6 +325,9 @@ describe('Order', () => {
[wrap({foo: 1})],
[wrap({foo: 2})],
[wrap({foo: '0'})],
+
+ // MaxKey
+ [wrap(MaxKey.instance()), wrap(MaxKey.instance())],
];
for (let i = 0; i < groups.length; i++) {
diff --git a/handwritten/firestore/types/firestore.d.ts b/handwritten/firestore/types/firestore.d.ts
index dc108842f3df..b6baa008e2fa 100644
--- a/handwritten/firestore/types/firestore.d.ts
+++ b/handwritten/firestore/types/firestore.d.ts
@@ -2707,6 +2707,146 @@ declare namespace FirebaseFirestore {
*/
isEqual(other: VectorValue): boolean;
}
+
+ /** Represent a "Min Key" type in Firestore documents. */
+ export class MinKey {
+ private constructor();
+
+ /** A type string to uniquely identify instances of this class. */
+ readonly type = 'MinKey';
+
+ /**
+ * @return The singleton `MinKey` instance.
+ */
+ static instance(): MinKey;
+ }
+
+ /** Represent a "Max Key" type in Firestore documents. */
+ export class MaxKey {
+ private constructor();
+
+ /** A type string to uniquely identify instances of this class. */
+ readonly type = 'MaxKey';
+
+ /**
+ * @return The singleton `MaxKey` instance.
+ */
+ static instance(): MaxKey;
+ }
+
+ /** Represents a regular expression type in Firestore documents. */
+ export class RegexValue {
+ constructor(pattern: string, options: string);
+
+ /** The regular expression pattern */
+ readonly pattern: string;
+
+ /** The regular expression options */
+ readonly options: string;
+
+ /**
+ * Returns true if this `RegexValue` is equal to the provided one.
+ *
+ * @param other The `RegexValue` to compare against.
+ * @return 'true' if this `RegexValue` is equal to the provided one.
+ */
+ isEqual(other: RegexValue): boolean;
+ }
+
+ /** Represents an ObjectId type in Firestore documents. */
+ export class BsonObjectId {
+ constructor(value: string);
+
+ /** The 24-character hex string representation of the ObjectId. */
+ readonly value: string;
+
+ /**
+ * Returns true if this `BsonObjectId` is equal to the provided one.
+ *
+ * @param other The `BsonObjectId` to compare against.
+ * @return 'true' if this `BsonObjectId` is equal to the provided one.
+ */
+ isEqual(other: BsonObjectId): boolean;
+ }
+
+ /** Represents a 32-bit integer type in Firestore documents. */
+ export class Int32Value {
+ /**
+ * Note: values larger than the largest 32-bit signed integer,
+ * or smaller than the smallest 32-bit signed integer are invalid
+ * and will get rejected.
+ */
+ constructor(value: number);
+
+ /** The underlying 32-bit number */
+ readonly value: number;
+
+ /**
+ * Returns true if this `Int32Value` is equal to the provided one.
+ *
+ * @param other The `Int32Value` to compare against.
+ * @return 'true' if this `Int32Value` is equal to the provided one.
+ */
+ isEqual(other: Int32Value): boolean;
+ }
+
+ /** Represents a 128-bit decimal type in Firestore documents. */
+ export class Decimal128Value {
+ constructor(value: string);
+
+ /** The underlying 128-bit decimal number */
+ readonly value: string;
+
+ /**
+ * Returns true if this `Decimal128Value` is equal to the provided one.
+ *
+ * @param other The `Decimal128Value` to compare against.
+ * @return 'true' if this `Decimal128Value` is equal to the provided one.
+ */
+ isEqual(other: Decimal128Value): boolean;
+ }
+
+ /** Represents a BSON Timestamp type in Firestore documents. */
+ export class BsonTimestamp {
+ /**
+ * Note: negative values and values larger than the largest 32-bit
+ * unsigned integer are invalid and will get rejected.
+ */
+ constructor(seconds: number, increment: number);
+
+ /** The underlying unsigned 32-bit integer for seconds */
+ readonly seconds: number;
+
+ /** The underlying unsigned 32-bit integer for increment */
+ readonly increment: number;
+
+ /**
+ * Returns true if this `BsonTimestamp` is equal to the provided one.
+ *
+ * @param other The `BsonTimestamp` to compare against.
+ * @return 'true' if this `BsonTimestamp` is equal to the provided one.
+ */
+ isEqual(other: BsonTimestamp): boolean;
+ }
+
+ /** Represents a BSON Binary Data type in Firestore documents. */
+ export class BsonBinaryData {
+ constructor(subtype: number, data: Uint8Array);
+
+ /** The subtype for the data */
+ readonly subtype: number;
+
+ /** The binary data as a byte array */
+ readonly data: Uint8Array;
+
+ /**
+ * Returns true if this `BsonBinaryData` is equal to the provided one.
+ *
+ * @param other The `BsonBinaryData` to compare against.
+ * @return 'true' if this `BsonBinaryData` is equal to the provided one.
+ */
+ isEqual(other: BsonBinaryData): boolean;
+ }
/**
* Sentinel values that can be used when writing document fields with set(),
* create() or update().