google-cloud-firestore 3.0.0 → 3.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 308b67067868ac77277cef71c5d5fffcb80683344cf9e4e53c82dd7f94d0214e
4
- data.tar.gz: 18549006ffbf3d7c31d691cd5aa08aca8365ce2462ae3459cc23e423e7017b59
3
+ metadata.gz: 791bfbf175ebfc435afa810200d9345d6faa8de259687c1013f31b775cd3c704
4
+ data.tar.gz: ad58d22b57213e43771e4c22ad17555120e99a63419a9a50c5a7571e89a08d00
5
5
  SHA512:
6
- metadata.gz: a73c2637872557c0c295e5513d6f95f1e51271f1881ebcbce1b21797239f4a19e0672966e05bd1bfce01d6766c96760579a04c63db5c6b21a1dfb42e7159d7ee
7
- data.tar.gz: 597ae0aa8b48466143524529533e8ebe5cc8512117550432cf1456c890d6cec98e3e60fe351dda4d9cbb3e00ceb778e537e0f1d04d952d0efb7bd4b1b7181334
6
+ metadata.gz: 4f45cf2d0a38b4bfb799842a4feac370d7f7d06b27d3cb9104e49f2fb2d3be76357e8d5c2191ab8a8b313ac77b23aacb8ddd50f9a714bb63a04c022ffa82124b
7
+ data.tar.gz: 7a4e35af0173d3a4bed5f70fb77d5c27a11bcb143e30f0f75db49059f1c89f85239640ebee68a5c36a149bcefc97961a9a11e8dbd734a77dfe948bcddbb5d311
data/.yardopts CHANGED
@@ -11,6 +11,7 @@ OVERVIEW.md
11
11
  AUTHENTICATION.md
12
12
  EMULATOR.md
13
13
  LOGGING.md
14
+ QUERY_PERFORMANCE.md
14
15
  CONTRIBUTING.md
15
16
  TROUBLESHOOTING.md
16
17
  CHANGELOG.md
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Release History
2
2
 
