google-cloud-firestore 0.22.0 → 0.23.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/README.md +8 -8
  4. data/lib/google-cloud-firestore.rb +1 -1
  5. data/lib/google/cloud/firestore.rb +46 -0
  6. data/lib/google/cloud/firestore/batch.rb +1 -1
  7. data/lib/google/cloud/firestore/client.rb +18 -13
  8. data/lib/google/cloud/firestore/convert.rb +78 -35
  9. data/lib/google/cloud/firestore/credentials.rb +2 -12
  10. data/lib/google/cloud/firestore/document_change.rb +124 -0
  11. data/lib/google/cloud/firestore/document_listener.rb +125 -0
  12. data/lib/google/cloud/firestore/document_reference.rb +35 -0
  13. data/lib/google/cloud/firestore/document_snapshot.rb +91 -9
  14. data/lib/google/cloud/firestore/field_path.rb +23 -13
  15. data/lib/google/cloud/firestore/query.rb +513 -69
  16. data/lib/google/cloud/firestore/query_listener.rb +118 -0
  17. data/lib/google/cloud/firestore/query_snapshot.rb +121 -0
  18. data/lib/google/cloud/firestore/service.rb +8 -0
  19. data/lib/google/cloud/firestore/transaction.rb +2 -2
  20. data/lib/google/cloud/firestore/v1beta1.rb +62 -37
  21. data/lib/google/cloud/firestore/v1beta1/credentials.rb +41 -0
  22. data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/common.rb +1 -1
  23. data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/document.rb +5 -4
  24. data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/firestore.rb +1 -12
  25. data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/query.rb +4 -1
  26. data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/write.rb +37 -8
  27. data/lib/google/cloud/firestore/v1beta1/doc/google/protobuf/any.rb +1 -1
  28. data/lib/google/cloud/firestore/v1beta1/doc/google/protobuf/empty.rb +28 -0
  29. data/lib/google/cloud/firestore/v1beta1/doc/google/protobuf/timestamp.rb +1 -1
  30. data/lib/google/cloud/firestore/v1beta1/doc/google/protobuf/wrappers.rb +1 -1
  31. data/lib/google/cloud/firestore/v1beta1/doc/google/rpc/status.rb +1 -1
  32. data/lib/google/cloud/firestore/v1beta1/firestore_client.rb +124 -56
  33. data/lib/google/cloud/firestore/v1beta1/firestore_client_config.json +2 -2
  34. data/lib/google/cloud/firestore/version.rb +1 -1
  35. data/lib/google/cloud/firestore/watch/enumerator_queue.rb +47 -0
  36. data/lib/google/cloud/firestore/watch/inventory.rb +280 -0
  37. data/lib/google/cloud/firestore/watch/listener.rb +298 -0
  38. data/lib/google/cloud/firestore/watch/order.rb +98 -0
  39. data/lib/google/firestore/v1beta1/firestore_services_pb.rb +2 -4
  40. data/lib/google/firestore/v1beta1/query_pb.rb +1 -0
  41. data/lib/google/firestore/v1beta1/write_pb.rb +2 -0
  42. metadata +40 -3
  43. data/lib/google/cloud/firestore/v1beta1/doc/overview.rb +0 -53
@@ -0,0 +1,125 @@
1
+ # Copyright 2018 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 "google/cloud/firestore/watch/listener"
17
+
18
+ module Google
19
+ module Cloud
20
+ module Firestore
21
+ ##
22
+ # An ongoing listen operation on a document reference. This is returned by
23
+ # calling {DocumentReference#listen}.
24
+ #
25
+ # @example
26
+ # require "google/cloud/firestore"
27
+ #
28
+ # firestore = Google::Cloud::Firestore.new
29
+ #
30
+ # # Get a document reference
31
+ # nyc_ref = firestore.doc "cities/NYC"
32
+ #
33
+ # listener = nyc_ref.listen do |snapshot|
34
+ # puts "The population of #{snapshot[:name]} "
35
+ # puts "is #{snapshot[:population]}."
36
+ # end
37
+ #
38
+ # # When ready, stop the listen operation and close the stream.
39
+ # listener.stop
40
+ #
41
+ class DocumentListener
42
+ ##
43
+ # @private
44
+ # Creates the watch stream and listener object.
45
+ def initialize doc_ref, &callback
46
+ @doc_ref = doc_ref
47
+ raise ArgumentError if @doc_ref.nil?
48
+
49
+ @callback = callback
50
+ raise ArgumentError if @callback.nil?
51
+
52
+ @listener = Watch::Listener.for_doc_ref doc_ref do |query_snp|
53
+ doc_snp = query_snp.docs.find { |doc| doc.path == @doc_ref.path }
54
+
55
+ if doc_snp.nil?
56
+ doc_snp = DocumentSnapshot.missing \
57
+ @doc_ref, read_at: query_snp.read_at
58
+ end
59
+
60
+ @callback.call doc_snp
61
+ end
62
+ end
63
+
64
+ ##
65
+ # @private
66
+ def start
67
+ @listener.start
68
+ self
69
+ end
70
+
71
+ ##
72
+ # Stops the client listening for changes.
73
+ #
74
+ # @example
75
+ # require "google/cloud/firestore"
76
+ #
77
+ # firestore = Google::Cloud::Firestore.new
78
+ #
79
+ # # Get a document reference
80
+ # nyc_ref = firestore.doc "cities/NYC"
81
+ #
82
+ # listener = nyc_ref.listen do |snapshot|
83
+ # puts "The population of #{snapshot[:name]} "
84
+ # puts "is #{snapshot[:population]}."
85
+ # end
86
+ #
87
+ # # When ready, stop the listen operation and close the stream.
88
+ # listener.stop
89
+ #
90
+ def stop
91
+ @listener.stop
92
+ end
93
+
94
+ ##
95
+ # Whether the client has stopped listening for changes.
96
+ #
97
+ # @example
98
+ # require "google/cloud/firestore"
99
+ #
100
+ # firestore = Google::Cloud::Firestore.new
101
+ #
102
+ # # Get a document reference
103
+ # nyc_ref = firestore.doc "cities/NYC"
104
+ #
105
+ # listener = nyc_ref.listen do |snapshot|
106
+ # puts "The population of #{snapshot[:name]} "
107
+ # puts "is #{snapshot[:population]}."
108
+ # end
109
+ #
110
+ # # Checks if the listener is stopped.
111
+ # listener.stopped? #=> false
112
+ #
113
+ # # When ready, stop the listen operation and close the stream.
114
+ # listener.stop
115
+ #
116
+ # # Checks if the listener is stopped.
117
+ # listener.stopped? #=> true
118
+ #
119
+ def stopped?
120
+ @listener.stopped?
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -16,6 +16,7 @@
16
16
  require "google/cloud/firestore/v1beta1"
