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: + * + * + */ + 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().