google-cloud-firestore 2.7.2 → 2.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/AUTHENTICATION.md +8 -26
  3. data/CHANGELOG.md +69 -0
  4. data/LOGGING.md +1 -1
  5. data/lib/google/cloud/firestore/aggregate_query.rb +285 -0
  6. data/lib/google/cloud/firestore/aggregate_query_snapshot.rb +145 -0
  7. data/lib/google/cloud/firestore/bulk_commit_batch.rb +73 -0
  8. data/lib/google/cloud/firestore/bulk_writer.rb +558 -0
  9. data/lib/google/cloud/firestore/bulk_writer_exception.rb +40 -0
  10. data/lib/google/cloud/firestore/bulk_writer_operation.rb +126 -0
  11. data/lib/google/cloud/firestore/bulk_writer_scheduler.rb +164 -0
  12. data/lib/google/cloud/firestore/client.rb +161 -10
  13. data/lib/google/cloud/firestore/collection_group.rb +20 -4
  14. data/lib/google/cloud/firestore/collection_reference.rb +17 -2
  15. data/lib/google/cloud/firestore/collection_reference_list.rb +4 -3
  16. data/lib/google/cloud/firestore/convert.rb +6 -7
  17. data/lib/google/cloud/firestore/document_reference/list.rb +5 -3
  18. data/lib/google/cloud/firestore/document_reference.rb +20 -3
  19. data/lib/google/cloud/firestore/document_snapshot.rb +1 -1
  20. data/lib/google/cloud/firestore/errors.rb +60 -0
  21. data/lib/google/cloud/firestore/filter.rb +326 -0
  22. data/lib/google/cloud/firestore/promise/future.rb +97 -0
  23. data/lib/google/cloud/firestore/query.rb +112 -89
  24. data/lib/google/cloud/firestore/rate_limiter.rb +80 -0
  25. data/lib/google/cloud/firestore/service.rb +74 -23
  26. data/lib/google/cloud/firestore/transaction.rb +57 -4
  27. data/lib/google/cloud/firestore/version.rb +1 -1
  28. data/lib/google/cloud/firestore.rb +17 -7
  29. data/lib/google-cloud-firestore.rb +45 -8
  30. metadata +17 -146
