-
Notifications
You must be signed in to change notification settings - Fork 938
Don't deserialize full Document proto for Query execution #2115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8a276f2
f318dcd
91c228d
549aa06
69e402b
27cb4fe
1ded5b1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,13 +16,14 @@ | |
*/ | ||
|
||
import { SnapshotVersion } from '../core/snapshot_version'; | ||
import { fail } from '../util/assert'; | ||
import { assert, fail } from '../util/assert'; | ||
|
||
import { DocumentKey } from './document_key'; | ||
import { FieldValue, JsonObject, ObjectValue } from './field_value'; | ||
import { FieldPath } from './path'; | ||
|
||
import * as api from '../protos/firestore_proto_api'; | ||
import * as obj from '../util/obj'; | ||
|
||
export interface DocumentOptions { | ||
hasLocalMutations?: boolean; | ||
|
@@ -59,43 +60,91 @@ export class Document extends MaybeDocument { | |
readonly hasLocalMutations: boolean; | ||
readonly hasCommittedMutations: boolean; | ||
|
||
/** | ||
* A cache of canonicalized FieldPaths to FieldValues that have already been | ||
* deserialized in `getField()`. | ||
*/ | ||
private fieldValueCache?: Map<string, FieldValue | null>; | ||
|
||
constructor( | ||
key: DocumentKey, | ||
version: SnapshotVersion, | ||
readonly data: ObjectValue, | ||
options: DocumentOptions, | ||
/** | ||
* Memoized serialized form of the document for optimization purposes (avoids repeated | ||
* serialization). Might be undefined. | ||
*/ | ||
readonly proto?: api.Document | ||
private objectValue?: ObjectValue, | ||
readonly proto?: api.Document, | ||
private readonly converter?: (value: api.Value) => FieldValue | ||
) { | ||
super(key, version); | ||
assert( | ||
this.objectValue !== undefined || | ||
(this.proto !== undefined && this.converter !== undefined), | ||
'If objectValue is not defined, proto and converter need to be set.' | ||
); | ||
|
||
this.hasLocalMutations = !!options.hasLocalMutations; | ||
this.hasCommittedMutations = !!options.hasCommittedMutations; | ||
} | ||
|
||
field(path: FieldPath): FieldValue | null { | ||
return this.data.field(path); | ||
if (this.objectValue) { | ||
return this.objectValue.field(path); | ||
} else { | ||
if (!this.fieldValueCache) { | ||
// TODO(b/136090445): Remove the cache when `getField` is no longer | ||
// called during Query ordering. | ||
this.fieldValueCache = new Map<string, FieldValue>(); | ||
} | ||
|
||
const canonicalPath = path.canonicalString(); | ||
|
||
let fieldValue = this.fieldValueCache.get(canonicalPath); | ||
|
||
if (fieldValue === undefined) { | ||
// Instead of deserializing the full Document proto, we only | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function is pretty long now. Perhaps split this part into a helper function? if (fieldValue === undefined) {
return parseFieldValue(path, canonicalPath);
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I moved the code that extract the Proto value from the protobuf into a separate function, which also helps with the control flow of the loop. |
||
// deserialize the value at the requested field path. This speeds up | ||
// Query execution as query filters can discard documents based on a | ||
// single field. | ||
const protoValue = this.getProtoField(path); | ||
if (protoValue === undefined) { | ||
fieldValue = null; | ||
} else { | ||
fieldValue = this.converter!(protoValue); | ||
} | ||
this.fieldValueCache.set(canonicalPath, fieldValue); | ||
} | ||
|
||
return fieldValue!; | ||
} | ||
} | ||
|
||
fieldValue(path: FieldPath): unknown { | ||
const field = this.field(path); | ||
return field ? field.value() : undefined; | ||
data(): ObjectValue { | ||
if (!this.objectValue) { | ||
let result = ObjectValue.EMPTY; | ||
obj.forEach(this.proto!.fields, (key: string, value: api.Value) => { | ||
result = result.set(new FieldPath([key]), this.converter!(value)); | ||
}); | ||
this.objectValue = result; | ||
|
||
// Once objectValue is computed, values inside the fieldValueCache are no | ||
// longer accessed. | ||
this.fieldValueCache = undefined; | ||
} | ||
|
||
return this.objectValue; | ||
} | ||
|
||
value(): JsonObject<unknown> { | ||
return this.data.value(); | ||
return this.data().value(); | ||
} | ||
|
||
isEqual(other: MaybeDocument | null | undefined): boolean { | ||
return ( | ||
other instanceof Document && | ||
this.key.isEqual(other.key) && | ||
this.version.isEqual(other.version) && | ||
this.data.isEqual(other.data) && | ||
this.hasLocalMutations === other.hasLocalMutations && | ||
this.hasCommittedMutations === other.hasCommittedMutations | ||
this.hasCommittedMutations === other.hasCommittedMutations && | ||
this.data().isEqual(other.data()) | ||
); | ||
} | ||
|
||
|
@@ -111,6 +160,29 @@ export class Document extends MaybeDocument { | |
return this.hasLocalMutations || this.hasCommittedMutations; | ||
} | ||
|
||
/** | ||
* Returns the nested Protobuf value for 'path`. Can only be called if | ||
* `proto` was provided at construction time. | ||
*/ | ||
private getProtoField(path: FieldPath): api.Value | undefined { | ||
assert( | ||
this.proto !== undefined, | ||
'Can only call getProtoField() when proto is defined' | ||
); | ||
|
||
let protoValue: api.Value | undefined = this.proto!.fields[ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: add an empty line after There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
path.firstSegment() | ||
]; | ||
for (let i = 1; i < path.length; ++i) { | ||
if (!protoValue || !protoValue.mapValue) { | ||
return undefined; | ||
} | ||
protoValue = protoValue.mapValue.fields[path.get(i)]; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: add an empty line right after the loop? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
|
||
return protoValue; | ||
} | ||
|
||
static compareByField(field: FieldPath, d1: Document, d2: Document): number { | ||
const v1 = d1.field(field); | ||
const v2 = d2.field(field); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it ever valid to pass both the
objectValue
and theproto
/converter
pair? If not, would it be worthwhile to assert that either onlyobjectValue
or else bothproto
andconverter
are given? (perhaps that could supersede the assertions insidefield
anddata
)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It theoretically allows you to memoize both the proto and the objectValue, which helps when we write an object back to persistence. I changed the asserts in this PR to assert in the constructor that
proto
andconverter
are set ifobjectValue
is not defined.