private Cursor CreateCursorFromSnapshot(DocumentSnapshot snapshot, bool before, out IReadOnlyList <InternalOrdering> newOrderings) { GaxPreconditions.CheckArgument(Equals(snapshot.Reference.Parent, Collection), nameof(snapshot), "Snapshot was from incorrect collection"); GaxPreconditions.CheckNotNull(snapshot, nameof(snapshot)); var cursor = new Cursor { Before = before }; bool hasDocumentId = false; // We may or may not need to add some orderings; this is communicated through the out parameter. newOrderings = _orderings; // Only used when we need to add orderings; set newOrderings to this at the same time. List <InternalOrdering> modifiedOrderings = null; if (_orderings.Count == 0 && _filters != null) { // If no explicit ordering is specified, use the first inequality to define an implicit order. foreach (var filter in _filters) { if (!filter.IsEqualityFilter()) { modifiedOrderings = new List <InternalOrdering>(newOrderings) { new InternalOrdering(filter.Field, Direction.Ascending) }; newOrderings = modifiedOrderings; } } } else { hasDocumentId = _orderings.Any(order => Equals(order.Field, FieldPath.DocumentId)); } if (!hasDocumentId) { // Add implicit sorting by name, using the last specified direction. Direction lastDirection = _orderings.Count == 0 ? Direction.Ascending : _orderings.Last().Direction; // Clone iff this is the first new ordering. if (modifiedOrderings == null) { modifiedOrderings = new List <InternalOrdering>(newOrderings); newOrderings = modifiedOrderings; } modifiedOrderings.Add(new InternalOrdering(FieldPath.DocumentId, lastDirection)); } foreach (var ordering in newOrderings) { var field = ordering.Field; var value = Equals(field, FieldPath.DocumentId) ? ValueSerializer.Serialize(snapshot.Reference) : snapshot.ExtractValue(field); if (value == null) { throw new ArgumentException($"Snapshot does not contain field {field}", nameof(snapshot)); } cursor.Values.Add(ValueSerializer.Serialize(value)); } return(cursor); }
/// <summary> /// Adds an operation that sets data in a document, either replacing it completely or merging fields. /// </summary> /// <param name="documentReference">A document reference indicating the path of the document to update. Must not be null.</param> /// <param name="documentData">The data to store in the document. Must not be null.</param> /// <param name="options">The options to use when setting data in the document. May be null, which is equivalent to <see cref="SetOptions.Overwrite"/>.</param> /// <returns>This batch, for the purposes of method chaining.</returns> public WriteBatch Set(DocumentReference documentReference, object documentData, SetOptions options = null) { GaxPreconditions.CheckNotNull(documentReference, nameof(documentReference)); GaxPreconditions.CheckNotNull(documentData, nameof(documentData)); var fields = ValueSerializer.SerializeMap(documentReference.Database.SerializationContext, documentData); options = options ?? SetOptions.Overwrite; var sentinels = FindSentinels(fields); var deletes = sentinels.Where(sf => sf.IsDelete).ToList(); var nonDeletes = sentinels.Where(sf => !sf.IsDelete).ToList(); IDictionary <FieldPath, Value> updates; IReadOnlyList <FieldPath> updatePaths; if (options.Merge) { var mask = options.FieldMask; if (mask.Count == 0) { // Merge all: // - If the data is empty, we force a write // - Deletes are allowed anywhere // - All timestamps converted to transforms // - Each top-level entry becomes a FieldPath RemoveSentinels(fields, nonDeletes); // Work out the update paths after removing server timestamps but before removing deletes, // so that we correctly perform the deletes. updatePaths = ExtractDocumentMask(fields); RemoveSentinels(fields, deletes); updates = fields.ToDictionary(pair => new FieldPath(pair.Key), pair => pair.Value); } else { // Merge specific: // - Deletes must be in the mask // - Only timestamps in the mask are converted to transforms // - Apply the field mask to get the updates GaxPreconditions.CheckArgument(deletes.All(sf => mask.Contains(sf.FieldPath)), nameof(documentData), "Delete cannot appear in an unmerged field"); nonDeletes = nonDeletes.Where(sf => mask.Any(fp => fp.IsPrefixOf(sf.FieldPath))).ToList(); RemoveSentinels(fields, deletes); RemoveSentinels(fields, nonDeletes); updates = ApplyFieldMask(fields, mask); // Every field path in the mask must either refer to a now-removed sentinel, or a remaining value. // Sentinels are permitted to be in the mask in a nested fashion rather than directly, e.g. a mask of "parent" with a sentinel of "parent.child.timestamp" is fine. GaxPreconditions.CheckArgument( mask.All(p => updates.ContainsKey(p) || deletes.Any(sf => p.IsPrefixOf(sf.FieldPath)) || nonDeletes.Any(sf => p.IsPrefixOf(sf.FieldPath))), nameof(documentData), "All paths specified for merging must appear in the data."); updatePaths = mask; } } else { // Overwrite: // - No deletes allowed // - All timestamps converted to transforms // - Each top-level entry becomes a FieldPath GaxPreconditions.CheckArgument(deletes.Count == 0, nameof(documentData), "Delete cannot appear in document data when overwriting"); RemoveSentinels(fields, nonDeletes); updates = fields.ToDictionary(pair => new FieldPath(pair.Key), pair => pair.Value); updatePaths = null; } AddUpdateWrite(documentReference, ExpandObject(updates), updatePaths, precondition: null, sentinelFields: nonDeletes); return(this); }