17
17
  require "google/cloud/firestore/document_snapshot"
18
18
  require "google/cloud/firestore/collection_reference"
19
+ require "google/cloud/firestore/document_listener"
19
20
 
20
21
  module Google
21
22
  module Cloud
@@ -147,6 +148,40 @@ module Google
147
148
  client.get_all([self]).first
148
149
  end
149
150
 
151
+ ##
152
+ # Listen to this document reference for changes.
153
+ #
154
+ # @yield [callback] The block for accessing the document snapshot.
155
+ # @yieldparam [DocumentSnapshot] snapshot A document snapshot.
156
+ #
157
+ # @return [DocumentListener] The ongoing listen operation on the
158
+ # document reference.
159
+ #
160
+ # @example
161
+ # require "google/cloud/firestore"
162
+ #
163
+ # firestore = Google::Cloud::Firestore.new
164
+ #
165
+ # # Get a document reference
166
+ # nyc_ref = firestore.doc "cities/NYC"
167
+ #
168
+ # listener = nyc_ref.listen do |snapshot|
169
+ # puts "The population of #{snapshot[:name]} "
170
+ # puts "is #{snapshot[:population]}."
171
+ # end
172
+ #
173
+ # # When ready, stop the listen operation and close the stream.
174
+ # listener.stop
175
+ #
176
+ def listen &callback
177
+ raise ArgumentError, "callback required" if callback.nil?
178
+
179
+ ensure_client!
180
+
181
+ DocumentListener.new(self, &callback).start
182
+ end
183
+ alias on_snapshot listen
184
+
150
185
  ##
151
186
  # The collection the document reference belongs to.
152
187
  #
@@ -17,6 +17,7 @@ require "google/cloud/firestore/v1beta1"
17
17
  require "google/cloud/firestore/document_reference"
18
18
  require "google/cloud/firestore/collection_reference"
19
19
  require "google/cloud/firestore/convert"
20
+ require "google/cloud/firestore/watch/order"
20
21
 
21
22
  module Google
22
23
  module Cloud
@@ -29,6 +30,9 @@ module Google
29
30
  #
30
31
  # The snapshot can reference a non-existing document.
31
32
  #
33
+ # See {DocumentReference#get}, {DocumentReference#listen},
34
+ # {Query#get}, {Query#listen}, and {QuerySnapshot#docs}.
35
+ #
32
36
  # @example
33
37
  # require "google/cloud/firestore"
34
38
  #
@@ -40,6 +44,22 @@ module Google
40
44
  # # Get the document data
41
45
  # nyc_snap[:population] #=> 1000000
42
46
  #
47
+ # @example Listen to a document reference for changes:
48
+ # require "google/cloud/firestore"
49
+ #
50
+ # firestore = Google::Cloud::Firestore.new
51
+ #
52
+ # # Get a document reference
53
+ # nyc_ref = firestore.doc "cities/NYC"
54
+ #
55
+ # listener = nyc_ref.listen do |snapshot|
56
+ # puts "The population of #{snapshot[:name]} "
57
+ # puts "is #{snapshot[:population]}."
58
+ # end
59
+ #
60
+ # # When ready, stop the listen operation and close the stream.
61
+ # listener.stop
62
+ #
43
63
  class DocumentSnapshot
44
64
  ##
45
65
  # @private The Google::Firestore::V1beta1::Document object.
@@ -118,9 +138,11 @@ module Google
118
138
  # @!group Data
119
139
 
120
140
  ##
121
- # Retrieves the document data.
141
+ # Retrieves the document data. When the document exists the data hash is
142
+ # frozen and will not allow any changes. When the document does not
143
+ # exist `nil` will be returned.
122
144
  #
123
- # @return [Hash] The document data.
145
+ # @return [Hash, nil] The document data.
124
146
  #
125
147
  # @example
126
148
  # require "google/cloud/firestore"
@@ -134,7 +156,7 @@ module Google
134
156
  #
135
157
  def data
136
158
  return nil if missing?
137
- Convert.fields_to_hash grpc.fields, ref.client
159
+ @data ||= Convert.fields_to_hash(grpc.fields, ref.client).freeze
138
160
  end
139
161
  alias fields data
140
162
 
@@ -192,8 +214,9 @@ module Google
192
214
  end
193
215
 
194
216
  nodes = field_path.fields.map(&:to_sym)
195
- selected_data = data
217
+ return ref if nodes == [:__name__]
196
218
 
219
+ selected_data = data
197
220
  nodes.each do |node|
198
221
  unless selected_data.is_a? Hash
199
222
  err_msg = "#{field_path.formatted_string} is not " \
@@ -285,11 +308,46 @@ module Google
285
308
  grpc.nil?
286
309
  end
287
310
 
311
+ ##
312
+ # @private
313
+ def <=> other
314
+ return nil unless other.is_a? DocumentSnapshot
315
+ return data <=> other.data if path == other.path
316
+ path <=> other.path
317
+ end
318
+
319
+ ##
320
+ # @private
321
+ def eql? other
322
+ return nil unless other.is_a? DocumentSnapshot
323
+ return data.eql? other.data if path == other.path
324
+ path.eql? other.path
325
+ end
326
+
327
+ ##
328
+ # @private
329
+ def hash
330
+ @hash ||= [path, data].hash
331
+ end
332
+
333
+ ##
334
+ # @private
335
+ def query_comparisons_for query_grpc
336
+ @memoized_comps ||= {}
337
+ if @memoized_comps.key? query_grpc.hash
338
+ return @memoized_comps[query_grpc.hash]
339
+ end
340
+
341
+ @memoized_comps[query_grpc.hash] = query_grpc.order_by.map do |order|
342
+ Watch::Order.field_comparison get(order.field.field_path)
343
+ end
344
+ end
345
+
288
346
  ##