@@ -0,0 +1,126 @@
1
+ # Copyright 2023 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ require "concurrent"
17
+ require "google/cloud/firestore/bulk_writer_exception"
18
+
19
+
20
+ module Google
21
+ module Cloud
22
+ module Firestore
23
+ ##
24
+ #
25
+ # @private A BulkWriterOperation object refers to a write operation and contains
26
+ # all the necessary information for a specific write task, including meta
27
+ # information like the current number of attempts
28
+ #
29
+ class BulkWriterOperation
30
+ attr_reader :retry_time
31
+ attr_reader :result
32
+ attr_reader :completion_event
33
+ attr_reader :write
34
+
35
+ ##
36
+ # Initialize the object
37
+ def initialize write, retries
38
+ @write = write
39
+ @failed_attempts = 0
40
+ @retries = retries
41
+ @retry_time = Time.now
42
+ @completion_event = Concurrent::Event.new
43
+ end
44
+
45
+ ##
46
+ # Processing to be done when the response is a success.
47
+ # Updates the result and set the completion event.
48
+ #
49
+ # @param [Google::Cloud::Firestore::V1::WriteResult] result The result returned in the response.
50
+ def on_success result
51
+ begin
52
+ @result = WriteResult.new result
53
+ rescue StandardError => e
54
+ raise BulkWriterOperationError, e
55
+ ensure
56
+ @completion_event.set
57
+ end
58
+ end
59
+
60
+ ##
61
+ # Processing to be done when the response is a failure.
62
+ # Updates the failure attempts. If the retry count reaches
63
+ # the upper threshold, operations will be marked
64
+ # as failure and the completion event will be set.
65
+ #
66
+ # @param [Google::Rpc::Status] status The status received in the response.
67
+ #
68
+ def on_failure status
69
+ @failed_attempts += 1
70
+ if @failed_attempts == @retries + 1
71
+ begin
72
+ @result = BulkWriterException.new status
73
+ rescue StandardError => e
74
+ raise BulkWriterOperationError, e
75
+ ensure
76
+ @completion_event.set
77
+ end
78
+ else
79
+ backoff_duration
80
+ end
81
+ end
82
+
83
+ ##
84
+ # Exponentially increases the waiting time for retry.
85
+ #
86
+ def backoff_duration
87
+ @retry_time = Time.now + (@failed_attempts**2)
88
+ end
89
+
90
+ ##
91
+ # Represents the result of applying a write.
92
+ #
93
+ # @example
94
+ # require "google/cloud/firestore"
95
+ #
96
+ # firestore = Google::Cloud::Firestore.new
97
+ # bw = firestore.bulk_writer
98
+ #
99
+ # # Set the data for NYC
100
+ # result = bw.set("cities/NYC", { name: "New York City" })
101
+ #
102
+ # result.wait!
103
+ #
104
+ # puts result.value
105
+ #
106
+ class WriteResult
107
+ ##
108
+ # The last update time of the document after applying the write. Set to
109
+ # nil for a +delete+ mutation.
110
+ #
111
+ # If the write did not actually change the document, this will be
112
+ # the previous update_time.
113
+ #
114
+ # @return [Time] The last update time.
115
+ attr_reader :update_time
116
+
117
+ ##
118
+ # @private
119
+ def initialize result
120
+ @update_time = Convert.timestamp_to_time result.update_time
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,164 @@
1
+ # Copyright 2023 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License")
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ require "concurrent"
17
+ require "google/cloud/firestore/errors"
18
+ require "google/cloud/firestore/bulk_writer_operation"
19
+ require "google/cloud/firestore/rate_limiter"
20
+ require "google/cloud/firestore/bulk_commit_batch"
21
+ require "google/cloud/firestore/bulk_writer_exception"
22
+ require "google/cloud/firestore/bulk_writer_scheduler"
23
+
24
+
25
+ module Google
26
+ module Cloud
27
+ module Firestore
28
+ ##
29
+ #
30
+ # @private Accumulate BulkWriterOperations from the BulkWriter, schedules them
31
+ # in accordance with 555 rule and retry the failed operations from the BulkCommitBatch.
32
+ #
33
+ class BulkWriterScheduler
34
+ MAX_BATCH_SIZE = 20
35
+ BATCH_THREAD_COUNT = 4
36
+
37
+ ##
38
+ # Initialize the attributes and start the schedule_operations job
39
+ #
40
+ def initialize client, service, batch_threads
41
+ @client = client
42
+ @service = service
43
+ @rate_limiter = RateLimiter.new
44
+ @buffered_operations = []
45
+ @batch_threads = (batch_threads || BATCH_THREAD_COUNT).to_i
46
+ @batch_thread_pool = Concurrent::ThreadPoolExecutor.new max_threads: @batch_threads,
47
+ max_queue: 0,
48
+ auto_terminate: true
49
+ @retry_operations = []
50
+ @mutex = Mutex.new
51
+ start_scheduling_operations
52
+ end
53
+
54
+ def start_scheduling_operations
55
+ Concurrent::Promises.future_on @batch_thread_pool do
56
+ begin
57
+ schedule_operations
58
+ rescue StandardError
59
+ # TODO: Log the error when logging is available
60
+ retry
61
+ end
62
+ end
63
+ end
64
+
65
+ def add_operation operation
66
+ @mutex.synchronize { @buffered_operations << operation }
67
+ end
68
+
69
+ ##
70
+ # Closes the scheduler object.
71
+ # Waits for the enqueued tasks to complete
72
+ # before closing down.
73
+ #
74
+ # @return [nil]
75
+ def close
76
+ @mutex.synchronize do
77
+ @batch_thread_pool.shutdown
78
+ @batch_thread_pool.wait_for_termination 1
79
+ @batch_thread_pool.kill unless @batch_thread_pool.shutdown?
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ ##
86
+ # @private Adds failed operations in the retry heap.
87
+ #
88
+ def post_commit_batch bulk_commit_batch
89
+ @mutex.synchronize do
90
+ bulk_commit_batch.operations.each do |operation|
91
+ unless operation.completion_event.set?
92
+ @retry_operations << operation
93
+ end
94
+ end
95
+ @retry_operations.sort_by!(&:retry_time)
96
+ end
97
+ end
98
+
99
+ ##
100
+ # @private Commits a batch of scheduled operations.
101
+ # Batch size <= 20 to match the constraint of request size < 9.8 MB
102
+ #
103
+ # @return [nil]
104
+ def commit_batch bulk_commit_batch
105
+ begin
106
+ Concurrent::Promises.future_on @batch_thread_pool, bulk_commit_batch do |batch|
107
+ begin
108
+ batch.commit
109
+ rescue StandardError
110
+ # TODO: Log the errors while committing a batch
111
+ ensure
112
+ post_commit_batch bulk_commit_batch
113
+ end
114
+ end
115
+ rescue StandardError => e
116
+ post_commit_batch bulk_commit_batch
117
+ raise BulkWriterSchedulerError, e.message
118
+ end
119
+ end
120
+
121
+ ##
122
+ # @private Schedule the enqueued operations in batches.
123
+ #
124
+ # @return [nil]
125
+ def schedule_operations
126
+ loop do
127
+ break if @batch_thread_pool.shuttingdown?
128
+ dequeue_retry_operations
129
+ batch_size = [MAX_BATCH_SIZE, @buffered_operations.length].min
130
+ if batch_size.zero?
131
+ sleep 0.001
132
+ next
133
+ end
134
+ @rate_limiter.wait_for_tokens batch_size
135
+ operations = dequeue_buffered_operations batch_size
136
+ commit_batch BulkCommitBatch.new(@service, operations)
137
+ end
138
+ end
139
+
140
+ ##
141
+ # @private Removes BulkWriterOperations from the buffered queue to scheduled in
142
+ # the current batch
143
+ #
144
+ def dequeue_buffered_operations size
145
+ @mutex.synchronize do
146
+ @buffered_operations.shift size
147
+ end
148
+ end
149
+
150
+ ##
151
+ # @private Removes BulkWriterOperations from the retry queue to scheduled in
152
+ # the current batch
153
+ #
154
+ def dequeue_retry_operations
155
+ @mutex.synchronize do
156
+ while @retry_operations.length.positive? && @retry_operations.first.retry_time <= Time.now
157
+ @buffered_operations << @retry_operations.shift
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -23,6 +23,8 @@ require "google/cloud/firestore/document_snapshot"
23
23
  require "google/cloud/firestore/collection_group"