3
+ ### 3.1.0 (2025-08-07)
4
+
5
+ #### Features
6
+
7
+ * add AggregateQuery explanation features for RubyFirestore ([#30484](https://github.com/googleapis/google-cloud-ruby/issues/30484))
8
+ * add Query explanation features for Ruby Firestore ([#29469](https://github.com/googleapis/google-cloud-ruby/issues/29469))
9
+ * update routing headers to new format ([#30481](https://github.com/googleapis/google-cloud-ruby/issues/30481))
10
+
3
11
  ### 3.0.0 (2025-03-10)
4
12
 
5
13
  ### ⚠ BREAKING CHANGES
@@ -14,6 +14,7 @@
14
14
 
15
15
  require "google/cloud/firestore/v1"
16
16
  require "google/cloud/firestore/aggregate_query_snapshot"
17
+ require "google/cloud/firestore/aggregate_query_explain_result"
17
18
 
18
19
  module Google
19
20
  module Cloud
@@ -252,6 +253,60 @@ module Google
252
253
  end
253
254
  end
254
255
 
256
+ ##
257
+ # Retrieves the query explanation for the aggregate query.
258
+ # By default, the query is only planned, not executed, returning only metrics from the
259
+ # planning stages. If `analyze` is set to `true` the query will be planned and executed,
260
+ # returning the `AggregateQuerySnapshot` alongside both planning and execution stage metrics.
261
+ #
262
+ # Unlike the enumerator returned from `AggregateQuery#get`, the `AggregateQueryExplainResult`
263
+ # caches its snapshot and metrics after the first access.
264
+ #
265
+ # @param [Boolean] analyze
266
+ # Whether to execute the query and return the execution stage metrics
267
+ # in addition to planning metrics.
268
+ # If set to `false` the query will be planned only and will return planning
269
+ # stage metrics without results.
270
+ # If set to `true` the query will be executed, and will return the query results,
271
+ # planning stage metrics, and execution stage metrics.
272
+ # Defaults to `false`.
273
+ #
274
+ # @return [AggregateQueryExplainResult]
275
+ #
276
+ # @example Getting only the planning stage metrics for the aggregate query
277
+ # require "google/cloud/firestore"
278
+ #
279
+ # firestore = Google::Cloud::Firestore.new
280
+ # query = firestore.col(:cities).aggregate_query.add_count
281
+ #
282
+ # explain_result = query.explain
283
+ # metrics = explain_result.explain_metrics
284
+ # puts "Plan summary: #{metrics.plan_summary}" if metrics&.plan_summary
285
+ #
286
+ # @example Getting planning and execution stage metrics, as well as aggregate query results
287
+ # require "google/cloud/firestore"
288
+ #
289
+ # firestore = Google::Cloud::Firestore.new
290
+ # query = firestore.col(:cities).aggregate_query.add_count
291
+ #
292
+ # explain_result = query.explain analyze: true
293
+ # metrics = explain_result.explain_metrics
294
+ # puts "Plan summary: #{metrics.plan_summary}" if metrics&.plan_summary
295
+ # puts "Results returned: #{metrics.execution_stats.results_returned}" if metrics&.execution_stats
296
+ # snapshot = explain_result.snapshot
297
+ # puts "Count: #{snapshot.get}" if snapshot
298
+ #
299
+ def explain analyze: false
300
+ ensure_service!
301
+ validate_analyze_option! analyze
302
+
303
+ explain_options = ::Google::Cloud::Firestore::V1::ExplainOptions.new analyze: analyze
304
+
305
+ responses_enum = service.run_aggregate_query @parent_path, @grpc, explain_options: explain_options
306
+
307
+ AggregateQueryExplainResult.new responses_enum
308
+ end
309
+
255
310
  ##
256
311
  # @private
257
312
  def to_grpc
@@ -279,6 +334,13 @@ module Google
279
334
  ensure_client!
280
335
  client.service
281
336
  end
337
+
338
+ # @private
339
+ # Validates the analyze option.
340
+ def validate_analyze_option! analyze_value
341
+ return if [true, false].include? analyze_value
342
+ raise ArgumentError, "analyze must be a boolean"
343
+ end
282
344
  end
283
345
  end
284
346
  end
@@ -0,0 +1,152 @@
1
+ # Copyright 2025 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
+ require "google/cloud/firestore/aggregate_query_snapshot"
16
+ require "google/cloud/firestore/convert"
17
+ require "google/cloud/firestore/v1" # For ExplainMetrics and AggregationResult types
18
+
19
+ module Google
20
+ module Cloud
21
+ module Firestore
22
+ ##
23
+ # # AggregateQueryExplainResult
24
+ #
25
+ # Represents the result of a Firestore aggregate query explanation.
26
+ # This class provides access to the
27
+ # {Google::Cloud::Firestore::V1::ExplainMetrics}, which contain details
28
+ # about the query plan and execution statistics. If the explanation was
29
+ # run with `analyze: true`, it also provides access to the
30
+ # {AggregateQuerySnapshot}.
31
+ #
32
+ # The metrics and snapshot (if applicable) are fetched and cached upon
33
+ # the first call to either {#explain_metrics} or {#snapshot}.
34
+ #
35
+ # @see AggregateQuery#explain
36
+ #
37
+ # @example Getting planning and execution metrics with the snapshot
38
+ # require "google/cloud/firestore"
39
+ #
40
+ # firestore = Google::Cloud::Firestore.new
41
+ # aggregate_query = firestore.col(:cities).aggregate_query.add_count
42
+ #
43
+ # explain_result = aggregate_query.explain analyze: true
44
+ #
45
+ # metrics = explain_result.explain_metrics
46
+ # if metrics
47
+ # puts "Plan summary: #{metrics.plan_summary&.to_json}"
48
+ # puts "Execution stats: #{metrics.execution_stats&.to_json}"
49
+ # end
50
+ #
51
+ # snapshot = explain_result.snapshot
52
+ # puts "Count: #{snapshot.get}" if snapshot
53
+ #
54
+ # @example Getting only planning metrics
55
+ # require "google/cloud/firestore"
56
+ #
57
+ # firestore = Google::Cloud::Firestore.new
58
+ # aggregate_query = firestore.col(:cities).aggregate_query.add_count
59
+ #
60
+ # explain_result = aggregate_query.explain analyze: false # Default
61
+ #
62
+ # metrics = explain_result.explain_metrics
63
+ # puts "Plan summary: #{metrics.plan_summary&.to_json}" if metrics
64
+ #
65
+ # # Snapshot will be nil because analyze was false
66
+ # puts "Snapshot is nil: #{explain_result.snapshot.nil?}"
67
+ #
68
+ class AggregateQueryExplainResult
69
+ # Indicates whether the metrics and snapshot (if applicable) have been
70
+ # fetched from the server response and cached.
71
+ # This becomes `true` after the first call to {#explain_metrics} or
72
+ # {#snapshot}.
73
+ #
74
+ # @return [Boolean] `true` if data has been fetched, `false` otherwise.
75
+ attr_reader :metrics_fetched
76
+ alias metrics_fetched? metrics_fetched
77
+
78
+ ##
79
+ # @private Creates a new AggregateQueryExplainResult.
80
+ #
81
+ # @param responses_enum [Enumerable<Google::Cloud::Firestore::V1::RunAggregationQueryResponse>]
82
+ # The enum of response objects from the gRPC call.
83
+ #
84
+ def initialize responses_enum
85
+ @responses_enum = responses_enum
86
+
87
+ @metrics_fetched = false
88
+ @explain_metrics = nil
89
+ @snapshot = nil
90
+ end
91
+
92
+ # The metrics from planning and potentially execution stages of the
93
+ # aggregate query.
94
+ #
95
+ # Calling this method for the first time will process the server
96
+ # responses to extract and cache the metrics (and snapshot if
97
+ # `analyze: true` was used). Subsequent calls return the cached metrics.
98
+ #
99
+ # @return [Google::Cloud::Firestore::V1::ExplainMetrics, nil]
100
+ # The query explanation metrics, or `nil` if no metrics were returned
101
+ # by the server.
102
+ #
103
+ def explain_metrics
104
+ ensure_fetched!
105
+ @explain_metrics
106
+ end
107
+
108
+ # The {AggregateQuerySnapshot} containing the aggregation results.
109
+ #
110
+ # This is only available if the explanation was run with `analyze: true`.
111
+ # If `analyze: false` was used, or if the query yielded no results
112
+ # even with `analyze: true`, this method returns `nil`.
113
+ #
114
+ # Calling this method for the first time will process the server
115
+ # responses to extract and cache the snapshot (and metrics).
116
+ # Subsequent calls return the cached snapshot.
117
+ #
118
+ # @return [AggregateQuerySnapshot, nil]
119
+ # The aggregate query snapshot if `analyze: true` was used and results
120
+ # are available, otherwise `nil`.
121
+ #
122
+ def snapshot
123
+ ensure_fetched!
124
+ @snapshot
125
+ end
126
+
127
+ private
128
+
129
+ # Processes the responses from the server to populate metrics and,
130
+ # if applicable, the snapshot. This method is called internally by
131
+ # {#explain_metrics} and {#snapshot} and ensures that processing
132
+ # happens only once.
133
+ # @private
134
+ def ensure_fetched!
135
+ return if @metrics_fetched
136
+
137
+ @responses_enum.each do |response|
138
+ if @explain_metrics.nil? && response.explain_metrics
139
+ @explain_metrics = response.explain_metrics
140
+ end
141
+
142
+ if @snapshot.nil? && response.result
143
+ @snapshot = AggregateQuerySnapshot.from_run_aggregate_query_response response
144
+ end
145
+ end
146
+
147
+ @metrics_fetched = true
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -20,6 +20,7 @@ require "google/cloud/firestore/convert"
20
20
  require "google/cloud/firestore/aggregate_query"
21
21
  require "google/cloud/firestore/filter"
22
22
  require "json"
23
+ require "google/cloud/firestore/query_explain_result"
23
24
 
24
25
  module Google
25
26
  module Cloud
@@ -79,6 +80,12 @@ module Google
79
80
 
80
81
  ##
81
82
  # @private Creates a new Query.
83
+ #
84
+ # @param [Google::Cloud::Firestore::V1::StructuredQuery] query The structured query object.
85
+ # @param [String] parent_path The parent path for the query.
86
+ # @param [Google::Cloud::Firestore::Client] client The firestore client object.
87
+ # @param [Symbol] limit_type (Optional) The type of limit query (:first or :last).
88
+ # Defaults to `nil` if not provided.
82
89
  def initialize query, parent_path, client, limit_type: nil
83
90
  query ||= StructuredQuery.new
84
91
  @query = query
@@ -135,7 +142,7 @@ module Google
135
142
  new_query.select.fields << field_ref
136
143
  end
137
144
 
138
- Query.start new_query, parent_path, client, limit_type: limit_type
145
+ start_new_query new_query
139
146
  end
140
147
 
141
148
  ##
@@ -171,7 +178,7 @@ module Google
171
178
 
172
179
  new_query.from.last.all_descendants = true
173
180
 
174
- Query.start new_query, parent_path, client, limit_type: limit_type
181
+ start_new_query new_query
175
182
  end
176
183
 
177
184
  ##
@@ -207,7 +214,7 @@ module Google
207
214
 
208
215
  new_query.from.last.all_descendants = false
209
216
 
210
- Query.start new_query, parent_path, client, limit_type: limit_type
217
+ start_new_query new_query
211
218
  end
212
219
 
213
220
  ##
@@ -305,7 +312,7 @@ module Google
305
312
  add_filters_to_query new_query, new_filter.filter
306
313
  end
307
314
 
308
- Query.start new_query, parent_path, client, limit_type: limit_type
315
+ start_new_query new_query
309
316
  end
310
317
 
311
318
  ##
@@ -373,7 +380,7 @@ module Google
373
380
  direction: order_direction(direction)
374
381
  )
375
382
 
376
- Query.start new_query, parent_path, client, limit_type: limit_type
383
+ start_new_query new_query
377
384
  end
378
385
  alias order_by order
379
386
 
@@ -406,7 +413,7 @@ module Google
406
413
 
407
414
  new_query.offset = num
408
415
 
409
- Query.start new_query, parent_path, client, limit_type: limit_type
416
+ start_new_query new_query
410
417
  end
411
418
 
412
419
  ##
@@ -443,7 +450,7 @@ module Google
443
450
 
444
451
  new_query.limit = Google::Protobuf::Int32Value.new value: num
445
452
 
446
- Query.start new_query, parent_path, client, limit_type: :first
453
+ start_new_query new_query, limit_type_override: :first
447
454
  end
448
455
 
449
456
  ##
@@ -503,7 +510,7 @@ module Google
503
510
 
504
511
  new_query.limit = Google::Protobuf::Int32Value.new value: num
505
512
 
506
- Query.start new_query, parent_path, client, limit_type: :last
513
+ start_new_query new_query, limit_type_override: :last
507
514
  end
508
515
 
509
516
  ##
@@ -611,7 +618,7 @@ module Google
611
618
  cursor.before = true
612
619
  new_query.start_at = cursor
613
620
 
614
- Query.start new_query, parent_path, client, limit_type: limit_type
621
+ start_new_query new_query
615
622
  end
616
623
 
617
624
  ##
@@ -720,7 +727,7 @@ module Google
720
727
  cursor.before = false
721
728
  new_query.start_at = cursor
722
729
 
723
- Query.start new_query, parent_path, client, limit_type: limit_type
730
+ start_new_query new_query
724
731
  end
725
732
 
726
733
  ##
@@ -829,7 +836,7 @@ module Google
829
836
  cursor.before = true
830
837
  new_query.end_at = cursor
831
838
 
832
- Query.start new_query, parent_path, client, limit_type: limit_type
839
+ start_new_query new_query
833
840
  end
834
841
 
835
842
  ##
@@ -938,7 +945,7 @@ module Google
938
945
  cursor.before = false
939
946
  new_query.end_at = cursor
940
947
 
941
- Query.start new_query, parent_path, client, limit_type: limit_type
948
+ start_new_query new_query
942
949
  end
943
950
 
944
951
  ##
@@ -1001,6 +1008,83 @@ module Google
1001
1008
  end
1002
1009
  alias run get
1003
1010
 
1011
+ ##
1012
+ # Retrieves the query explanation for the query.
1013
+ # By default the query is only planned, not executed. returning only metrics from the
1014
+ # planning stages. If `analyze` is set to `true` the query will be planned and executed,
1015
+ # returning the full query results alongside both planning and execution stage metrics.
1016
+ #
1017
+ # Unlike the Enumerator object that is returned from the `Query#get`,
1018
+ # iterating over QueryExplainResult multiple times will not result in
1019
+ # multiple requests to the server. The first set of results will be saved
1020
+ # and re-used instead.
1021
+ # This is to avoid the situations where the metrics change unpredictably when results are looked at.
1022
+ #
1023
+ # @param [Time] read_time Reads documents as they were at the given time.
1024
+ # This may not be older than 270 seconds. Optional
1025
+ #
1026
+ # @param [Boolean] analyze
1027
+ # Whether to execute the query and return the execution stage metrics
1028
+ # in addition to planning metrics.
1029
+ # If set to `false` the query will be planned only and will return planning
1030
+ # stage metrics without results.
1031
+ # If set to `true` the query will be executed, and will return the query results,
1032
+ # planning stage metrics, and execution stage metrics.
1033
+ # Defaults to `false`.
1034
+ #
1035
+ # @example Iterating over results multiple times
1036
+ # require "google/cloud/firestore"
1037
+ #
1038
+ # firestore = Google::Cloud::Firestore.new
1039
+ # query = firestore.col(:cities).where(:population, :>, 100000)
1040
+ # explanation_result = query.explain analyze: true
1041
+ # results = explanation_result.to_a
1042
+ # results_2 = explanation_result.to_a # same results, no re-query
1043
+ #
1044
+ # @return [QueryExplainResult]
1045
+ #
1046
+ # @example Getting only the planning stage metrics for the query
1047
+ # require "google/cloud/firestore"
1048
+ #
1049
+ # firestore = Google::Cloud::Firestore.new
1050
+ # query = firestore.col(:cities).where(:population, :>, 100000)
1051
+ #
1052
+ # # Get the execution plan without running the query
1053
+ # explain_result = query.explain
1054
+ # metrics = explain_result.explain_metrics
1055
+ # puts "Plan summary: #{metrics.plan_summary}" if metrics&.plan_summary
1056
+ #
1057
+ # @example Getting planning and execution stage metrics, as well as query results
1058
+ # require "google/cloud/firestore"
1059
+ #
1060
+ # firestore = Google::Cloud::Firestore.new
1061
+ # query = firestore.col(:cities).where(:population, :>, 100000)
1062
+ #
1063
+ # # Run the query and return metrics from the planning and execution stages
1064
+ # explain_result = query.explain analyze: true
1065
+ # metrics = explain_result.explain_metrics
1066
+ # puts "Plan summary: #{metrics.plan_summary}" if metrics&.plan_summary
1067
+ # puts "Results returned: #{metrics.execution_stats.results_returned}" if metrics&.execution_stats
1068
+ # results = explain_result.to_a
1069
+ #
1070
+ def explain read_time: false, analyze: false
1071
+ ensure_service!
1072
+
1073
+ # Validate analyze parameter
1074
+ unless [true, false].include? analyze
1075
+ raise ArgumentError, "analyze must be a boolean"
1076
+ end
1077
+
1078
+ explain_options = ::Google::Cloud::Firestore::V1::ExplainOptions.new
1079
+ explain_options.analyze = analyze
1080
+
1081
+ results = service.run_query parent_path, @query, read_time: read_time, explain_options: explain_options
1082
+
1083
+ # Reverse the results for Query#limit_to_last queries since that method reversed the order_by directions.
1084
+ results = results.to_a.reverse if limit_type == :last
1085
+ QueryExplainResult.new results, client
1086
+ end
1087
+
1004
1088
  ##
1005
1089
  # Creates an AggregateQuery object for the query.
1006
1090
  #
@@ -1117,12 +1201,41 @@ module Google
1117
1201
 
1118
1202
  ##
1119
1203
  # @private Start a new Query.
1204
+ #
1205
+ # This method creates and returns a new `Query` instance, initializing it with the provided parameters.
1206
+ #
1207
+ # @param [Google::Cloud::Firestore::V1::StructuredQuery] query
1208
+ # The structured query object representing the query to be executed.
1209
+ # @param [String] parent_path
1210
+ # The parent path of the collection or document the query is operating on.
1211
+ # @param [Google::Cloud::Firestore::Client] client
1212
+ # The Firestore client instance.
1213
+ # @param [Symbol, nil] limit_type
1214
+ # (Optional) The type of limit to apply to the query results, either `:first` or `:last`.
1215
+ # Defaults to `nil` if no limit is applied.
1216
+ # @return [Google::Cloud::Firestore::Query]
1217
+ # A new `Query` instance initialized with the given parameters.
1120
1218
  def self.start query, parent_path, client, limit_type: nil
1121
1219
  new query, parent_path, client, limit_type: limit_type
1122
1220
  end
1123
1221
 
1124
1222
  protected
1125
1223
 
1224
+ ##
1225
+ # @private Helper for starting a new query copying existing parameters.
1226
+ #
1227
+ # @param [Google::Cloud::Firestore::V1::StructuredQuery] new_query
1228
+ # The new structured query object.
1229
+ # @param [Symbol] limit_type_override The limit type override for the new query
1230
+ #
1231
+ # @return [Query] A new query
1232
+ def start_new_query new_query, limit_type_override: nil
1233
+ Query.start(new_query,
1234
+ parent_path,
1235
+ client,
1236
+ limit_type: limit_type_override || limit_type)
1237
+ end
1238
+
1126
1239
  ##
1127
1240
  # @private
1128
1241
  StructuredQuery = Google::Cloud::Firestore::V1::StructuredQuery
@@ -0,0 +1,130 @@
1
+ # Copyright 2024 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
+ module Google
17
+ module Cloud
18
+ module Firestore
19
+ ##
20
+ # # QueryExplainResult
21
+ #
22
+ # Represents the result of a Firestore query explanation. This class
23
+ # provides an enumerable interface to iterate over the {DocumentSnapshot}
24
+ # results (if the explanation was run with `analyze: true`) and allows
25
+ # access to the {Google::Cloud::Firestore::V1::ExplainMetrics} which
26
+ # contain details about the query plan and execution statistics.
27
+ #
28
+ # Unlike the Enumerator object that is returned from the `Query#get`,
29
+ # iterating over QueryExplainResult multiple times will not result in
30
+ # multiple requests to the server. The first set of results will be saved
31
+ # and re-used instead.
32
+ #
33
+ # This is to avoid the situations where the metrics do not correspond to the results
34
+ # if results are partially re-enumerated
35
+ #
36
+ # @see Query#explain
37
+ #
38
+ # @example Iterating over results and accessing metrics
39
+ # require "google/cloud/firestore"
40
+ #
41
+ # firestore = Google::Cloud::Firestore.new
42
+ # query = firestore.col(:cities).where(:population, :>, 100000)
43
+ #
44
+ # # Run the query and return metrics from the planning and execution stages
45
+ # explanation_result = query.explain analyze: true
46
+ #
47
+ # explanation_result.each do |city_snapshot|
48
+ # puts "City: #{city_snapshot.document_id}, Population: #{city_snapshot[:population]}"
49
+ # end
50
+ #
51
+ # metrics = explanation_result.explain_metrics
52
+ # puts "Results returned: #{metrics.execution_stats.results_returned}" if metrics&.execution_stats
53
+ #
54
+ # @example Fetching metrics directly (which also iterates internally if needed)
55
+ # require "google/cloud/firestore"
56
+ #
57
+ # firestore = Google::Cloud::Firestore.new
58
+ # query = firestore.col(:cities).where(:population, :>, 100000)
59
+ #
60
+ # # Get the execution plan without running the query (or with analyze: true)
61
+ # explanation_result = query.explain analyze: false # or true
62
+ #
63
+ # metrics = explanation_result.explain_metrics
64
+ # puts "Plan summary: #{metrics.plan_summary}" if metrics&.plan_summary
65
+ # puts "Results returned: #{metrics.execution_stats.results_returned}" if metrics&.execution_stats
66
+ #
67
+ # @example Iterating over results multiple times
68
+ # require "google/cloud/firestore"
69
+ #
70
+ # firestore = Google::Cloud::Firestore.new
71
+ # query = firestore.col(:cities).where(:population, :>, 100000)
72
+ # explanation_result = query.explain analyze: true
73
+ # results = explanation_result.to_a
74
+ # results_2 = explanation_result.to_a # same results, no re-query
75
+ #
76
+ ##
77
+ class QueryExplainResult
78
+ include Enumerable
79
+
80
+ # Indicates whether the {#explain_metrics} have been populated.
81
+ # This becomes `true` after iterating through the results (e.g., via {#each})
82
+ # or by explicitly calling {#explain_metrics}.
83
+ #
84
+ # @return [Boolean] `true` if metrics are populated, `false` otherwise.
85
+ attr_reader :metrics_fetched
86
+ alias metrics_fetched? metrics_fetched
87
+
88
+ # @private Creates a new QueryRunResult.
89
+ def initialize results_enum, client
90
+ @results_enum = results_enum
91
+ @client = client
92
+ @metrics_fetched = false
93
+ end
94
+
95
+ # The metrics from planning and execution stages of the query.
96
+ # Calling this the first time will enumerate and cache all results as well as cache the metrics.
97
+ #
98
+ # Subsequent calls will return the cached value.
99
+ #
100
+ # @return [Google::Cloud::Firestore::V1::ExplainMetrics] The query explanation metrics.
101
+ def explain_metrics
102
+ # rubocop:disable Lint/EmptyBlock
103
+ each {} unless metrics_fetched?
104
+ # rubocop:enable Lint/EmptyBlock
105
+ @explain_metrics
106
+ end
107
+
108
+ # Iterates over the document snapshots returned by the query explanation
109
+ # if `analyze: true` was used. If `analyze: false` was used, this
110
+ # method will still iterate but will not yield any documents, though it
111
+ # will populate the query explanation metrics.
112
+ #
113
+ # @yieldparam [DocumentSnapshot] snapshot A document snapshot from the query results.
114
+ # @return [Enumerator] If no block is given.
115
+ def each
116
+ return enum_for :each unless block_given?
117
+ @results ||= @results_enum.to_a
118
+
119
+ @results.each do |result|
120
+ @explain_metrics ||= result.explain_metrics if result.explain_metrics
121
+ @metrics_fetched = !@explain_metrics.nil?
122
+ next if result.document.nil?
123
+
124
+ yield DocumentSnapshot.from_query_result(result, @client)
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -12,6 +12,7 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ require "cgi/escape"
15
16
 
16
17
  require "google/cloud/env"
17
18
  require "google/cloud/errors"
@@ -52,7 +53,8 @@ module Google
52
53
  config.endpoint = host if host
53
54
  config.lib_name = "gccl"
54
55
  config.lib_version = Google::Cloud::Firestore::VERSION
55
- config.metadata = { "google-cloud-resource-prefix": "projects/#{@project}/databases/#{@database}" }
56
+ routing_val = CGI.escapeURIComponent "projects/#{@project}/databases/#{@database}"
57
+ config.metadata = { "x-goog-request-params": "database=#{routing_val}" }
56
58
  end
57
59
  end
58
60
  end
@@ -120,26 +122,32 @@ module Google
120
122
  paged_enum.response
121
123
  end
122
124
 
123
- def run_query path, query_grpc, transaction: nil, read_time: nil
125
+ def run_query path, query_grpc, transaction: nil, read_time: nil, explain_options: nil
124
126
  run_query_req = {
125
127
  parent: path,
126
128
  structured_query: query_grpc
127
129
  }
130
+
128
131
  if transaction.is_a? String
129
132
  run_query_req[:transaction] = transaction
130
133
  elsif transaction
131
134
  run_query_req[:new_transaction] = transaction
132
135
  end
136
+
133
137
  if read_time
134
138
  run_query_req[:read_time] = read_time_to_timestamp(read_time)
135
139
  end
136
140
 
141
+ if explain_options
142
+ run_query_req[:explain_options] = explain_options
143
+ end
144
+
137
145
  firestore.run_query run_query_req, call_options(parent: database_path)
138
146
  end
139
147
 
140
148
  ##
141
149
  # Returns Google::Cloud::Firestore::V1::RunAggregationQueryResponse
142
- def run_aggregate_query parent, structured_aggregation_query, transaction: nil
150
+ def run_aggregate_query parent, structured_aggregation_query, transaction: nil, explain_options: nil
143
151
  request = Google::Cloud::Firestore::V1::RunAggregationQueryRequest.new(
144
152
  parent: parent,
145
153
  structured_aggregation_query: structured_aggregation_query
@@ -149,6 +157,9 @@ module Google
149
157
  elsif transaction
150
158
  request.new_transaction = transaction
151
159
  end
160
+ if explain_options
161
+ request.explain_options = explain_options
162
+ end
152
163
  firestore.run_aggregation_query request
153
164
  end
154
165
 
@@ -227,7 +238,8 @@ module Google
227
238
 
228
239
  def default_headers parent = nil
229
240
  parent ||= database_path
230
- { "google-cloud-resource-prefix" => parent }
241
+ routing_val = CGI.escapeURIComponent parent
242
+ { "x-goog-request-params" => "database=#{routing_val}" }
231
243
  end
232
244
 
233
245
  def call_options parent: nil, token: nil
@@ -16,7 +16,7 @@
16
16
  module Google
17
17
  module Cloud
18
18
  module Firestore
19
- VERSION = "3.0.0".freeze
19
+ VERSION = "3.1.0".freeze
20
20
  end
21
21
  end
22
22
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: google-cloud-firestore
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Google Inc
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-03-10 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: bigdecimal
@@ -99,6 +99,7 @@ files:
99
99
  - lib/google-cloud-firestore.rb
100
100
  - lib/google/cloud/firestore.rb
101
101
  - lib/google/cloud/firestore/aggregate_query.rb
102
+ - lib/google/cloud/firestore/aggregate_query_explain_result.rb
102
103
  - lib/google/cloud/firestore/aggregate_query_snapshot.rb
103
104
  - lib/google/cloud/firestore/batch.rb
104
105
  - lib/google/cloud/firestore/bulk_commit_batch.rb
@@ -125,6 +126,7 @@ files:
125
126
  - lib/google/cloud/firestore/generate.rb
126
127
  - lib/google/cloud/firestore/promise/future.rb
127
128
  - lib/google/cloud/firestore/query.rb
129
+ - lib/google/cloud/firestore/query_explain_result.rb
128
130
  - lib/google/cloud/firestore/query_listener.rb
129
131
  - lib/google/cloud/firestore/query_partition.rb
130
132
  - lib/google/cloud/firestore/query_snapshot.rb
@@ -155,7 +157,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
155
157
  - !ruby/object:Gem::Version
156
158
  version: '0'
157
159
  requirements: []
158
- rubygems_version: 3.6.5
160
+ rubygems_version: 3.6.9
159
161
  specification_version: 4
160
162
  summary: API Client library for Google Cloud Firestore API
161
163
  test_files: []