Skip to content

Commit 76b0816

Browse files
committed
feat: add SetOptions for merging data in BulkWriter and WriteBatch
1 parent 4b7ea97 commit 76b0816

File tree

10 files changed

+391
-30
lines changed

10 files changed

+391
-30
lines changed

packages/googleapis_firestore/lib/googleapis_firestore.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,5 @@ export 'src/firestore.dart'
4848
DocumentChange,
4949
DocumentChangeType,
5050
Precondition,
51-
TransactionHandler;
51+
TransactionHandler,
52+
SetOptions;

packages/googleapis_firestore/lib/src/bulk_writer.dart

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -575,9 +575,17 @@ class BulkWriter {
575575
/// print('Write failed with: $err');
576576
/// });
577577
/// ```
578-
Future<WriteResult> set<T>(DocumentReference<T> ref, T data) {
578+
Future<WriteResult> set<T>(
579+
DocumentReference<T> ref,
580+
T data, {
581+
SetOptions? options,
582+
}) {
579583
_verifyNotClosed();
580-
return _enqueue(ref, 'set', (batch) => batch.set(ref, data));
584+
return _enqueue(
585+
ref,
586+
'set',
587+
(batch) => batch.set(ref, data, options: options),
588+
);
581589
}
582590

583591
/// Update fields of the document referred to by the provided

packages/googleapis_firestore/lib/src/document.dart

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,8 +474,110 @@ class _DocumentMask {
474474
return _DocumentMask(fieldPaths);
475475
}
476476

477+
/// Creates a document mask from a list of field paths.
478+
factory _DocumentMask.fromFieldMask(List<FieldPath> fieldMask) {
479+
return _DocumentMask(List.from(fieldMask));
480+
}
481+
482+
/// Creates a document mask with the field names of a document.
483+
/// Recursively extracts all field paths from the data object.
484+
factory _DocumentMask.fromObject(Map<String, Object?> data) {
485+
final fieldPaths = <FieldPath>[];
486+
487+
void extractFieldPaths(
488+
Map<String, Object?> currentData, [
489+
FieldPath? currentPath,
490+
]) {
491+
var isEmpty = true;
492+
493+
for (final entry in currentData.entries) {
494+
isEmpty = false;
495+
496+
final key = entry.key;
497+
final childSegment = FieldPath([key]);
498+
final childPath = currentPath != null
499+
? currentPath.append(childSegment)
500+
: childSegment;
501+
final value = entry.value;
502+
503+
if (value is _FieldTransform) {
504+
if (value.includeInDocumentMask) {
505+
fieldPaths.add(childPath);
506+
}
507+
} else if (value is Map<String, Object?>) {
508+
extractFieldPaths(value, childPath);
509+
} else if (value != null) {
510+
fieldPaths.add(childPath);
511+
}
512+
}
513+
514+
// Add a field path for an explicitly updated empty map.
515+
if (currentPath != null && isEmpty) {
516+
fieldPaths.add(currentPath);
517+
}
518+
}
519+
520+
extractFieldPaths(data);
521+
return _DocumentMask(fieldPaths);
522+
}
523+
477524
final List<FieldPath> _sortedPaths;
478525

526+
bool get isEmpty => _sortedPaths.isEmpty;
527+
528+
/// Removes the specified field paths from this document mask.
529+
void removeFields(List<FieldPath> fieldPaths) {
530+
_sortedPaths.removeWhere((path) => fieldPaths.any((fp) => path == fp));
531+
}
532+
533+
/// Returns whether this document mask contains the specified field path.
534+
bool contains(FieldPath fieldPath) {
535+
return _sortedPaths.any((path) => path == fieldPath);
536+
}
537+
538+
/// Applies this DocumentMask to data and returns a new object containing only
539+
/// the fields specified in the mask.
540+
Map<String, Object?> applyTo(Map<String, Object?> data) {
541+
final remainingPaths = List<FieldPath>.from(_sortedPaths);
542+
543+
Map<String, Object?> processObject(
544+
Map<String, Object?> currentData, [
545+
FieldPath? currentPath,
546+
]) {
547+
final result = <String, Object?>{};
548+
549+
for (final entry in currentData.entries) {
550+
final key = entry.key;
551+
final childSegment = FieldPath([key]);
552+
final childPath = currentPath != null
553+
? currentPath.append(childSegment)
554+
: childSegment;
555+
556+
// Check if this field or any of its children are in the mask
557+
final shouldInclude = remainingPaths.any((path) {
558+
return path == childPath || path.isPrefixOf(childPath);
559+
});
560+
561+
if (shouldInclude) {
562+
final value = entry.value;
563+
564+
if (value is Map<String, Object?>) {
565+
result[key] = processObject(value, childPath);
566+
} else {
567+
result[key] = value;
568+
}
569+
570+
// Remove this path from remaining
571+
remainingPaths.removeWhere((path) => path == childPath);
572+
}
573+
}
574+
575+
return result;
576+
}
577+
578+
return processObject(data);
579+
}
580+
479581
firestore_v1.DocumentMask toProto() {
480582
if (_sortedPaths.isEmpty) return firestore_v1.DocumentMask();
481583

packages/googleapis_firestore/lib/src/firestore.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'dart:convert' show jsonDecode, jsonEncode;
33
import 'dart:io';
44
import 'dart:math' as math;
55
import 'dart:typed_data';
6+
67
import 'package:collection/collection.dart';
78
import 'package:googleapis/firestore/v1.dart' as firestore_v1;
89
import 'package:googleapis_auth/googleapis_auth.dart'
@@ -13,11 +14,11 @@ import 'package:http/http.dart'
1314
show BaseRequest, StreamedResponse, ByteStream, BaseClient, Client;
1415
import 'package:intl/intl.dart';
1516
import 'package:meta/meta.dart';
16-
1717
import 'backoff.dart';
1818
import 'environment.dart';
1919

2020
part 'aggregate.dart';
21+
part 'bulk_writer.dart';
2122
part 'collection_group.dart';
2223
part 'convert.dart';
2324
part 'document.dart';
@@ -29,6 +30,7 @@ part 'firestore_exception.dart';
2930
part 'firestore_http_client.dart';
3031
part 'geo_point.dart';
3132
part 'path.dart';
33+
part 'rate_limiter.dart';
3234
part 'reference/aggregate_query.dart';
3335
part 'reference/aggregate_query_snapshot.dart';
3436
part 'reference/collection_reference.dart';
@@ -44,15 +46,14 @@ part 'reference/query_snapshot.dart';
4446
part 'reference/query_util.dart';
4547
part 'reference/types.dart';
4648
part 'serializer.dart';
49+
part 'set_options.dart';
4750
part 'status_code.dart';
4851
part 'timestamp.dart';
4952
part 'transaction.dart';
5053
part 'types.dart';
5154
part 'util.dart';
5255
part 'validate.dart';
5356
part 'write_batch.dart';
54-
part 'rate_limiter.dart';
55-
part 'bulk_writer.dart';
5657

5758
/// Plain credentials object for service account authentication.
5859
///

packages/googleapis_firestore/lib/src/path.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,12 @@ class FieldPath extends _Path<FieldPath> {
312312
.join('.');
313313
}
314314

315+
/// Checks whether this field path is a prefix of the specified path.
316+
bool isPrefixOf(FieldPath other) => _isPrefixOf(other);
317+
318+
/// Appends a child segment to this field path.
319+
FieldPath append(FieldPath childSegment) => _appendPath(childSegment);
320+
315321
@override
316322
FieldPath _construct(List<String> segments) => FieldPath(segments);
317323

packages/googleapis_firestore/lib/src/reference/document_reference.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,11 @@ final class DocumentReference<T> implements _Serializable {
135135
}
136136

137137
/// Writes to the document referred to by this DocumentReference. If the
138-
/// document does not yet exist, it will be created.
139-
Future<WriteResult> set(T data) async {
140-
final writeBatch = WriteBatch._(firestore)..set(this, data);
138+
/// document does not yet exist, it will be created. If [SetOptions] is provided,
139+
/// the data can be merged into the existing document.
140+
Future<WriteResult> set(T data, {SetOptions? options}) async {
141+
final writeBatch = WriteBatch._(firestore)
142+
..set(this, data, options: options);
141143

142144
final results = await writeBatch.commit();
143145
return results.single;
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
part of 'firestore.dart';
2+
3+
/// Options to configure [WriteBatch.set], [Transaction.set], and [BulkWriter.set] behavior.
4+
///
5+
/// Provides control over whether the set operation should merge data into an
6+
/// existing document instead of replacing it entirely.
7+
@immutable
8+
sealed class SetOptions {
9+
const SetOptions._();
10+
11+
/// Merge all provided fields.
12+
///
13+
/// If a field is present in the data but not in the document, it will be added.
14+
/// If a field is present in both, the document's field will be updated.
15+
/// Fields in the document that are not in the data will remain untouched.
16+
const factory SetOptions.merge() = _MergeAllSetOptions;
17+
18+
/// Merge only the specified fields.
19+
///
20+
/// Only the field paths listed in [mergeFields] will be updated or created.
21+
/// All other fields will remain untouched.
22+
///
23+
/// Example:
24+
/// ```dart
25+
/// // Only update the 'name' field, leave other fields unchanged
26+
/// ref.set(
27+
/// {'name': 'John', 'age': 30},
28+
/// SetOptions.mergeFields([FieldPath(['name'])]),
29+
/// );
30+
/// ```
31+
const factory SetOptions.mergeFields(List<FieldPath> fields) =
32+
_MergeFieldsSetOptions;
33+
34+
/// Whether this represents a merge operation (either merge all or specific fields).
35+
bool get isMerge;
36+
37+
/// The list of field paths to merge. Null if merging all fields or not merging.
38+
List<FieldPath>? get mergeFields;
39+
40+
@override
41+
bool operator ==(Object other);
42+
43+
@override
44+
int get hashCode;
45+
}
46+
47+
/// Merge all fields from the provided data.
48+
@immutable
49+
class _MergeAllSetOptions extends SetOptions {
50+
const _MergeAllSetOptions() : super._();
51+
52+
@override
53+
bool get isMerge => true;
54+
55+
@override
56+
List<FieldPath>? get mergeFields => null;
57+
58+
@override
59+
bool operator ==(Object other) =>
60+
identical(this, other) || other is _MergeAllSetOptions;
61+
62+
@override
63+
int get hashCode => runtimeType.hashCode;
64+
65+
@override
66+
String toString() => 'SetOptions.merge()';
67+
}
68+
69+
/// Merge only the specified field paths.
70+
@immutable
71+
class _MergeFieldsSetOptions extends SetOptions {
72+
const _MergeFieldsSetOptions(this.fields) : super._();
73+
74+
final List<FieldPath> fields;
75+
76+
@override
77+
bool get isMerge => true;
78+
79+
@override
80+
List<FieldPath>? get mergeFields => fields;
81+
82+
@override
83+
bool operator ==(Object other) =>
84+
identical(this, other) ||
85+
(other is _MergeFieldsSetOptions &&
86+
const ListEquality<FieldPath>().equals(fields, other.fields));
87+
88+
@override
89+
int get hashCode =>
90+
Object.hash(runtimeType, const ListEquality<FieldPath>().hash(fields));
91+
92+
@override
93+
String toString() => 'SetOptions.mergeFields($fields)';
94+
}

packages/googleapis_firestore/lib/src/transaction.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,24 +140,24 @@ class Transaction {
140140
_writeBatch.create(documentRef, documentData);
141141
}
142142

143-
//TODO support SetOptions to include merge parameter
144-
145143
/// Write to the document referred to by the provided
146144
/// [DocumentReference]. If the document does not exist yet, it will be
147145
/// created. If the document already exists, its contents will be
148-
/// overwritten with the newly provided data.
146+
/// overwritten with the newly provided data unless [SetOptions] is provided
147+
/// to merge the data.
149148
///
150149
/// - [documentRef]: A reference to the document to be set.
151150
/// - [data] The object to serialize as the document.
151+
/// - [options] Optional [SetOptions] to control merge behavior.
152152
///
153-
void set<T>(DocumentReference<T> documentRef, T data) {
153+
void set<T>(DocumentReference<T> documentRef, T data, {SetOptions? options}) {
154154
if (_writeBatch == null) {
155155
throw FirestoreException(
156156
FirestoreClientErrorCode.failedPrecondition,
157157
readOnlyWriteErrorMsg,
158158
);
159159
}
160-
_writeBatch.set<T>(documentRef, data);
160+
_writeBatch.set<T>(documentRef, data, options: options);
161161
}
162162

163163
/// Updates fields in the document referred to by the provided

0 commit comments

Comments
 (0)