From 3576df8923fd1cd71b3a1fac868df43c39616af4 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Mon, 20 Dec 2021 10:59:16 -0700 Subject: [PATCH] Port IndexValueWriter --- .../index/directional_index_byte_encoder.ts | 28 +++ .../src/index/firestore_index_value_writer.ts | 197 ++++++++++++++++++ .../firestore/src/index/index_byte_encoder.ts | 83 ++++++++ .../src/index/ordered_code_writer.ts | 64 ++++++ packages/firestore/src/model/values.ts | 21 ++ 5 files changed, 393 insertions(+) create mode 100644 packages/firestore/src/index/directional_index_byte_encoder.ts create mode 100644 packages/firestore/src/index/firestore_index_value_writer.ts create mode 100644 packages/firestore/src/index/index_byte_encoder.ts create mode 100644 packages/firestore/src/index/ordered_code_writer.ts diff --git a/packages/firestore/src/index/directional_index_byte_encoder.ts b/packages/firestore/src/index/directional_index_byte_encoder.ts new file mode 100644 index 00000000000..2e047193d3a --- /dev/null +++ b/packages/firestore/src/index/directional_index_byte_encoder.ts @@ -0,0 +1,28 @@ +/** + * @license + * Copyright 2021 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 { ByteString } from '../util/byte_string'; + +/** An index value encoder. */ +export interface DirectionalIndexByteEncoder { + // Note: This code is copied from the backend. Code that is not used by + // Firestore was removed. + writeBytes(value: ByteString): void; + writeString(value: string): void; + writeNumber(value: number): void; + writeInfinity(): void; +} diff --git a/packages/firestore/src/index/firestore_index_value_writer.ts b/packages/firestore/src/index/firestore_index_value_writer.ts new file mode 100644 index 00000000000..9b5e205558b --- /dev/null +++ b/packages/firestore/src/index/firestore_index_value_writer.ts @@ -0,0 +1,197 @@ +/** + * @license + * Copyright 2021 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 { DocumentKey } from '../model/document_key'; +import { normalizeByteString, normalizeNumber } from '../model/normalize'; +import { isMaxValue } from '../model/values'; +import { ArrayValue, MapValue, Value } from '../protos/firestore_proto_api'; +import { fail } from '../util/assert'; +import { isNegativeZero } from '../util/types'; + +import { DirectionalIndexByteEncoder } from './directional_index_byte_encoder'; + +// Note: This code is copied from the backend. Code that is not used by +// Firestore was removed. + +const INDEX_TYPE_NULL = 5; +const INDEX_TYPE_BOOLEAN = 10; +const INDEX_TYPE_NAN = 13; +const INDEX_TYPE_NUMBER = 15; +const INDEX_TYPE_TIMESTAMP = 20; +const INDEX_TYPE_STRING = 25; +const INDEX_TYPE_BLOB = 30; +const INDEX_TYPE_REFERENCE = 37; +const INDEX_TYPE_GEOPOINT = 45; +const INDEX_TYPE_ARRAY = 50; +const INDEX_TYPE_MAP = 55; +const INDEX_TYPE_REFERENCE_SEGMENT = 60; + +// A terminator that indicates that a truncable value was not truncated. +// This must be smaller than all other type labels. +const NOT_TRUNCATED = 2; + +/** Firestore index value writer. */ +export class FirestoreIndexValueWriter { + static INSTANCE = new FirestoreIndexValueWriter(); + + private constructor() {} + + // The write methods below short-circuit writing terminators for values + // containing a (terminating) truncated value. + // + // As an example, consider the resulting encoding for: + // + // ["bar", [2, "foo"]] -> (STRING, "bar", TERM, ARRAY, NUMBER, 2, STRING, "foo", TERM, TERM, TERM) + // ["bar", [2, truncated("foo")]] -> (STRING, "bar", TERM, ARRAY, NUMBER, 2, STRING, "foo", TRUNC) + // ["bar", truncated(["foo"])] -> (STRING, "bar", TERM, ARRAY. STRING, "foo", TERM, TRUNC) + + /** Writes an index value. */ + writeIndexValue(value: Value, encoder: DirectionalIndexByteEncoder): void { + this.writeIndexValueAux(value, encoder); + // Write separator to split index values + // (see go/firestore-storage-format#encodings). + encoder.writeInfinity(); + } + + private writeIndexValueAux( + indexValue: Value, + encoder: DirectionalIndexByteEncoder + ): void { + if ('nullValue' in indexValue) { + this.writeValueTypeLabel(encoder, INDEX_TYPE_NULL); + } else if ('booleanValue' in indexValue) { + this.writeValueTypeLabel(encoder, INDEX_TYPE_BOOLEAN); + encoder.writeNumber(indexValue.booleanValue ? 1 : 0); + } else if ('integerValue' in indexValue) { + this.writeValueTypeLabel(encoder, INDEX_TYPE_NUMBER); + encoder.writeNumber(normalizeNumber(indexValue.integerValue)); + } else if ('doubleValue' in indexValue) { + const n = normalizeNumber(indexValue.doubleValue); + if (isNaN(n)) { + this.writeValueTypeLabel(encoder, INDEX_TYPE_NAN); + } else { + this.writeValueTypeLabel(encoder, INDEX_TYPE_NUMBER); + if (isNegativeZero(n)) { + // -0.0, 0 and 0.0 are all considered the same + encoder.writeNumber(0.0); + } else { + encoder.writeNumber(n); + } + } + } else if ('timestampValue' in indexValue) { + const timestamp = indexValue.timestampValue!; + this.writeValueTypeLabel(encoder, INDEX_TYPE_TIMESTAMP); + if (typeof timestamp === 'string') { + encoder.writeString(timestamp); + } else { + encoder.writeString(`${timestamp.seconds || ''}`); + encoder.writeNumber(timestamp.nanos || 0); + } + } else if ('stringValue' in indexValue) { + this.writeIndexString(indexValue.stringValue!, encoder); + this.writeTruncationMarker(encoder); + } else if ('bytesValue' in indexValue) { + this.writeValueTypeLabel(encoder, INDEX_TYPE_BLOB); + encoder.writeBytes(normalizeByteString(indexValue.bytesValue!)); + this.writeTruncationMarker(encoder); + } else if ('referenceValue' in indexValue) { + this.writeIndexEntityRef(indexValue.referenceValue!, encoder); + } else if ('geoPointValue' in indexValue) { + const geoPoint = indexValue.geoPointValue!; + this.writeValueTypeLabel(encoder, INDEX_TYPE_GEOPOINT); + encoder.writeNumber(geoPoint.latitude || 0); + encoder.writeNumber(geoPoint.longitude || 0); + } else if ('mapValue' in indexValue) { + if (isMaxValue(indexValue)) { + this.writeValueTypeLabel(encoder, Number.MAX_SAFE_INTEGER); + } else { + this.writeIndexMap(indexValue.mapValue!, encoder); + this.writeTruncationMarker(encoder); + } + } else if ('arrayValue' in indexValue) { + this.writeIndexArray(indexValue.arrayValue!, encoder); + this.writeTruncationMarker(encoder); + } else { + fail('unknown index value type ' + indexValue); + } + } + + private writeIndexString( + stringIndexValue: string, + encoder: DirectionalIndexByteEncoder + ): void { + this.writeValueTypeLabel(encoder, INDEX_TYPE_STRING); + this.writeUnlabeledIndexString(stringIndexValue, encoder); + } + + private writeUnlabeledIndexString( + stringIndexValue: string, + encoder: DirectionalIndexByteEncoder + ): void { + encoder.writeString(stringIndexValue); + } + + private writeIndexMap( + mapIndexValue: MapValue, + encoder: DirectionalIndexByteEncoder + ): void { + const map = mapIndexValue.fields || {}; + this.writeValueTypeLabel(encoder, INDEX_TYPE_MAP); + for (const key of Object.keys(map)) { + this.writeIndexString(key, encoder); + this.writeIndexValueAux(map[key], encoder); + } + } + + private writeIndexArray( + arrayIndexValue: ArrayValue, + encoder: DirectionalIndexByteEncoder + ): void { + const values = arrayIndexValue.values || []; + this.writeValueTypeLabel(encoder, INDEX_TYPE_ARRAY); + for (const element of values) { + this.writeIndexValueAux(element, encoder); + } + } + + private writeIndexEntityRef( + referenceValue: string, + encoder: DirectionalIndexByteEncoder + ): void { + this.writeValueTypeLabel(encoder, INDEX_TYPE_REFERENCE); + const path = DocumentKey.fromName(referenceValue).path; + for (let i = 0; i < path.length; ++i) { + const segment = path.get(i); + this.writeValueTypeLabel(encoder, INDEX_TYPE_REFERENCE_SEGMENT); + this.writeUnlabeledIndexString(segment, encoder); + } + } + + private writeValueTypeLabel( + encoder: DirectionalIndexByteEncoder, + typeOrder: number + ): void { + encoder.writeNumber(typeOrder); + } + + private writeTruncationMarker(encoder: DirectionalIndexByteEncoder): void { + // While the SDK does not implement truncation, the truncation marker is + // used to terminate all variable length values (which are strings, bytes, + // references, arrays and maps). + encoder.writeNumber(NOT_TRUNCATED); + } +} diff --git a/packages/firestore/src/index/index_byte_encoder.ts b/packages/firestore/src/index/index_byte_encoder.ts new file mode 100644 index 00000000000..727920e8571 --- /dev/null +++ b/packages/firestore/src/index/index_byte_encoder.ts @@ -0,0 +1,83 @@ +/** + * @license + * Copyright 2021 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 { ByteString } from '../util/byte_string'; + +import { DirectionalIndexByteEncoder } from './directional_index_byte_encoder'; +import { OrderedCodeWriter } from './ordered_code_writer'; + +class AscendingIndexByteEncoder implements DirectionalIndexByteEncoder { + constructor(private orderedCode: OrderedCodeWriter) {} + writeBytes(value: ByteString): void { + this.orderedCode.writeBytesAscending(value); + } + + writeString(value: string): void { + this.orderedCode.writeUtf8Ascending(value); + } + + writeNumber(value: number): void { + this.orderedCode.writeNumberAscending(value); + } + + writeInfinity(): void { + this.orderedCode.writeInfinityAscending(); + } +} + +class DescendingIndexByteEncoder implements DirectionalIndexByteEncoder { + constructor(private orderedCode: OrderedCodeWriter) {} + writeBytes(value: ByteString): void { + this.orderedCode.writeBytesDescending(value); + } + + writeString(value: string): void { + this.orderedCode.writeUtf8Descending(value); + } + + writeNumber(value: number): void { + this.orderedCode.writeNumberDescending(value); + } + + writeInfinity(): void { + this.orderedCode.writeInfinityDescending(); + } +} +/** + * Implements `DirectionalIndexByteEncoder` using `OrderedCodeWriter` for the + * actual encoding. + */ +export class IndexByteEncoder { + private orderedCode = new OrderedCodeWriter(); + private ascending = new AscendingIndexByteEncoder(this.orderedCode); + private descending = new DescendingIndexByteEncoder(this.orderedCode); + + seed(encodedBytes: Uint8Array): void { + this.orderedCode.seed(encodedBytes); + } + + forKind(kind: 'asc' | 'desc'): DirectionalIndexByteEncoder { + return kind === 'asc' ? this.ascending : this.descending; + } + + encodedBytes(): Uint8Array { + return this.orderedCode.encodedBytes(); + } + + reset(): void { + this.orderedCode.reset(); + } +} diff --git a/packages/firestore/src/index/ordered_code_writer.ts b/packages/firestore/src/index/ordered_code_writer.ts new file mode 100644 index 00000000000..9f993ab9eb4 --- /dev/null +++ b/packages/firestore/src/index/ordered_code_writer.ts @@ -0,0 +1,64 @@ +/** + * @license + * Copyright 2021 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 | agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES | CONDITIONS OF ANY KIND, either express | implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { fail } from '../util/assert'; +import { ByteString } from '../util/byte_string'; + +export class OrderedCodeWriter { + writeBytesAscending(value: ByteString): void { + fail('Not implemented'); + } + + writeBytesDescending(value: ByteString): void { + fail('Not implemented'); + } + + writeUtf8Ascending(sequence: string): void { + fail('Not implemented'); + } + + writeUtf8Descending(sequence: string): void { + fail('Not implemented'); + } + + writeNumberAscending(val: number): void { + fail('Not implemented'); + } + + writeNumberDescending(val: number): void { + fail('Not implemented'); + } + + writeInfinityAscending(): void { + fail('Not implemented'); + } + + writeInfinityDescending(): void { + fail('Not implemented'); + } + + reset(): void { + fail('Not implemented'); + } + + encodedBytes(): Uint8Array { + fail('Not implemented'); + } + + seed(encodedBytes: Uint8Array): void { + fail('Not implemented'); + } +} diff --git a/packages/firestore/src/model/values.ts b/packages/firestore/src/model/values.ts index 854655010fc..2d88577acb9 100644 --- a/packages/firestore/src/model/values.ts +++ b/packages/firestore/src/model/values.ts @@ -41,6 +41,14 @@ import { } from './server_timestamps'; import { TypeOrder } from './type_order'; +export const MAX_VALUE: Value = { + mapValue: { + fields: { + '__type__': { stringValue: '__max___' } + } + } +}; + /** Extracts the backend's type order for the provided value. */ export function typeOrder(value: Value): TypeOrder { if ('nullValue' in value) { @@ -73,6 +81,10 @@ export function typeOrder(value: Value): TypeOrder { /** Tests `left` and `right` for equality based on the backend semantics. */ export function valueEquals(left: Value, right: Value): boolean { + if (left === right) { + return true; + } + const leftType = typeOrder(left); const rightType = typeOrder(right); if (leftType !== rightType) { @@ -195,6 +207,10 @@ export function arrayValueContains( } export function valueCompare(left: Value, right: Value): number { + if (left === right) { + return 0; + } + const leftType = typeOrder(left); const rightType = typeOrder(right); @@ -581,3 +597,8 @@ export function deepClone(source: Value): Value { return { ...source }; } } + +/** Returns true if the Value represents the canonical {@link #MAX_VALUE} . */ +export function isMaxValue(value: Value): boolean { + return valueEquals(value, MAX_VALUE); +}