289
347
  # @private New DocumentSnapshot from a
290
348
  # Google::Firestore::V1beta1::RunQueryResponse object.
291
- def self.from_query_result result, context
292
- ref = DocumentReference.from_path result.document.name, context
349
+ def self.from_query_result result, client
350
+ ref = DocumentReference.from_path result.document.name, client
293
351
  read_at = Convert.timestamp_to_time result.read_time
294
352
 
295
353
  new.tap do |s|
@@ -299,17 +357,30 @@ module Google
299
357
  end
300
358
  end
301
359
 
360
+ ##
361
+ # @private New DocumentSnapshot from a
362
+ # Google::Firestore::V1beta1::DocumentChange object.
363
+ def self.from_document document, client, read_at: nil
364
+ ref = DocumentReference.from_path document.name, client
365
+
366
+ new.tap do |s|
367
+ s.grpc = document
368
+ s.instance_variable_set :@ref, ref
369
+ s.instance_variable_set :@read_at, read_at
370
+ end
371
+ end
372
+
302
373
  ##
303
374
  # @private New DocumentSnapshot from a
304
375
  # Google::Firestore::V1beta1::BatchGetDocumentsResponse object.
305
- def self.from_batch_result result, context
376
+ def self.from_batch_result result, client
306
377
  ref = nil
307
378
  grpc = nil
308
379
  if result.result == :found
309
380
  grpc = result.found
310
- ref = DocumentReference.from_path grpc.name, context
381
+ ref = DocumentReference.from_path grpc.name, client
311
382
  else
312
- ref = DocumentReference.from_path result.missing, context
383
+ ref = DocumentReference.from_path result.missing, client
313
384
  end
314
385
  read_at = Convert.timestamp_to_time result.read_time
315
386
 
@@ -319,6 +390,17 @@ module Google
319
390
  s.instance_variable_set :@read_at, read_at
320
391
  end
321
392
  end
393
+
394
+ ##
395
+ # @private New non-existant DocumentSnapshot from a
396
+ # DocumentReference object.
397
+ def self.missing doc_ref, read_at: nil
398
+ new.tap do |s|
399
+ s.grpc = nil
400
+ s.instance_variable_set :@ref, doc_ref
401
+ s.instance_variable_set :@read_at, read_at
402
+ end
403
+ end
322
404
  end
323
405
  end
324
406
  end
@@ -61,15 +61,15 @@ module Google
61
61
  # user_snap.get(nested_field_path) #=> "Pizza"
62
62
  #
63
63
  def initialize *fields
64
- @fields = fields.flatten.map(&:to_s)
65
- @fields.each do |field|
66
- raise ArgumentError, "empty paths not allowed" if field.empty?
67
- end
64
+ @fields = fields.flatten.map(&:to_s).freeze
65
+
66
+ invalid_fields = @fields.detect(&:empty?)
67
+ raise ArgumentError, "empty paths not allowed" if invalid_fields
68
68
  end
69
69
 
70
70
  ##
71
71
  # @private The individual fields representing the nested field path for
72
- # document data.
72
+ # document data. The fields are frozen.
73
73
  #
74
74
  # @return [Array<String>] The fields.
75
75
  #
@@ -129,9 +129,9 @@ module Google
129
129
  # cities_col = firestore.col "cities"
130
130
  #
131
131
  # # Create a query
132
- # query = cities_col.start_at("NYC").order(
132
+ # query = cities_col.order(
133
133
  # Google::Cloud::Firestore::FieldPath.document_id
134
- # )
134
+ # ).start_at("NYC")
135
135
  #
136
136
  # query.get do |city|
137
137
  # puts "#{city.document_id} has #{city[:population]} residents."
@@ -145,6 +145,8 @@ module Google
145
145
  # @private Creates a field path object representing the nested fields
146
146
  # for document data.
147
147
  #
148
+ # The values are memoized to increase performance.
149
+ #
148
150
  # @param [String] dotted_string A string representing the path of the
149
151
  # document data. The string can represent as a string of individual
150
152
  # fields joined by ".". Fields containing `~`, `*`, `/`, `[`, `]`, and
@@ -166,15 +168,23 @@ module Google
166
168
  # field_path.fields #=> ["favorites", "food"]
167
169
  #
168
170
  def self.parse dotted_string
169
- return new dotted_string if dotted_string.is_a? Array
171
+ # Memoize parsed field paths
172
+ @memoized_field_paths ||= {}
173
+ if @memoized_field_paths.key? dotted_string
174
+ return @memoized_field_paths[dotted_string]
175
+ end
176
+
177
+ if dotted_string.is_a? Array
178
+ return @memoized_field_paths[dotted_string] = new(dotted_string)
179
+ end
170
180
 
171
181
  fields = String(dotted_string).split(".")
172
- fields.each do |field|
173
- if INVALID_FIELD_PATH_CHARS.match field
174
- raise ArgumentError, "invalid character, use FieldPath instead"
175
- end
182
+
183
+ if fields.grep(INVALID_FIELD_PATH_CHARS).any?
184
+ raise ArgumentError, "invalid character, use FieldPath instead"
176
185
  end
177
- new fields
186
+
187
+ @memoized_field_paths[dotted_string] = new(fields)
178
188
  end
179
189
 
180
190
  ##
@@ -15,6 +15,7 @@
15
15
 
16
16
  require "google/cloud/firestore/v1beta1"
17
17
  require "google/cloud/firestore/document_snapshot"
18
+ require "google/cloud/firestore/query_listener"
18
19
  require "google/cloud/firestore/convert"
19
20
 
20
21
  module Google
@@ -40,6 +41,22 @@ module Google
40
41
  # puts "#{city.document_id} has #{city[:population]} residents."
41
42
  # end
42
43
  #
