/// <summary> /// Adds a write operation which will create the specified document with a precondition /// that it doesn't exist already. /// </summary> /// <param name="documentReference">A document reference indicating the path of the document to create. Must not be null.</param> /// <param name="documentData">The data for the document. Must not be null.</param> /// <returns>This batch, for the purpose of method chaining</returns> public WriteBatch Create(DocumentReference documentReference, object documentData) { GaxPreconditions.CheckNotNull(documentReference, nameof(documentReference)); GaxPreconditions.CheckNotNull(documentData, nameof(documentData)); var fields = ValueSerializer.SerializeMap(documentReference.Database.SerializationContext, documentData); var sentinels = FindSentinels(fields); GaxPreconditions.CheckArgument(!sentinels.Any(sf => sf.IsDelete), nameof(documentData), "Delete sentinels cannot appear in Create calls"); RemoveSentinels(fields, sentinels); AddUpdateWrite(documentReference, fields, updatePaths: null, Precondition.MustNotExist, sentinels); return(this); }
/// <summary> /// Adds a write operation which will create the specified document with a precondition /// that it doesn't exist already. /// </summary> /// <param name="documentReference">A document reference indicating the path of the document to create. Must not be null.</param> /// <param name="documentData">The data for the document. Must not be null.</param> /// <returns>This batch, for the purpose of method chaining</returns> public WriteBatch Create(DocumentReference documentReference, object documentData) { GaxPreconditions.CheckNotNull(documentReference, nameof(documentReference)); GaxPreconditions.CheckNotNull(documentData, nameof(documentData)); var fields = ValueSerializer.SerializeMap(documentData); var sentinels = FindSentinels(fields); GaxPreconditions.CheckArgument(!sentinels.Any(sf => sf.IsDelete), nameof(documentData), "Delete sentinels cannot appear in Create calls"); RemoveSentinels(fields, sentinels); // Force a write if we've not got any sentinel values. Otherwise, we end up with an empty transform instead, // just to specify the precondition. AddUpdateWrites(documentReference, fields, s_emptyFieldPathList, Precondition.MustNotExist, sentinels, sentinels.Count == 0, false); return(this); }
/// <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(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(); bool forceWrite = false; 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 forceWrite = fields.Count == 0; 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 = s_emptyFieldPathList; forceWrite = true; } AddUpdateWrites(documentReference, ExpandObject(updates), updatePaths, null, nonDeletes, forceWrite, options.Merge); return(this); }
/// <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(documentData); options = options ?? SetOptions.Overwrite; var serverTimestamps = new List <FieldPath>(); var deletes = new List <FieldPath>(); FindSentinels(fields, FieldPath.Empty, serverTimestamps, deletes); bool forceWrite = false; IDictionary <FieldPath, Value> updates; IReadOnlyList <FieldPath> updatePaths; if (options.Merge) { var mask = options.FieldMask; if (mask.Count == 0) { // Merge all: // - Empty data is not allowed // - Deletes are allowed anywhere // - All timestamps converted to transforms // - Each top-level entry becomes a FieldPath GaxPreconditions.CheckArgument(fields.Count != 0, nameof(documentData), "{0} cannot be specified with empty data", nameof(SetOptions.MergeAll)); RemoveSentinels(fields, serverTimestamps); // 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(p => mask.Contains(p)), nameof(documentData), "Delete cannot appear in an unmerged field"); serverTimestamps = serverTimestamps.Intersect(mask).ToList(); RemoveSentinels(fields, deletes); RemoveSentinels(fields, serverTimestamps); updates = ApplyFieldMask(fields, mask); // Every field path in the mask must either refer to a now-removed sentinel, or a remaining value. GaxPreconditions.CheckArgument(mask.All(p => updates.ContainsKey(p) || deletes.Contains(p) || serverTimestamps.Contains(p)), 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, serverTimestamps); updates = fields.ToDictionary(pair => new FieldPath(pair.Key), pair => pair.Value); updatePaths = s_emptyFieldPathList; forceWrite = true; } AddUpdateWrites(documentReference, ExpandObject(updates), updatePaths, null, serverTimestamps, forceWrite); return(this); }