24
24
  require "google/cloud/firestore/batch"
25
25
  require "google/cloud/firestore/transaction"
26
+ require "google/cloud/firestore/bulk_writer"
27
+ require "google/cloud/firestore/filter"
26
28
 
27
29
  module Google
28
30
  module Cloud
@@ -69,7 +71,7 @@ module Google
69
71
  #
70
72
  # @return [String] database identifier.
71
73
  def database_id
72
- "(default)"
74
+ service.database
73
75
  end
74
76
 
75
77
  ##
@@ -85,6 +87,9 @@ module Google
85
87
  ##
86
88
  # Retrieves an enumerator for the root collections.
87
89
  #
90
+ # @param [Time] read_time Reads documents as they were at the given time.
91
+ # This may not be older than 270 seconds. Optional
92
+ #
88
93
  # @yield [collections] The block for accessing the collections.
89
94
  # @yieldparam [CollectionReference] collection A collection reference object.
90
95
  #
@@ -101,10 +106,21 @@ module Google
101
106
  # puts col.collection_id
102
107
  # end
103
108
  #
104
- def cols &block
109
+ # @example
110
+ # require "google/cloud/firestore"
111
+ #
112
+ # firestore = Google::Cloud::Firestore.new
113
+ # read_time = Time.now
114
+ #
115
+ # # Get the root collections
116
+ # firestore.cols(read_time: read_time).each do |col|
117
+ # puts col.collection_id
118
+ # end
119
+ #
120
+ def cols read_time: nil, &block
105
121
  ensure_service!