44
+ # @example Listen to a query for changes:
45
+ # require "google/cloud/firestore"
46
+ #
47
+ # firestore = Google::Cloud::Firestore.new
48
+ #
49
+ # # Create a query
50
+ # query = firestore.col(:cities).order(:population, :desc)
51
+ #
52
+ # listener = query.listen do |snapshot|
53
+ # puts "The query snapshot has #{snapshot.docs.count} documents "
54
+ # puts "and has #{snapshot.changes.count} changes."
55
+ # end
56
+ #
57
+ # # When ready, stop the listen operation and close the stream.
58
+ # listener.stop
59
+ #
43
60
  class Query
44
61
  ##
45
62
  # @private The parent path for the query.
@@ -88,13 +105,15 @@ module Google
88
105
  new_query = @query.dup
89
106
  new_query ||= StructuredQuery.new
90
107
 
108
+ fields = Array(fields).flatten.compact
109
+ fields = [FieldPath.document_id] if fields.empty?
91
110
  field_refs = fields.flatten.compact.map do |field|
92
111
  field = FieldPath.parse field unless field.is_a? FieldPath
93
112
  StructuredQuery::FieldReference.new \
94
113
  field_path: field.formatted_string
95
114
  end
96
115
 
97
- new_query.select ||= StructuredQuery::Projection.new
116
+ new_query.select = StructuredQuery::Projection.new
98
117
  field_refs.each do |field_ref|
99
118
  new_query.select.fields << field_ref
100
119
  end
@@ -130,7 +149,7 @@ module Google
130
149
  new_query ||= StructuredQuery.new
131
150
 
132
151
  if new_query.from.empty?
133
- raise "missing collection_id to specify descendants."
152
+ raise "missing collection_id to specify descendants"
134
153
  end
135
154
 
136
155
  new_query.from.last.all_descendants = true
@@ -166,7 +185,7 @@ module Google
166
185
  new_query ||= StructuredQuery.new
167
186
 
168
187
  if new_query.from.empty?
169
- raise "missing collection_id to specify descendants."
188
+ raise "missing collection_id to specify descendants"
170
189
  end
171
190
 
172
191
  new_query.from.last.all_descendants = false
@@ -213,14 +232,18 @@ module Google
213
232
  # end
214
233
  #
215
234
  def where field, operator, value
235
+ if query_has_cursors?
236
+ raise "cannot call where after calling " \
237
+ "start_at, start_after, end_before, or end_at"
238
+ end
239
+
216
240
  new_query = @query.dup
217
241
  new_query ||= StructuredQuery.new
218
242
 
219
243
  field = FieldPath.parse field unless field.is_a? FieldPath
220
244
 
221
- new_query.where ||= default_filter
222
- new_query.where.composite_filter.filters << \
223
- filter(field.formatted_string, operator, value)
245
+ new_filter = filter field.formatted_string, operator, value
246
+ add_filters_to_query new_query, new_filter
224
247
 
225
248
  Query.start new_query, parent_path, client
226
249
  end
@@ -274,6 +297,11 @@ module Google
274
297
  # end
275
298
  #
276
299
  def order field, direction = :asc
300
+ if query_has_cursors?
301
+ raise "cannot call order after calling " \
302
+ "start_at, start_after, end_before, or end_at"
303
+ end
304
+
277
305
  new_query = @query.dup
278
306
  new_query ||= StructuredQuery.new
279
307
 
@@ -355,108 +383,285 @@ module Google
355
383
  end
356
384
 
357
385
  ##
358
- # Starts query results at a set of field values. The result set will
386
+ # Starts query results at a set of field values. The field values can be
387
+ # specified explicitly as arguments, or can be specified implicitly by
388
+ # providing a {DocumentSnapshot} object instead. The result set will
359
389
  # include the document specified by `values`.
360
390
  #
361
391
  # If the current query already has specified `start_at` or
362
392
  # `start_after`, this will overwrite it.
363
393
  #
364
- # The values provided here are for the field paths provides to `order`.
365
- # Values provided to `start_at` without an associated field path
366
- # provided to `order` will result in an error.
394
+ # The values are associated with the field paths that have been provided
395
+ # to `order`, and must match the same sort order. An ArgumentError will
396
+ # be raised if more explicit values are given than are present in
397
+ # `order`.
367
398
  #
368
- # @param [Object, Array<Object>] values The field value to start the
369
- # query at.
399
+ # @param [DocumentSnapshot, Object, Array<Object>] values The field
400
+ # values to start the query at.
370
401
  #
371
402
  # @return [Query] New query with `start_at` called on it.
372
403
  #
373
- # @example
404
+ # @example Starting a query at a document reference id
374
405
  # require "google/cloud/firestore"
375
406
  #
376
407
  # firestore = Google::Cloud::Firestore.new
377
408
  #
378
409
  # # Get a collection reference
379
410
  # cities_col = firestore.col "cities"
411
+ # nyc_doc_id = "NYC"
380
412
  #
381
413
  # # Create a query
382
- # query = cities_col.start_at("NYC").order(firestore.document_id)
414
+ # query = cities_col.order(firestore.document_id)
415
+ # .start_at(nyc_doc_id)
416
+ #
417
+ # query.get do |city|
418
+ # puts "#{city.document_id} has #{city[:population]} residents."
419
+ # end
420
+ #
421
+ # @example Starting a query at a document reference object
422
+ # require "google/cloud/firestore"
423
+ #
424
+ # firestore = Google::Cloud::Firestore.new
425
+ #
426
+ # # Get a collection reference
427
+ # cities_col = firestore.col "cities"
428
+ # nyc_doc_id = "NYC"
429
+ # nyc_ref = cities_col.doc nyc_doc_id
430
+ #
431
+ # # Create a query
432
+ # query = cities_col.order(firestore.document_id)
433
+ # .start_at(nyc_ref)
434
+ #
435
+ # query.get do |city|
436
+ # puts "#{city.document_id} has #{city[:population]} residents."
437
+ # end
438
+ #
439
+ # @example Starting a query at multiple explicit values
440
+ # require "google/cloud/firestore"
441
+ #
442
+ # firestore = Google::Cloud::Firestore.new
443
+ #
444
+ # # Get a collection reference
445
+ # cities_col = firestore.col "cities"
446
+ #
447
+ # # Create a query
448
+ # query = cities_col.order(:population, :desc)
449
+ # .order(:name)
450
+ # .start_at(1000000, "New York City")
451
+ #
452
+ # query.get do |city|
453
+ # puts "#{city.document_id} has #{city[:population]} residents."
454
+ # end
455
+ #
456
+ # @example Starting a query at a DocumentSnapshot
457
+ # require "google/cloud/firestore"
458
+ #
459
+ # firestore = Google::Cloud::Firestore.new
460
+ #
461
+ # # Get a collection reference
462
+ # cities_col = firestore.col "cities"
463
+ #
464
+ # # Get a document snapshot
465
+ # nyc_snap = firestore.doc("cities/NYC").get
466
+ #
467
+ # # Create a query
468
+ # query = cities_col.order(:population, :desc)
469
+ # .order(:name)
470
+ # .start_at(nyc_snap)
383
471
  #
