Skip to content

Commit

Permalink
Port IndexValueWriter (#5826)
Browse files Browse the repository at this point in the history
  • Loading branch information
schmidt-sebastian committed Dec 23, 2021
1 parent 4c042b2 commit 3d8109c
Show file tree
Hide file tree
Showing 5 changed files with 392 additions and 0 deletions.
28 changes: 28 additions & 0 deletions 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;
}
196 changes: 196 additions & 0 deletions packages/firestore/src/index/firestore_index_value_writer.ts
@@ -0,0 +1,196 @@
/**
* @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 truncatable 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;
path.forEach(segment => {
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);
}
}
83 changes: 83 additions & 0 deletions 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();
}
}
64 changes: 64 additions & 0 deletions 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');
}
}

0 comments on commit 3d8109c

Please sign in to comment.