106
- grpc = service.list_collections "#{path}/documents"
107
- cols_enum = CollectionReferenceList.from_grpc(grpc, self, "#{path}/documents").all
122
+ grpc = service.list_collections "#{path}/documents", read_time: read_time
123
+ cols_enum = CollectionReferenceList.from_grpc(grpc, self, "#{path}/documents", read_time: read_time).all
108
124
  cols_enum.each(&block) if block_given?
109
125
  cols_enum
110
126
  end
@@ -171,6 +187,56 @@ module Google
171
187
  end
172
188
  alias collection_group col_group
173
189
 
190
+ ##
191
+ # Creates a filter object.
192
+ #
193
+ # @param field [FieldPath, String, Symbol] A field path to filter
194
+ # results with.
195
+ # If a {FieldPath} object is not provided then the field will be
196
+ # treated as a dotted string, meaning the string represents individual
197
+ # fields joined by ".". Fields containing `~`, `*`, `/`, `[`, `]`, and
198
+ # `.` cannot be in a dotted string, and should provided using a
199
+ # {FieldPath} object instead.
200
+ #
201
+ # @param operator [String, Symbol] The operation to compare the field
202
+ # to. Acceptable values include:
203
+ # * less than: `<`, `lt`
204
+ # * less than or equal: `<=`, `lte`
205
+ # * greater than: `>`, `gt`
206
+ # * greater than or equal: `>=`, `gte`
207
+ # * equal: `=`, `==`, `eq`, `eql`, `is`
208
+ # * not equal: `!=`
209
+ # * in: `in`
210
+ # * not in: `not-in`, `not_in`
211
+ # * array contains: `array-contains`, `array_contains`
212
+ #
213
+ # @param value [Object] The value to compare the property to. Defaults to nil.
214
+ # Possible values are:
215
+ # * Integer
216
+ # * Float/BigDecimal
217
+ # * String
218
+ # * Boolean
219
+ # * Array
220
+ # * Date/Time
221
+ # * StringIO
222
+ # * Google::Cloud::Datastore::Key
223
+ # * Google::Cloud::Datastore::Entity
224
+ # * nil
225
+ #
226
+ # @return [Google::Cloud::Firestore::Filter] New filter object.
227
+ #
228
+ # @example
229
+ # require "google/cloud/firestore"
230
+ #
231
+ # firestore = Google::Cloud::Firestore.new
232
+ #
233
+ # # Create a filter
234
+ # filter = firestore.filter(:population, :>=, 1000000)
235
+ #
236
+ def filter field, operator, value
237
+ Filter.new field, operator, value
238
+ end
239
+
174
240
  ##
175
241
  # Retrieves a document reference.
176
242
  #
@@ -217,6 +283,8 @@ module Google
217
283
  # individual fields joined by ".". Fields containing `~`, `*`, `/`,
218
284
  # `[`, `]`, and `.` cannot be in a dotted string, and should provided
219
285
  # using a {FieldPath} object instead. (See {#field_path}.)
286
+ # @param [Time] read_time Reads documents as they were at the given time.
287
+ # This may not be older than 270 seconds. Optional
220
288
  #
221
289
  # @yield [documents] The block for accessing the document snapshots.
222
290
  # @yieldparam [DocumentSnapshot] document A document snapshot.
@@ -245,11 +313,24 @@ module Google
245
313
  # puts "#{city.document_id} has #{city[:population]} residents."
246
314
  # end
247
315
  #