384
472
  # query.get do |city|
385
473
  # puts "#{city.document_id} has #{city[:population]} residents."
386
474
  # end
387
475
  #
388
476
  def start_at *values
477
+ raise ArgumentError, "must provide values" if values.empty?
478
+
389
479
  new_query = @query.dup
390
480
  new_query ||= StructuredQuery.new
391
481
 
392
- values = values.flatten.map { |value| Convert.raw_to_value value }
393
- new_query.start_at = Google::Firestore::V1beta1::Cursor.new(
394
- values: values, before: true
395
- )
482
+ cursor = values_to_cursor values, new_query
483
+ cursor.before = true
484
+ new_query.start_at = cursor
396
485
 
397
486
  Query.start new_query, parent_path, client
398
487
  end
399
488
 
400
489
  ##
401
- # Starts query results after a set of field values. The result set will
490
+ # Starts query results after a set of field values. The field values can
491
+ # be specified explicitly as arguments, or can be specified implicitly
492
+ # by providing a {DocumentSnapshot} object instead. The result set will
402
493
  # not include the document specified by `values`.
403
494
  #
404
495
  # If the current query already has specified `start_at` or
405
496
  # `start_after`, this will overwrite it.
406
497
  #
407
- # The values provided here are for the field paths provides to `order`.
408
- # Values provided to `start_after` without an associated field path
409
- # provided to `order` will result in an error.
498
+ # The values are associated with the field paths that have been provided
499
+ # to `order`, and must match the same sort order. An ArgumentError will
500
+ # be raised if more explicit values are given than are present in
501
+ # `order`.
410
502
  #
411
- # @param [Object, Array<Object>] values The field value to start the
412
- # query after.
503
+ # @param [DocumentSnapshot, Object, Array<Object>] values The field
504
+ # values to start the query after.
413
505
  #
414
506
  # @return [Query] New query with `start_after` called on it.
415
507
  #
416
- # @example
508
+ # @example Starting a query after a document reference id
509
+ # require "google/cloud/firestore"
510
+ #
511
+ # firestore = Google::Cloud::Firestore.new
512
+ #
513
+ # # Get a collection reference
514
+ # cities_col = firestore.col "cities"
515
+ # nyc_doc_id = "NYC"
516
+ #
517
+ # # Create a query
518
+ # query = cities_col.order(firestore.document_id)
519
+ # .start_after(nyc_doc_id)
520
+ #
521
+ # query.get do |city|
522
+ # puts "#{city.document_id} has #{city[:population]} residents."
523
+ # end
524
+ #
525
+ # @example Starting a query after a document reference object
417
526
  # require "google/cloud/firestore"
418
527
  #
419
528
  # firestore = Google::Cloud::Firestore.new
420
529
  #
421
530
  # # Get a collection reference
422
531
  # cities_col = firestore.col "cities"
532
+ # nyc_doc_id = "NYC"
533
+ # nyc_ref = cities_col.doc nyc_doc_id
423
534
  #
424
535
  # # Create a query
425
- # query = cities_col.start_after("NYC").order(firestore.document_id)
536
+ # query = cities_col.order(firestore.document_id)
537
+ # .start_after(nyc_ref)
538
+ #
539
+ # query.get do |city|
540
+ # puts "#{city.document_id} has #{city[:population]} residents."
541
+ # end
542
+ #
543
+ # @example Starting a query after multiple explicit values
544
+ # require "google/cloud/firestore"
545
+ #
546
+ # firestore = Google::Cloud::Firestore.new
547
+ #
548
+ # # Get a collection reference
549
+ # cities_col = firestore.col "cities"
550
+ #
551
+ # # Create a query
552
+ # query = cities_col.order(:population, :desc)
553
+ # .order(:name)
554
+ # .start_after(1000000, "New York City")
555
+ #
556
+ # query.get do |city|
557
+ # puts "#{city.document_id} has #{city[:population]} residents."
558
+ # end
559
+ #
560
+ # @example Starting a query after a DocumentSnapshot
561
+ # require "google/cloud/firestore"
562
+ #
563
+ # firestore = Google::Cloud::Firestore.new
564
+ #
565
+ # # Get a collection reference
566
+ # cities_col = firestore.col "cities"
567
+ #
568
+ # # Get a document snapshot
569
+ # nyc_snap = firestore.doc("cities/NYC").get
570
+ #
571
+ # # Create a query
572
+ # query = cities_col.order(:population, :desc)
573
+ # .order(:name)
574
+ # .start_after(nyc_snap)
426
575
  #
427
576
  # query.get do |city|
428
577
  # puts "#{city.document_id} has #{city[:population]} residents."
429
578
  # end
430
579
  #
431
580
  def start_after *values
581
+ raise ArgumentError, "must provide values" if values.empty?
582
+
432
583
  new_query = @query.dup
433
584
  new_query ||= StructuredQuery.new
434
585
 
435
- values = values.flatten.map { |value| Convert.raw_to_value value }
436
- new_query.start_at = Google::Firestore::V1beta1::Cursor.new(
437
- values: values, before: false
438
- )
586
+ cursor = values_to_cursor values, new_query
587
+ cursor.before = false
588
+ new_query.start_at = cursor
439
589
 
440
590
  Query.start new_query, parent_path, client
441
591
  end
442
592
 
443
593
  ##
444
- # Ends query results before a set of field values. The result set will
594
+ # Ends query results before a set of field values. The field values can
595
+ # be specified explicitly as arguments, or can be specified implicitly
596
+ # by providing a {DocumentSnapshot} object instead. The result set will
445
597
  # not include the document specified by `values`.
446
598
  #
447
599
  # If the current query already has specified `end_before` or
448
600
  # `end_at`, this will overwrite it.
449
601
  #
450
- # The values provided here are for the field paths provides to `order`.
451
- # Values provided to `end_before` without an associated field path
452
- # provided to `order` will result in an error.
602
+ # The values are associated with the field paths that have been provided
603
+ # to `order`, and must match the same sort order. An ArgumentError will
604
+ # be raised if more explicit values are given than are present in
605
+ # `order`.
453
606
  #
454
- # @param [Object, Array<Object>] values The field value to end the query
455
- # before.
607
+ # @param [DocumentSnapshot, Object, Array<Object>] values The field
608
+ # values to end the query before.
456
609
  #
457
610
  # @return [Query] New query with `end_before` called on it.
458
611
  #
459
- # @example
612
+ # @example Ending a query before a document reference id
613
+ # require "google/cloud/firestore"
614
+ #
615
+ # firestore = Google::Cloud::Firestore.new
616
+ #
617
+ # # Get a collection reference
618
+ # cities_col = firestore.col "cities"
619
+ # nyc_doc_id = "NYC"
620
+ #
621
+ # # Create a query
622
+ # query = cities_col.order(firestore.document_id)
623
+ # .end_before(nyc_doc_id)
624
+ #
625
+ # query.get do |city|
626
+ # puts "#{city.document_id} has #{city[:population]} residents."
627
+ # end
628
+ #
629
+ # @example Ending a query before a document reference object
630
+ # require "google/cloud/firestore"
631
+ #
632
+ # firestore = Google::Cloud::Firestore.new
633
+ #
634
+ # # Get a collection reference
635
+ # cities_col = firestore.col "cities"
636
+ # nyc_doc_id = "NYC"
637
+ # nyc_ref = cities_col.doc nyc_doc_id
638
+ #
639
+ # # Create a query
640
+ # query = cities_col.order(firestore.document_id)
641
+ # .end_before(nyc_ref)
642
+ #
643
+ # query.get do |city|
644
+ # puts "#{city.document_id} has #{city[:population]} residents."
645
+ # end
646
+ #
647
+ # @example Ending a query before multiple explicit values
648
+ # require "google/cloud/firestore"
649
+ #
650
+ # firestore = Google::Cloud::Firestore.new
651
+ #
652
+ # # Get a collection reference
653
+ # cities_col = firestore.col "cities"
654
+ #
655
+ # # Create a query
656
+ # query = cities_col.order(:population, :desc)
657
+ # .order(:name)
658
+ # .end_before(1000000, "New York City")
659
+ #
660
+ # query.get do |city|
661
+ # puts "#{city.document_id} has #{city[:population]} residents."
662
+ # end
663
+ #
664
+ # @example Ending a query before a DocumentSnapshot
460
665
  # require "google/cloud/firestore"
461
666
  #
462
667
  # firestore = Google::Cloud::Firestore.new
@@ -464,64 +669,131 @@ module Google
464
669
  # # Get a collection reference
465
670
  # cities_col = firestore.col "cities"
466
671
  #
672
+ # # Get a document snapshot
673
+ # nyc_snap = firestore.doc("cities/NYC").get
674
+ #
467
675
  # # Create a query
468
- # query = cities_col.end_before("NYC").order(firestore.document_id)
676
+ # query = cities_col.order(:population, :desc)
677
+ # .order(:name)
678
+ # .end_before(nyc_snap)
469
679
  #
470
680
  # query.get do |city|
471
681
  # puts "#{city.document_id} has #{city[:population]} residents."
472
682
  # end
473
683
  #
474
684
  def end_before *values
685
+ raise ArgumentError, "must provide values" if values.empty?
686
+
475
687
  new_query = @query.dup
476
688
  new_query ||= StructuredQuery.new
477
689
 
478
- values = values.flatten.map { |value| Convert.raw_to_value value }
479
- new_query.end_at = Google::Firestore::V1beta1::Cursor.new(
480
- values: values, before: true
481
- )
690
+ cursor = values_to_cursor values, new_query
691
+ cursor.before = true
692
+ new_query.end_at = cursor
482
693
 
483
694
  Query.start new_query, parent_path, client
484
695
  end
485
696
 
486
697
  ##
487
- # Ends query results at a set of field values. The result set will
698
+ # Ends query results at a set of field values. The field values can
699
+ # be specified explicitly as arguments, or can be specified implicitly
700
+ # by providing a {DocumentSnapshot} object instead. The result set will
488
701
  # include the document specified by `values`.
489
702
  #
490
703
  # If the current query already has specified `end_before` or
491
704
  # `end_at`, this will overwrite it.
492
705
  #
493
- # The values provided here are for the field paths provides to `order`.
494
- # Values provided to `end_at` without an associated field path provided
495
- # to `order` will result in an error.
706
+ # The values are associated with the field paths that have been provided
707
+ # to `order`, and must match the same sort order. An ArgumentError will
708
+ # be raised if more explicit values are given than are present in
709
+ # `order`.
496
710
  #
497
- # @param [Object, Array<Object>] values The field value to end the query
498
- # at.
711
+ # @param [DocumentSnapshot, Object, Array<Object>] values The field
712
+ # values to end the query at.
499
713
  #
500
714
  # @return [Query] New query with `end_at` called on it.
501
715
  #
502
- # @example
716
+ # @example Ending a query at a document reference id
503
717
  # require "google/cloud/firestore"
504
718
  #
505
719
  # firestore = Google::Cloud::Firestore.new
506
720
  #