248
- def get_all *docs, field_mask: nil
316
+ # @example Get docs using a read_time:
317
+ # require "google/cloud/firestore"
318
+ #
319
+ # firestore = Google::Cloud::Firestore.new
320
+ #
321
+ # read_time = Time.now
322
+ #
323
+ # # Get and print city documents
324
+ # cities = ["cities/NYC", "cities/SF", "cities/LA"]
325
+ # firestore.get_all(cities, read_time: read_time).each do |city|
326
+ # puts "#{city.document_id} has #{city[:population]} residents."
327
+ # end
328
+ #
329
+ def get_all *docs, field_mask: nil, read_time: nil
249
330
  ensure_service!
250
331
 
251
332
  unless block_given?
252
- return enum_for :get_all, *docs, field_mask: field_mask
333
+ return enum_for :get_all, *docs, field_mask: field_mask, read_time: read_time
253
334
  end
254
335
 
255
336
  doc_paths = Array(docs).flatten.map do |doc_path|
@@ -264,7 +345,7 @@ module Google
264
345
  end
265
346
  mask = nil if mask.empty?
266
347
 
267
- results = service.get_documents doc_paths, mask: mask
348
+ results = service.get_documents doc_paths, mask: mask, read_time: read_time
268
349
  results.each do |result|
269
350
  next if result.result.nil?
270
351
  yield DocumentSnapshot.from_batch_result result, self
@@ -668,13 +749,83 @@ module Google
668
749
  end
669
750
  end
670
751
 
752
+ ##
753
+ # Create a transaction to perform multiple reads that are
754
+ # executed atomically at a single logical point in time in a database.
755
+ #
756
+ # All changes are accumulated in memory until the block completes.
757
+ # Transactions will be automatically retried when documents change
758
+ # before the transaction is committed. See {Transaction}.
759
+ #
760
+ # @see https://firebase.google.com/docs/firestore/manage-data/transactions
761
+ # Transactions and Batched Writes
762
+ #
763
+ # @param [Time] read_time The maximum number of retries for
764
+ # transactions failed due to errors. Default is 5. Optional.
765
+ #
766
+ # @yield [transaction] The block for reading data.
767
+ # @yieldparam [Transaction] transaction The transaction object for
768
+ # making changes.
769
+ #
770
+ # @return [Object] The return value of the provided
771
+ # yield block
772
+ #
773
+ # @example Read only transaction with read time
774
+ # require "google/cloud/firestore"
775
+ #
776
+ # firestore = Google::Cloud::Firestore.new
777
+ #
778
+ # # Get a document reference
779
+ # nyc_ref = firestore.doc "cities/NYC"
780
+ #
781
+ # read_time = Time.now
782
+ #
783
+ # firestore.read_only_transaction(read_time: read_time) do |tx|
784
+ # # Get a document snapshot
785
+ # nyc_snap = tx.get nyc_ref
786
+ # end
787
+ #
788
+ def read_only_transaction read_time: nil
789
+ transaction = Transaction.from_client self, read_time: read_time, read_only: true
790
+ yield transaction
791
+ end
792
+
793
+ ##
794
+ # Create a bulk writer to perform multiple writes that are
795
+ # executed parallely.
796
+ #
797
+ # @param [Integer] request_threads The number of threads used for handling
798
+ # requests. Default is 2. Optional.
799
+ # @param [Integer] batch_threads The number of threads used for processing
800
+ # batches. Default is 4. Optional.
801
+ # @param [Integer] retries The number of times a failed write request will
802
+ # be retried (with exponential delay) before being marked as failure. Max
803
+ # attempts are 15. Optional
804
+ #
805
+ # @return [Google::Cloud::Firestore::BulkWriter] Returns an object of
806
+ # bulk writer.
807
+ #
808
+ # @example Initializing a BulkWriter with all the configurations.
809
+ # require "google/cloud/firestore"
810
+ #
811
+ # firestore = Google::Cloud::Firestore.new
812
+ #
813
+ # bw = firestore.bulk_writer
814
+ #
815
+ # bulk_write_result = bw.create "doc_ref", request_threads: 4, batch_threads: 10, retries: 10
816
+ #
817
+ def bulk_writer request_threads: nil, batch_threads: nil, retries: nil
818
+ BulkWriter.new self, @service, request_threads: request_threads,
819
+ batch_threads: batch_threads, retries: retries
820
+ end
821
+
671
822
  # @!endgroup