507
721
  # # Get a collection reference
508
722
  # cities_col = firestore.col "cities"
723
+ # nyc_doc_id = "NYC"
509
724
  #
510
725
  # # Create a query
511
- # query = cities_col.end_at("NYC").order(firestore.document_id)
726
+ # query = cities_col.order(firestore.document_id)
727
+ # .end_at(nyc_doc_id)
728
+ #
729
+ # query.get do |city|
730
+ # puts "#{city.document_id} has #{city[:population]} residents."
731
+ # end
732
+ #
733
+ # @example Ending a query at a document reference object
734
+ # require "google/cloud/firestore"
735
+ #
736
+ # firestore = Google::Cloud::Firestore.new
737
+ #
738
+ # # Get a collection reference
739
+ # cities_col = firestore.col "cities"
740
+ # nyc_doc_id = "NYC"
741
+ # nyc_ref = cities_col.doc nyc_doc_id
742
+ #
743
+ # # Create a query
744
+ # query = cities_col.order(firestore.document_id)
745
+ # .end_at(nyc_ref)
746
+ #
747
+ # query.get do |city|
748
+ # puts "#{city.document_id} has #{city[:population]} residents."
749
+ # end
750
+ #
751
+ # @example Ending a query at multiple explicit values
752
+ # require "google/cloud/firestore"
753
+ #
754
+ # firestore = Google::Cloud::Firestore.new
755
+ #
756
+ # # Get a collection reference
757
+ # cities_col = firestore.col "cities"
758
+ #
759
+ # # Create a query
760
+ # query = cities_col.order(:population, :desc)
761
+ # .order(:name)
762
+ # .end_at(1000000, "New York City")
763
+ #
764
+ # query.get do |city|
765
+ # puts "#{city.document_id} has #{city[:population]} residents."
766
+ # end
767
+ #
768
+ # @example Ending a query at a DocumentSnapshot
769
+ # require "google/cloud/firestore"
770
+ #
771
+ # firestore = Google::Cloud::Firestore.new
772
+ #
773
+ # # Get a collection reference
774
+ # cities_col = firestore.col "cities"
775
+ #
776
+ # # Get a document snapshot
777
+ # nyc_snap = firestore.doc("cities/NYC").get
778
+ #
779
+ # # Create a query
780
+ # query = cities_col.order(:population, :desc)
781
+ # .order(:name)
782
+ # .end_at(nyc_snap)
512
783
  #
513
784
  # query.get do |city|
514
785
  # puts "#{city.document_id} has #{city[:population]} residents."
515
786
  # end
516
787
  #
517
788
  def end_at *values
789
+ raise ArgumentError, "must provide values" if values.empty?
790
+
518
791
  new_query = @query.dup
519
792
  new_query ||= StructuredQuery.new
520
793
 
521
- values = values.flatten.map { |value| Convert.raw_to_value value }
522
- new_query.end_at = Google::Firestore::V1beta1::Cursor.new(
523
- values: values, before: false
524
- )
794
+ cursor = values_to_cursor values, new_query
795
+ cursor.before = false
796
+ new_query.end_at = cursor
525
797
 
526
798
  Query.start new_query, parent_path, client
527
799
  end
@@ -552,7 +824,7 @@ module Google
552
824
  def get
553
825
  ensure_service!
554
826
 
555
- return enum_for(:run) unless block_given?
827
+ return enum_for(:get) unless block_given?
556
828
 
557
829
  results = service.run_query parent_path, @query
558
830
  results.each do |result|
@@ -562,6 +834,39 @@ module Google
562
834
  end
563
835
  alias run get
564
836
 
837
+ ##
838
+ # Listen to this query for changes.
839
+ #
840
+ # @yield [callback] The block for accessing the query snapshot.
841
+ # @yieldparam [QuerySnapshot] snapshot A query snapshot.
842
+ #
843
+ # @return [QueryListener] The ongoing listen operation on the query.
844
+ #
845
+ # @example
846
+ # require "google/cloud/firestore"
847
+ #
848
+ # firestore = Google::Cloud::Firestore.new
849
+ #
850
+ # # Create a query
851
+ # query = firestore.col(:cities).order(:population, :desc)
852
+ #
853
+ # listener = query.listen do |snapshot|
854
+ # puts "The query snapshot has #{snapshot.docs.count} documents "
855
+ # puts "and has #{snapshot.changes.count} changes."
856
+ # end
857
+ #
858
+ # # When ready, stop the listen operation and close the stream.
859
+ # listener.stop
860
+ #
861
+ def listen &callback
862
+ raise ArgumentError, "callback required" if callback.nil?
863
+
864
+ ensure_service!
865
+
866
+ QueryListener.new(self, &callback).start
867
+ end
868
+ alias on_snapshot listen
869
+
565
870
  ##
566
871
  # @private Start a new Query.
567
872
  def self.start query, parent_path, client
@@ -608,41 +913,180 @@ module Google
608
913
 
609
914
  def filter name, op, value
610
915
  field = StructuredQuery::FieldReference.new field_path: name.to_s
611
- op = FILTER_OPS[op.to_s.downcase] || :EQUAL
916
+ operator = FILTER_OPS[op.to_s.downcase]
917
+ raise ArgumentError, "unknown operator #{op}" if operator.nil?
612
918
 
613
919
  is_value_nan = value.respond_to?(:nan?) && value.nan?
614
920
  if UNARY_VALUES.include?(value) || is_value_nan
615
- if op != :EQUAL
921
+ if operator != :EQUAL
616
922
  raise ArgumentError,
617
- "can only check equality for #{value} values."
923
+ "can only check equality for #{value} values"
618
924
  end
619
925
 
620
- op = :IS_NULL
621
- op = :IS_NAN if UNARY_NAN_VALUES.include?(value) || is_value_nan
926
+ operator = :IS_NULL
927
+ if UNARY_NAN_VALUES.include?(value) || is_value_nan
928
+ operator = :IS_NAN
929
+ end
622
930
 
623
931
  return StructuredQuery::Filter.new(unary_filter:
624
- StructuredQuery::UnaryFilter.new(field: field, op: op))
932
+ StructuredQuery::UnaryFilter.new(field: field, op: operator))
625
933
  end