672
823
 
673
824
  # @private
674
- def list_documents parent, collection_id, token: nil, max: nil
825
+ def list_documents parent, collection_id, token: nil, max: nil, read_time: nil
675
826
  ensure_service!
676
- grpc = service.list_documents parent, collection_id, token: token, max: max
677
- DocumentReference::List.from_grpc grpc, self, parent, collection_id
827
+ grpc = service.list_documents parent, collection_id, token: token, max: max, read_time: read_time
828
+ DocumentReference::List.from_grpc grpc, self, parent, collection_id, read_time: read_time
678
829
  end
679
830
 
680
831
  protected
@@ -48,6 +48,8 @@ module Google
48
48
  #
49
49
  # @param [Integer] partition_count The desired maximum number of partition points. The number must be strictly
50
50
  # positive. The actual number of partitions returned may be fewer.
51
+ # @param [Time] read_time Reads documents as they were at the given time.
52
+ # This may not be older than 270 seconds. Optional
51
53
  #
52
54
  # @return [Array<QueryPartition>] An ordered array of query partitions.
53
55
  #
@@ -62,7 +64,20 @@ module Google
62
64
  #
63
65
  # queries = partitions.map(&:to_query)
64
66
  #
65
- def partitions partition_count
67
+ # @example partition with read time
68
+ # require "google/cloud/firestore"
69
+ #
70
+ # firestore = Google::Cloud::Firestore.new
71
+ #
72
+ # col_group = firestore.col_group "cities"
73
+ #
74
+ # read_time = Time.now
75
+ #
76
+ # partitions = col_group.partitions 3, read_time: read_time
77
+ #
78
+ # queries = partitions.map(&:to_query)
79
+ #
80
+ def partitions partition_count, read_time: nil
66
81
  ensure_service!
67
82
 
68
83
  raise ArgumentError, "partition_count must be > 0" unless partition_count.positive?
@@ -75,7 +90,7 @@ module Google
75
90
 
76
91
  grpc_partitions = if partition_count.positive?
77
92
  # Retrieve all pages, since cursor order is not guaranteed and they must be sorted.
78
- list_all partition_count, query_with_default_order
93
+ list_all partition_count, query_with_default_order, read_time
79
94
  else
80
95
  [] # Ensure that a single, empty QueryPartition is returned.
81
96
  end
@@ -118,11 +133,12 @@ module Google
118
133
 
119
134
  protected
120
135
 
121
- def list_all partition_count, query_with_default_order
136
+ def list_all partition_count, query_with_default_order, read_time
122
137
  grpc_partitions = []
123
138
  token = nil
124
139
  loop do
125
- grpc = service.partition_query parent_path, query_with_default_order.query, partition_count, token: token
140
+ grpc = service.partition_query parent_path, query_with_default_order.query, partition_count,
141
+ token: token, read_time: read_time
126
142
  grpc_partitions += Array(grpc.partitions)
127
143
  token = grpc.next_page_token
128
144
  token = nil if token == ""
@@ -147,6 +147,8 @@ module Google
147
147
  # @param [String] token A previously-returned page token representing
148
148
  # part of the larger set of results to view.
149
149
  # @param [Integer] max Maximum number of results to return.
150
+ # @param [Time] read_time Reads documents as they were at the given time.
151
+ # This may not be older than 270 seconds. Optional
150
152
  #
151
153
  # @return [Array<DocumentReference>] An array of document references.
152
154
  #
@@ -161,10 +163,23 @@ module Google
161
163
  # puts doc_ref.document_id
162
164
  # end
163
165
  #
164
- def list_documents token: nil, max: nil
166
+ # @example List documents with read time
167
+ # require "google/cloud/firestore"
168
+ #
169
+ # firestore = Google::Cloud::Firestore.new
170
+ #
171
+ # read_time = Time.now
172
+ #
173
+ # col = firestore.col "cities"
174
+ #
175
+ # col.list_documents(read_time: read_time).each do |doc_ref|
176
+ # puts doc_ref.document_id
177
+ # end
178
+ #
179
+ def list_documents token: nil, max: nil, read_time: nil
165
180
  ensure_client!
166
181
  client.list_documents \
167
- parent_path, collection_id, token: token, max: max
182
+ parent_path, collection_id, token: token, max: max, read_time: read_time
168
183
  end
169
184
 
170
185
  ##
@@ -52,8 +52,8 @@ module Google
52
52
  def next
53
53
  return nil unless next?
54
54
  ensure_service!
55
- grpc = @client.service.list_collections @parent, token: token, max: @max
56
- self.class.from_grpc grpc, @client, @parent, max: @max
55
+ grpc = @client.service.list_collections @parent, token: token, max: @max, read_time: @read_time
56
+ self.class.from_grpc grpc, @client, @parent, max: @max, read_time: @read_time
57
57
  end
58
58
 
59
59
  ##
@@ -110,7 +110,7 @@ module Google
110
110
  ##
111
111
  # @private New CollectionReference::List from a `Google::Cloud::Firestore::V1::ListCollectionIdsResponse`
112
112
  # object.
113
- def self.from_grpc grpc, client, parent, max: nil
113
+ def self.from_grpc grpc, client, parent, max: nil, read_time: nil
114
114
  raise ArgumentError, "parent is required" unless parent
115
115
  cols = CollectionReferenceList.new(Array(grpc.collection_ids).map do |collection_id|
116
116
  CollectionReference.from_path "#{parent}/#{collection_id}", client
@@ -121,6 +121,7 @@ module Google
121
121
  cols.instance_variable_set :@client, client
122
122
  cols.instance_variable_set :@parent, parent
123
123
  cols.instance_variable_set :@max, max
124
+ cols.instance_variable_set :@read_time, read_time
124
125
  cols
125
126
  end
126
127
 
@@ -365,7 +365,7 @@ module Google
365
365
  write.current_document = \
366
366
  Google::Cloud::Firestore::V1::Precondition.new({
367
367
  exists: exists, update_time: time_to_timestamp(update_time)
368
- }.delete_if { |_, v| v.nil? })
368
+ }.compact)
369
369
  end
370
370
 
371
371
  write
@@ -381,12 +381,12 @@ module Google
381
381
  return val if val
382
382
  end
383
383
  when Hash
384
- obj.each do |_k, v|
384
+ obj.each_value do |v|
385
385
  val = field_value_nested? v, field_value_type
386
386
  return val if val
387
387
  end
388
388
  end
389
- nil
389
+ nil # rubocop:disable Style/ReturnNilInPredicateMethodDefinition
390
390
  end
391
391
 
392
392
  def remove_field_value_from obj, field_value_type = nil
@@ -498,7 +498,6 @@ module Google
498
498
  dup_hash = dup_hash[field]
499
499
  end
500
500
  prev_hash[last_field] = dup_hash
501
- prev_hash.delete_if { |_k, v| v.nil? }
502
501
  ret_hash
503
502
  end
504
503
 
@@ -536,13 +535,13 @@ module Google
536
535
  ESCAPED_FIELD_PATH = /\A`(.*)`\z/.freeze
537
536
 
538
537
  def build_hash_from_field_paths_and_values pairs
539
- pairs.each do |field_path, _value|
540
- raise ArgumentError unless field_path.is_a? FieldPath
538
+ pairs.each do |pair|
539
+ raise ArgumentError unless pair.first.is_a? FieldPath
541
540
  end
542
541
 
543
542
  dup_hash = {}
544
543
 
545
- pairs.each do |field_path, value|
544
+ pairs.each do |(field_path, value)|
546
545
  tmp_dup = dup_hash
547
546
  last_field = nil
548
547
  field_path.fields.map(&:to_sym).each do |field|