626
934
 
627
935
  value = Convert.raw_to_value value
628
936
  StructuredQuery::Filter.new(field_filter:
629
- StructuredQuery::FieldFilter.new(field: field, op: op,
937
+ StructuredQuery::FieldFilter.new(field: field, op: operator,
630
938
  value: value))
631
939
  end
632
940
 
633
- def default_filter
941
+ def composite_filter
634
942
  StructuredQuery::Filter.new(composite_filter:
635
943
  StructuredQuery::CompositeFilter.new(op: :AND))
636
944
  end
637
945
 
638
- def order_direction direction
639
- if direction.to_s.downcase.start_with? "a"
640
- :ASCENDING
641
- elsif direction.to_s.downcase.start_with? "d"
642
- :DESCENDING
946
+ def add_filters_to_query query, filter
947
+ if query.where.nil?
948
+ query.where = filter
949
+ elsif query.where.filter_type == :composite_filter
950
+ query.where.composite_filter.filters << filter
643
951
  else
644
- :DIRECTION_UNSPECIFIED
952
+ old_filter = query.where
953
+ query.where = composite_filter
954
+ query.where.composite_filter.filters << old_filter
955
+ query.where.composite_filter.filters << filter
956
+ end
957
+ end
958
+
959
+ def order_direction direction
960
+ return :DESCENDING if direction.to_s.downcase.start_with? "d".freeze
961
+ :ASCENDING
962
+ end
963
+
964
+ def query_has_cursors?
965
+ query.start_at || query.end_at
966
+ end
967
+
968
+ def values_to_cursor values, query
969
+ if values.count == 1 && values.first.is_a?(DocumentSnapshot)
970
+ return snapshot_to_cursor(values.first, query)
971
+ end
972
+
973
+ # pair values with their field_paths to ensure correct formatting
974
+ order_field_paths = order_by_field_paths query
975
+ if values.count > order_field_paths.count
976
+ # raise if too many values provided for the cursor
977
+ raise ArgumentError, "too many values"
978
+ end
979
+
980
+ values = values.zip(order_field_paths).map do |value, field_path|
981
+ if field_path == doc_id_path && !value.is_a?(DocumentReference)
982
+ value = document_reference value
983
+ end
984
+ Convert.raw_to_value value
985
+ end
986
+
987
+ Google::Firestore::V1beta1::Cursor.new values: values
988
+ end
989
+
990
+ def snapshot_to_cursor snapshot, query
991
+ if snapshot.parent.path != query_collection_path
992
+ raise ArgumentError, "cursor snapshot must belong to collection"
993
+ end
994
+
995
+ # first, add any inequality filters missing from existing order_by
996
+ ensure_inequality_field_paths_in_order_by! query
997
+
998
+ # second, make sure __name__ is present in order_by
999
+ ensure_document_id_in_order_by! query
1000
+
1001
+ # lastly, create cursor for all field_paths in order_by
1002
+ values = order_by_field_paths(query).map do |field_path|
1003
+ if field_path == doc_id_path
1004
+ snapshot.ref
1005
+ else
1006
+ snapshot[field_path]
1007
+ end
645
1008
  end
1009
+
1010
+ values_to_cursor values, query
1011
+ end
1012
+
1013
+ def ensure_inequality_field_paths_in_order_by! query
1014
+ inequality_paths = inequality_filter_field_paths query
1015
+ orig_order = order_by_field_paths query
1016
+
1017
+ inequality_paths.reverse.each do |field_path|
1018
+ next if orig_order.include? field_path
1019
+
1020
+ query.order_by.unshift StructuredQuery::Order.new(
1021
+ field: StructuredQuery::FieldReference.new(
1022
+ field_path: field_path
1023
+ ),
1024
+ direction: :ASCENDING
1025
+ )
1026
+ end
1027
+ end
1028
+
1029
+ def ensure_document_id_in_order_by! query
1030
+ return if order_by_field_paths(query).include? doc_id_path
1031
+
1032
+ query.order_by.push StructuredQuery::Order.new(
1033
+ field: StructuredQuery::FieldReference.new(
1034
+ field_path: doc_id_path
1035
+ ),
1036
+ direction: last_order_direction(query)
1037
+ )
1038
+ end
1039
+
1040
+ def inequality_filter_field_paths query
1041
+ return [] if query.where.nil?
1042
+
1043
+ # The way we construct a query, where is always a CompositeFilter
1044
+ filters = if query.where.filter_type == :composite_filter
1045
+ query.where.composite_filter.filters
1046
+ else
1047
+ [query.where]
1048
+ end
1049
+ ineq_filters = filters.select do |filter|
1050
+ if filter.filter_type == :field_filter
1051
+ filter.field_filter.op != :EQUAL
1052
+ end
1053
+ end
1054
+ ineq_filters.map { |filter| filter.field_filter.field.field_path }
1055
+ end
1056
+
1057
+ def order_by_field_paths query
1058
+ query.order_by.map { |order_by| order_by.field.field_path }
1059
+ end
1060
+
1061
+ def last_order_direction query
1062
+ last_order_by = query.order_by.last
1063
+ return :ASCENDING if last_order_by.nil?
1064
+ last_order_by.direction
1065
+ end
1066
+
1067
+ def document_reference document_path
1068
+ if document_path.to_s.split("/").count.even?
1069
+ raise ArgumentError, "document_path must refer to a document"
1070
+ end
1071
+
1072
+ DocumentReference.from_path(
1073
+ "#{query_collection_path}/#{document_path}", client
1074
+ )
1075
+ end
1076
+
1077
+ def query_collection_path
1078
+ "#{parent_path}/#{query_collection_id}"
1079
+ end
1080
+
1081
+ def query_collection_id
1082
+ # We trust that query.from is always set, since Query cannot be
1083
+ # created without it.
1084
+ return nil if query.from.empty?
1085
+ query.from.first.collection_id
1086
+ end
1087
+
1088
+ def doc_id_path
1089
+ "__name__".freeze
646
1090
  end
647
1091
 
648
1092
  ##