Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/google/cloud/firestore_v1/base_query.py: 34%

432 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-09 06:27 +0000

1# Copyright 2017 Google LLC All rights reserved. 

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# http://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"""Classes for representing queries for the Google Cloud Firestore API. 

16 

17A :class:`~google.cloud.firestore_v1.query.Query` can be created directly from 

18a :class:`~google.cloud.firestore_v1.collection.Collection` and that can be 

19a more common way to create a query than direct usage of the constructor. 

20""" 

21from __future__ import annotations 

22 

23import abc 

24import copy 

25import math 

26import warnings 

27 

28from google.api_core import retry as retries 

29from google.protobuf import wrappers_pb2 

30 

31from google.cloud import firestore_v1 

32from google.cloud.firestore_v1 import _helpers 

33from google.cloud.firestore_v1 import document 

34from google.cloud.firestore_v1 import field_path as field_path_module 

35from google.cloud.firestore_v1 import transforms 

36from google.cloud.firestore_v1.types import StructuredQuery 

37from google.cloud.firestore_v1.types import query 

38from google.cloud.firestore_v1.types import Cursor 

39from google.cloud.firestore_v1.types import RunQueryResponse 

40from google.cloud.firestore_v1.order import Order 

41from typing import ( 

42 Any, 

43 Dict, 

44 Generator, 

45 Iterable, 

46 NoReturn, 

47 Optional, 

48 Tuple, 

49 Type, 

50 TypeVar, 

51 Union, 

52 TYPE_CHECKING, 

53) 

54 

55# Types needed only for Type Hints 

56from google.cloud.firestore_v1.base_document import DocumentSnapshot 

57 

58if TYPE_CHECKING: # pragma: NO COVER 

59 from google.cloud.firestore_v1.field_path import FieldPath 

60 

61_BAD_DIR_STRING: str 

62_BAD_OP_NAN_NULL: str 

63_BAD_OP_STRING: str 

64_COMPARISON_OPERATORS: Dict[str, Any] 

65_EQ_OP: str 

66_INVALID_CURSOR_TRANSFORM: str 

67_INVALID_WHERE_TRANSFORM: str 

68_MISMATCH_CURSOR_W_ORDER_BY: str 

69_MISSING_ORDER_BY: str 

70_NO_ORDERS_FOR_CURSOR: str 

71_operator_enum: Any 

72 

73 

74_EQ_OP = "==" 

75_operator_enum = StructuredQuery.FieldFilter.Operator 

76_COMPARISON_OPERATORS = { 

77 "<": _operator_enum.LESS_THAN, 

78 "<=": _operator_enum.LESS_THAN_OR_EQUAL, 

79 _EQ_OP: _operator_enum.EQUAL, 

80 "!=": _operator_enum.NOT_EQUAL, 

81 ">=": _operator_enum.GREATER_THAN_OR_EQUAL, 

82 ">": _operator_enum.GREATER_THAN, 

83 "array_contains": _operator_enum.ARRAY_CONTAINS, 

84 "in": _operator_enum.IN, 

85 "not-in": _operator_enum.NOT_IN, 

86 "array_contains_any": _operator_enum.ARRAY_CONTAINS_ANY, 

87} 

88# set of operators that don't involve equlity comparisons 

89# will be used in query normalization 

90_INEQUALITY_OPERATORS = ( 

91 _operator_enum.LESS_THAN, 

92 _operator_enum.LESS_THAN_OR_EQUAL, 

93 _operator_enum.GREATER_THAN_OR_EQUAL, 

94 _operator_enum.GREATER_THAN, 

95 _operator_enum.NOT_EQUAL, 

96 _operator_enum.NOT_IN, 

97) 

98_BAD_OP_STRING = "Operator string {!r} is invalid. Valid choices are: {}." 

99_BAD_OP_NAN_NULL = 'Only an equality filter ("==") can be used with None or NaN values' 

100_INVALID_WHERE_TRANSFORM = "Transforms cannot be used as where values." 

101_BAD_DIR_STRING = "Invalid direction {!r}. Must be one of {!r} or {!r}." 

102_INVALID_CURSOR_TRANSFORM = "Transforms cannot be used as cursor values." 

103_MISSING_ORDER_BY = ( 

104 'The "order by" field path {!r} is not present in the cursor data {!r}. ' 

105 "All fields sent to ``order_by()`` must be present in the fields " 

106 "if passed to one of ``start_at()`` / ``start_after()`` / " 

107 "``end_before()`` / ``end_at()`` to define a cursor." 

108) 

109 

110_NO_ORDERS_FOR_CURSOR = ( 

111 "Attempting to create a cursor with no fields to order on. " 

112 "When defining a cursor with one of ``start_at()`` / ``start_after()`` / " 

113 "``end_before()`` / ``end_at()``, all fields in the cursor must " 

114 "come from fields set in ``order_by()``." 

115) 

116_MISMATCH_CURSOR_W_ORDER_BY = "The cursor {!r} does not match the order fields {!r}." 

117 

118_not_passed = object() 

119 

120QueryType = TypeVar("QueryType", bound="BaseQuery") 

121 

122 

123class BaseFilter(abc.ABC): 

124 """Base class for Filters""" 

125 

126 @abc.abstractmethod 

127 def _to_pb(self): 

128 """Build the protobuf representation based on values in the filter""" 

129 

130 

131class FieldFilter(BaseFilter): 

132 """Class representation of a Field Filter.""" 

133 

134 def __init__(self, field_path, op_string, value=None): 

135 self.field_path = field_path 

136 self.value = value 

137 

138 if value is None: 

139 if op_string != _EQ_OP: 

140 raise ValueError(_BAD_OP_NAN_NULL) 

141 self.op_string = StructuredQuery.UnaryFilter.Operator.IS_NULL 

142 

143 elif _isnan(value): 

144 if op_string != _EQ_OP: 

145 raise ValueError(_BAD_OP_NAN_NULL) 

146 self.op_string = StructuredQuery.UnaryFilter.Operator.IS_NAN 

147 elif isinstance(value, (transforms.Sentinel, transforms._ValueList)): 

148 raise ValueError(_INVALID_WHERE_TRANSFORM) 

149 else: 

150 self.op_string = op_string 

151 

152 def _to_pb(self): 

153 """Returns the protobuf representation, either a StructuredQuery.UnaryFilter or a StructuredQuery.FieldFilter""" 

154 if self.value is None or _isnan(self.value): 

155 filter_pb = query.StructuredQuery.UnaryFilter( 

156 field=query.StructuredQuery.FieldReference(field_path=self.field_path), 

157 op=self.op_string, 

158 ) 

159 else: 

160 filter_pb = query.StructuredQuery.FieldFilter( 

161 field=query.StructuredQuery.FieldReference(field_path=self.field_path), 

162 op=_enum_from_op_string(self.op_string), 

163 value=_helpers.encode_value(self.value), 

164 ) 

165 return filter_pb 

166 

167 

168class BaseCompositeFilter(BaseFilter): 

169 """Base class for a Composite Filter. (either OR or AND).""" 

170 

171 def __init__( 

172 self, 

173 operator=StructuredQuery.CompositeFilter.Operator.OPERATOR_UNSPECIFIED, 

174 filters=None, 

175 ): 

176 self.operator = operator 

177 if filters is None: 

178 self.filters = [] 

179 else: 

180 self.filters = filters 

181 

182 def __repr__(self): 

183 repr = f"op: {self.operator}\nFilters:" 

184 for filter in self.filters: 

185 repr += f"\n\t{filter}" 

186 return repr 

187 

188 def _to_pb(self): 

189 """Build the protobuf representation based on values in the Composite Filter.""" 

190 filter_pb = StructuredQuery.CompositeFilter( 

191 op=self.operator, 

192 ) 

193 for filter in self.filters: 

194 if isinstance(filter, BaseCompositeFilter): 

195 fb = query.StructuredQuery.Filter(composite_filter=filter._to_pb()) 

196 else: 

197 fb = _filter_pb(filter._to_pb()) 

198 filter_pb.filters.append(fb) 

199 

200 return filter_pb 

201 

202 

203class Or(BaseCompositeFilter): 

204 """Class representation of an OR Filter.""" 

205 

206 def __init__(self, filters): 

207 super().__init__( 

208 operator=StructuredQuery.CompositeFilter.Operator.OR, filters=filters 

209 ) 

210 

211 

212class And(BaseCompositeFilter): 

213 """Class representation of an AND Filter.""" 

214 

215 def __init__(self, filters): 

216 super().__init__( 

217 operator=StructuredQuery.CompositeFilter.Operator.AND, filters=filters 

218 ) 

219 

220 

221class BaseQuery(object): 

222 """Represents a query to the Firestore API. 

223 

224 Instances of this class are considered immutable: all methods that 

225 would modify an instance instead return a new instance. 

226 

227 Args: 

228 parent (:class:`~google.cloud.firestore_v1.collection.CollectionReference`): 

229 The collection that this query applies to. 

230 projection (Optional[:class:`google.cloud.proto.firestore.v1.\ 

231 query.StructuredQuery.Projection`]): 

232 A projection of document fields to limit the query results to. 

233 field_filters (Optional[Tuple[:class:`google.cloud.proto.firestore.v1.\ 

234 query.StructuredQuery.FieldFilter`, ...]]): 

235 The filters to be applied in the query. 

236 orders (Optional[Tuple[:class:`google.cloud.proto.firestore.v1.\ 

237 query.StructuredQuery.Order`, ...]]): 

238 The "order by" entries to use in the query. 

239 limit (Optional[int]): 

240 The maximum number of documents the query is allowed to return. 

241 limit_to_last (Optional[bool]): 

242 Denotes whether a provided limit is applied to the end of the result set. 

243 offset (Optional[int]): 

244 The number of results to skip. 

245 start_at (Optional[Tuple[dict, bool]]): 

246 Two-tuple of : 

247 

248 * a mapping of fields. Any field that is present in this mapping 

249 must also be present in ``orders`` 

250 * an ``after`` flag 

251 

252 The fields and the flag combine to form a cursor used as 

253 a starting point in a query result set. If the ``after`` 

254 flag is :data:`True`, the results will start just after any 

255 documents which have fields matching the cursor, otherwise 

256 any matching documents will be included in the result set. 

257 When the query is formed, the document values 

258 will be used in the order given by ``orders``. 

259 end_at (Optional[Tuple[dict, bool]]): 

260 Two-tuple of: 

261 

262 * a mapping of fields. Any field that is present in this mapping 

263 must also be present in ``orders`` 

264 * a ``before`` flag 

265 

266 The fields and the flag combine to form a cursor used as 

267 an ending point in a query result set. If the ``before`` 

268 flag is :data:`True`, the results will end just before any 

269 documents which have fields matching the cursor, otherwise 

270 any matching documents will be included in the result set. 

271 When the query is formed, the document values 

272 will be used in the order given by ``orders``. 

273 all_descendants (Optional[bool]): 

274 When false, selects only collections that are immediate children 

275 of the `parent` specified in the containing `RunQueryRequest`. 

276 When true, selects all descendant collections. 

277 recursive (Optional[bool]): 

278 When true, returns all documents and all documents in any subcollections 

279 below them. Defaults to false. 

280 """ 

281 

282 ASCENDING = "ASCENDING" 

283 """str: Sort query results in ascending order on a field.""" 

284 DESCENDING = "DESCENDING" 

285 """str: Sort query results in descending order on a field.""" 

286 

287 def __init__( 

288 self, 

289 parent, 

290 projection=None, 

291 field_filters=(), 

292 orders=(), 

293 limit=None, 

294 limit_to_last=False, 

295 offset=None, 

296 start_at=None, 

297 end_at=None, 

298 all_descendants=False, 

299 recursive=False, 

300 ) -> None: 

301 self._parent = parent 

302 self._projection = projection 

303 self._field_filters = field_filters 

304 self._orders = orders 

305 self._limit = limit 

306 self._limit_to_last = limit_to_last 

307 self._offset = offset 

308 self._start_at = start_at 

309 self._end_at = end_at 

310 self._all_descendants = all_descendants 

311 self._recursive = recursive 

312 

313 def __eq__(self, other): 

314 if not isinstance(other, self.__class__): 

315 return NotImplemented 

316 return ( 

317 self._parent == other._parent 

318 and self._projection == other._projection 

319 and self._field_filters == other._field_filters 

320 and self._orders == other._orders 

321 and self._limit == other._limit 

322 and self._limit_to_last == other._limit_to_last 

323 and self._offset == other._offset 

324 and self._start_at == other._start_at 

325 and self._end_at == other._end_at 

326 and self._all_descendants == other._all_descendants 

327 ) 

328 

329 @property 

330 def _client(self): 

331 """The client of the parent collection. 

332 

333 Returns: 

334 :class:`~google.cloud.firestore_v1.client.Client`: 

335 The client that owns this query. 

336 """ 

337 return self._parent._client 

338 

339 def select(self: QueryType, field_paths: Iterable[str]) -> QueryType: 

340 """Project documents matching query to a limited set of fields. 

341 

342 See :meth:`~google.cloud.firestore_v1.client.Client.field_path` for 

343 more information on **field paths**. 

344 

345 If the current query already has a projection set (i.e. has already 

346 called :meth:`~google.cloud.firestore_v1.query.Query.select`), this 

347 will overwrite it. 

348 

349 Args: 

350 field_paths (Iterable[str, ...]): An iterable of field paths 

351 (``.``-delimited list of field names) to use as a projection 

352 of document fields in the query results. 

353 

354 Returns: 

355 :class:`~google.cloud.firestore_v1.query.Query`: 

356 A "projected" query. Acts as a copy of the current query, 

357 modified with the newly added projection. 

358 Raises: 

359 ValueError: If any ``field_path`` is invalid. 

360 """ 

361 field_paths = list(field_paths) 

362 for field_path in field_paths: 

363 field_path_module.split_field_path(field_path) 

364 

365 new_projection = query.StructuredQuery.Projection( 

366 fields=[ 

367 query.StructuredQuery.FieldReference(field_path=field_path) 

368 for field_path in field_paths 

369 ] 

370 ) 

371 return self._copy(projection=new_projection) 

372 

373 def _copy( 

374 self: QueryType, 

375 *, 

376 projection: Optional[query.StructuredQuery.Projection] = _not_passed, 

377 field_filters: Optional[Tuple[query.StructuredQuery.FieldFilter]] = _not_passed, 

378 orders: Optional[Tuple[query.StructuredQuery.Order]] = _not_passed, 

379 limit: Optional[int] = _not_passed, 

380 limit_to_last: Optional[bool] = _not_passed, 

381 offset: Optional[int] = _not_passed, 

382 start_at: Optional[Tuple[dict, bool]] = _not_passed, 

383 end_at: Optional[Tuple[dict, bool]] = _not_passed, 

384 all_descendants: Optional[bool] = _not_passed, 

385 recursive: Optional[bool] = _not_passed, 

386 ) -> QueryType: 

387 return self.__class__( 

388 self._parent, 

389 projection=self._evaluate_param(projection, self._projection), 

390 field_filters=self._evaluate_param(field_filters, self._field_filters), 

391 orders=self._evaluate_param(orders, self._orders), 

392 limit=self._evaluate_param(limit, self._limit), 

393 limit_to_last=self._evaluate_param(limit_to_last, self._limit_to_last), 

394 offset=self._evaluate_param(offset, self._offset), 

395 start_at=self._evaluate_param(start_at, self._start_at), 

396 end_at=self._evaluate_param(end_at, self._end_at), 

397 all_descendants=self._evaluate_param( 

398 all_descendants, self._all_descendants 

399 ), 

400 recursive=self._evaluate_param(recursive, self._recursive), 

401 ) 

402 

403 def _evaluate_param(self, value, fallback_value): 

404 """Helper which allows `None` to be passed into `copy` and be set on the 

405 copy instead of being misinterpreted as an unpassed parameter.""" 

406 return value if value is not _not_passed else fallback_value 

407 

408 def where( 

409 self: QueryType, 

410 field_path: Optional[str] = None, 

411 op_string: Optional[str] = None, 

412 value=None, 

413 *, 

414 filter=None, 

415 ) -> QueryType: 

416 """Filter the query on a field. 

417 

418 See :meth:`~google.cloud.firestore_v1.client.Client.field_path` for 

419 more information on **field paths**. 

420 

421 Returns a new :class:`~google.cloud.firestore_v1.query.Query` that 

422 filters on a specific field path, according to an operation (e.g. 

423 ``==`` or "equals") and a particular value to be paired with that 

424 operation. 

425 

426 Args: 

427 field_path (Optional[str]): A field path (``.``-delimited list of 

428 field names) for the field to filter on. 

429 op_string (Optional[str]): A comparison operation in the form of a string. 

430 Acceptable values are ``<``, ``<=``, ``==``, ``!=``, ``>=``, ``>``, 

431 ``in``, ``not-in``, ``array_contains`` and ``array_contains_any``. 

432 value (Any): The value to compare the field against in the filter. 

433 If ``value`` is :data:`None` or a NaN, then ``==`` is the only 

434 allowed operation. 

435 

436 Returns: 

437 :class:`~google.cloud.firestore_v1.query.Query`: 

438 A filtered query. Acts as a copy of the current query, 

439 modified with the newly added filter. 

440 

441 Raises: 

442 ValueError: If 

443 * ``field_path`` is invalid. 

444 * If ``value`` is a NaN or :data:`None` and ``op_string`` is not ``==``. 

445 * FieldFilter was passed without using the filter keyword argument. 

446 * `And` or `Or` was passed without using the filter keyword argument . 

447 * Both the positional arguments and the keyword argument `filter` were passed. 

448 """ 

449 

450 if isinstance(field_path, FieldFilter): 

451 raise ValueError( 

452 "FieldFilter object must be passed using keyword argument 'filter'" 

453 ) 

454 if isinstance(field_path, BaseCompositeFilter): 

455 raise ValueError( 

456 "'Or' and 'And' objects must be passed using keyword argument 'filter'" 

457 ) 

458 

459 field_path_module.split_field_path(field_path) 

460 new_filters = self._field_filters 

461 

462 if field_path is not None and op_string is not None: 

463 if filter is not None: 

464 raise ValueError( 

465 "Can't pass in both the positional arguments and 'filter' at the same time" 

466 ) 

467 warnings.warn( 

468 "Detected filter using positional arguments. Prefer using the 'filter' keyword argument instead.", 

469 UserWarning, 

470 stacklevel=2, 

471 ) 

472 if value is None: 

473 if op_string != _EQ_OP: 

474 raise ValueError(_BAD_OP_NAN_NULL) 

475 filter_pb = query.StructuredQuery.UnaryFilter( 

476 field=query.StructuredQuery.FieldReference(field_path=field_path), 

477 op=StructuredQuery.UnaryFilter.Operator.IS_NULL, 

478 ) 

479 elif _isnan(value): 

480 if op_string != _EQ_OP: 

481 raise ValueError(_BAD_OP_NAN_NULL) 

482 filter_pb = query.StructuredQuery.UnaryFilter( 

483 field=query.StructuredQuery.FieldReference(field_path=field_path), 

484 op=StructuredQuery.UnaryFilter.Operator.IS_NAN, 

485 ) 

486 elif isinstance(value, (transforms.Sentinel, transforms._ValueList)): 

487 raise ValueError(_INVALID_WHERE_TRANSFORM) 

488 else: 

489 filter_pb = query.StructuredQuery.FieldFilter( 

490 field=query.StructuredQuery.FieldReference(field_path=field_path), 

491 op=_enum_from_op_string(op_string), 

492 value=_helpers.encode_value(value), 

493 ) 

494 

495 new_filters += (filter_pb,) 

496 elif isinstance(filter, BaseFilter): 

497 new_filters += (filter._to_pb(),) 

498 else: 

499 raise ValueError( 

500 "Filter must be provided through positional arguments or the 'filter' keyword argument." 

501 ) 

502 return self._copy(field_filters=new_filters) 

503 

504 @staticmethod 

505 def _make_order(field_path, direction) -> StructuredQuery.Order: 

506 """Helper for :meth:`order_by`.""" 

507 return query.StructuredQuery.Order( 

508 field=query.StructuredQuery.FieldReference(field_path=field_path), 

509 direction=_enum_from_direction(direction), 

510 ) 

511 

512 def order_by( 

513 self: QueryType, field_path: str, direction: str = ASCENDING 

514 ) -> QueryType: 

515 """Modify the query to add an order clause on a specific field. 

516 

517 See :meth:`~google.cloud.firestore_v1.client.Client.field_path` for 

518 more information on **field paths**. 

519 

520 Successive :meth:`~google.cloud.firestore_v1.query.Query.order_by` 

521 calls will further refine the ordering of results returned by the query 

522 (i.e. the new "order by" fields will be added to existing ones). 

523 

524 Args: 

525 field_path (str): A field path (``.``-delimited list of 

526 field names) on which to order the query results. 

527 direction (Optional[str]): The direction to order by. Must be one 

528 of :attr:`ASCENDING` or :attr:`DESCENDING`, defaults to 

529 :attr:`ASCENDING`. 

530 

531 Returns: 

532 :class:`~google.cloud.firestore_v1.query.Query`: 

533 An ordered query. Acts as a copy of the current query, modified 

534 with the newly added "order by" constraint. 

535 

536 Raises: 

537 ValueError: If ``field_path`` is invalid. 

538 ValueError: If ``direction`` is not one of :attr:`ASCENDING` or 

539 :attr:`DESCENDING`. 

540 """ 

541 field_path_module.split_field_path(field_path) # raises 

542 

543 order_pb = self._make_order(field_path, direction) 

544 

545 new_orders = self._orders + (order_pb,) 

546 return self._copy(orders=new_orders) 

547 

548 def limit(self: QueryType, count: int) -> QueryType: 

549 """Limit a query to return at most `count` matching results. 

550 

551 If the current query already has a `limit` set, this will override it. 

552 

553 .. note:: 

554 `limit` and `limit_to_last` are mutually exclusive. 

555 Setting `limit` will drop previously set `limit_to_last`. 

556 

557 Args: 

558 count (int): Maximum number of documents to return that match 

559 the query. 

560 Returns: 

561 :class:`~google.cloud.firestore_v1.query.Query`: 

562 A limited query. Acts as a copy of the current query, modified 

563 with the newly added "limit" filter. 

564 """ 

565 return self._copy(limit=count, limit_to_last=False) 

566 

567 def limit_to_last(self: QueryType, count: int) -> QueryType: 

568 """Limit a query to return the last `count` matching results. 

569 If the current query already has a `limit_to_last` 

570 set, this will override it. 

571 

572 .. note:: 

573 `limit` and `limit_to_last` are mutually exclusive. 

574 Setting `limit_to_last` will drop previously set `limit`. 

575 

576 Args: 

577 count (int): Maximum number of documents to return that match 

578 the query. 

579 Returns: 

580 :class:`~google.cloud.firestore_v1.query.Query`: 

581 A limited query. Acts as a copy of the current query, modified 

582 with the newly added "limit" filter. 

583 """ 

584 return self._copy(limit=count, limit_to_last=True) 

585 

586 def _resolve_chunk_size(self, num_loaded: int, chunk_size: int) -> int: 

587 """Utility function for chunkify.""" 

588 if self._limit is not None and (num_loaded + chunk_size) > self._limit: 

589 return max(self._limit - num_loaded, 0) 

590 return chunk_size 

591 

592 def offset(self: QueryType, num_to_skip: int) -> QueryType: 

593 """Skip to an offset in a query. 

594 

595 If the current query already has specified an offset, this will 

596 overwrite it. 

597 

598 Args: 

599 num_to_skip (int): The number of results to skip at the beginning 

600 of query results. (Must be non-negative.) 

601 

602 Returns: 

603 :class:`~google.cloud.firestore_v1.query.Query`: 

604 An offset query. Acts as a copy of the current query, modified 

605 with the newly added "offset" field. 

606 """ 

607 return self._copy(offset=num_to_skip) 

608 

609 def _check_snapshot(self, document_snapshot) -> None: 

610 """Validate local snapshots for non-collection-group queries. 

611 

612 Raises: 

613 ValueError: for non-collection-group queries, if the snapshot 

614 is from a different collection. 

615 """ 

616 if self._all_descendants: 

617 return 

618 

619 if document_snapshot.reference._path[:-1] != self._parent._path: 

620 raise ValueError("Cannot use snapshot from another collection as a cursor.") 

621 

622 def _cursor_helper( 

623 self: QueryType, 

624 document_fields_or_snapshot: Union[DocumentSnapshot, dict, list, tuple], 

625 before: bool, 

626 start: bool, 

627 ) -> QueryType: 

628 """Set values to be used for a ``start_at`` or ``end_at`` cursor. 

629 

630 The values will later be used in a query protobuf. 

631 

632 When the query is sent to the server, the ``document_fields_or_snapshot`` will 

633 be used in the order given by fields set by 

634 :meth:`~google.cloud.firestore_v1.query.Query.order_by`. 

635 

636 Args: 

637 document_fields_or_snapshot 

638 (Union[:class:`~google.cloud.firestore_v1.document.DocumentSnapshot`, dict, list, tuple]): 

639 a document snapshot or a dictionary/list/tuple of fields 

640 representing a query results cursor. A cursor is a collection 

641 of values that represent a position in a query result set. 

642 before (bool): Flag indicating if the document in 

643 ``document_fields_or_snapshot`` should (:data:`False`) or 

644 shouldn't (:data:`True`) be included in the result set. 

645 start (Optional[bool]): determines if the cursor is a ``start_at`` 

646 cursor (:data:`True`) or an ``end_at`` cursor (:data:`False`). 

647 

648 Returns: 

649 :class:`~google.cloud.firestore_v1.query.Query`: 

650 A query with cursor. Acts as a copy of the current query, modified 

651 with the newly added "start at" cursor. 

652 """ 

653 if isinstance(document_fields_or_snapshot, tuple): 

654 document_fields_or_snapshot = list(document_fields_or_snapshot) 

655 elif isinstance(document_fields_or_snapshot, document.DocumentSnapshot): 

656 self._check_snapshot(document_fields_or_snapshot) 

657 else: 

658 # NOTE: We copy so that the caller can't modify after calling. 

659 document_fields_or_snapshot = copy.deepcopy(document_fields_or_snapshot) 

660 

661 cursor_pair = document_fields_or_snapshot, before 

662 query_kwargs = { 

663 "projection": self._projection, 

664 "field_filters": self._field_filters, 

665 "orders": self._orders, 

666 "limit": self._limit, 

667 "offset": self._offset, 

668 "all_descendants": self._all_descendants, 

669 } 

670 if start: 

671 query_kwargs["start_at"] = cursor_pair 

672 query_kwargs["end_at"] = self._end_at 

673 else: 

674 query_kwargs["start_at"] = self._start_at 

675 query_kwargs["end_at"] = cursor_pair 

676 

677 return self._copy(**query_kwargs) 

678 

679 def start_at( 

680 self: QueryType, 

681 document_fields_or_snapshot: Union[DocumentSnapshot, dict, list, tuple], 

682 ) -> QueryType: 

683 """Start query results at a particular document value. 

684 

685 The result set will **include** the document specified by 

686 ``document_fields_or_snapshot``. 

687 

688 If the current query already has specified a start cursor -- either 

689 via this method or 

690 :meth:`~google.cloud.firestore_v1.query.Query.start_after` -- this 

691 will overwrite it. 

692 

693 When the query is sent to the server, the ``document_fields`` will 

694 be used in the order given by fields set by 

695 :meth:`~google.cloud.firestore_v1.query.Query.order_by`. 

696 

697 Args: 

698 document_fields_or_snapshot 

699 (Union[:class:`~google.cloud.firestore_v1.document.DocumentSnapshot`, dict, list, tuple]): 

700 a document snapshot or a dictionary/list/tuple of fields 

701 representing a query results cursor. A cursor is a collection 

702 of values that represent a position in a query result set. 

703 

704 Returns: 

705 :class:`~google.cloud.firestore_v1.query.Query`: 

706 A query with cursor. Acts as 

707 a copy of the current query, modified with the newly added 

708 "start at" cursor. 

709 """ 

710 return self._cursor_helper(document_fields_or_snapshot, before=True, start=True) 

711 

712 def start_after( 

713 self: QueryType, 

714 document_fields_or_snapshot: Union[DocumentSnapshot, dict, list, tuple], 

715 ) -> QueryType: 

716 """Start query results after a particular document value. 

717 

718 The result set will **exclude** the document specified by 

719 ``document_fields_or_snapshot``. 

720 

721 If the current query already has specified a start cursor -- either 

722 via this method or 

723 :meth:`~google.cloud.firestore_v1.query.Query.start_at` -- this will 

724 overwrite it. 

725 

726 When the query is sent to the server, the ``document_fields_or_snapshot`` will 

727 be used in the order given by fields set by 

728 :meth:`~google.cloud.firestore_v1.query.Query.order_by`. 

729 

730 Args: 

731 document_fields_or_snapshot 

732 (Union[:class:`~google.cloud.firestore_v1.document.DocumentSnapshot`, dict, list, tuple]): 

733 a document snapshot or a dictionary/list/tuple of fields 

734 representing a query results cursor. A cursor is a collection 

735 of values that represent a position in a query result set. 

736 

737 Returns: 

738 :class:`~google.cloud.firestore_v1.query.Query`: 

739 A query with cursor. Acts as a copy of the current query, modified 

740 with the newly added "start after" cursor. 

741 """ 

742 return self._cursor_helper( 

743 document_fields_or_snapshot, before=False, start=True 

744 ) 

745 

746 def end_before( 

747 self: QueryType, 

748 document_fields_or_snapshot: Union[DocumentSnapshot, dict, list, tuple], 

749 ) -> QueryType: 

750 """End query results before a particular document value. 

751 

752 The result set will **exclude** the document specified by 

753 ``document_fields_or_snapshot``. 

754 

755 If the current query already has specified an end cursor -- either 

756 via this method or 

757 :meth:`~google.cloud.firestore_v1.query.Query.end_at` -- this will 

758 overwrite it. 

759 

760 When the query is sent to the server, the ``document_fields_or_snapshot`` will 

761 be used in the order given by fields set by 

762 :meth:`~google.cloud.firestore_v1.query.Query.order_by`. 

763 

764 Args: 

765 document_fields_or_snapshot 

766 (Union[:class:`~google.cloud.firestore_v1.document.DocumentSnapshot`, dict, list, tuple]): 

767 a document snapshot or a dictionary/list/tuple of fields 

768 representing a query results cursor. A cursor is a collection 

769 of values that represent a position in a query result set. 

770 

771 Returns: 

772 :class:`~google.cloud.firestore_v1.query.Query`: 

773 A query with cursor. Acts as a copy of the current query, modified 

774 with the newly added "end before" cursor. 

775 """ 

776 return self._cursor_helper( 

777 document_fields_or_snapshot, before=True, start=False 

778 ) 

779 

780 def end_at( 

781 self: QueryType, 

782 document_fields_or_snapshot: Union[DocumentSnapshot, dict, list, tuple], 

783 ) -> QueryType: 

784 """End query results at a particular document value. 

785 

786 The result set will **include** the document specified by 

787 ``document_fields_or_snapshot``. 

788 

789 If the current query already has specified an end cursor -- either 

790 via this method or 

791 :meth:`~google.cloud.firestore_v1.query.Query.end_before` -- this will 

792 overwrite it. 

793 

794 When the query is sent to the server, the ``document_fields_or_snapshot`` will 

795 be used in the order given by fields set by 

796 :meth:`~google.cloud.firestore_v1.query.Query.order_by`. 

797 

798 Args: 

799 document_fields_or_snapshot 

800 (Union[:class:`~google.cloud.firestore_v1.document.DocumentSnapshot`, dict, list, tuple]): 

801 a document snapshot or a dictionary/list/tuple of fields 

802 representing a query results cursor. A cursor is a collection 

803 of values that represent a position in a query result set. 

804 

805 Returns: 

806 :class:`~google.cloud.firestore_v1.query.Query`: 

807 A query with cursor. Acts as a copy of the current query, modified 

808 with the newly added "end at" cursor. 

809 """ 

810 return self._cursor_helper( 

811 document_fields_or_snapshot, before=False, start=False 

812 ) 

813 

814 def _filters_pb(self) -> Optional[StructuredQuery.Filter]: 

815 """Convert all the filters into a single generic Filter protobuf. 

816 

817 This may be a lone field filter or unary filter, may be a composite 

818 filter or may be :data:`None`. 

819 

820 Returns: 

821 :class:`google.cloud.firestore_v1.types.StructuredQuery.Filter`: 

822 A "generic" filter representing the current query's filters. 

823 """ 

824 num_filters = len(self._field_filters) 

825 if num_filters == 0: 

826 return None 

827 elif num_filters == 1: 

828 filter = self._field_filters[0] 

829 if isinstance(filter, query.StructuredQuery.CompositeFilter): 

830 return query.StructuredQuery.Filter(composite_filter=filter) 

831 else: 

832 return _filter_pb(filter) 

833 else: 

834 composite_filter = query.StructuredQuery.CompositeFilter( 

835 op=StructuredQuery.CompositeFilter.Operator.AND, 

836 ) 

837 for filter_ in self._field_filters: 

838 if isinstance(filter_, query.StructuredQuery.CompositeFilter): 

839 composite_filter.filters.append( 

840 query.StructuredQuery.Filter(composite_filter=filter_) 

841 ) 

842 else: 

843 composite_filter.filters.append(_filter_pb(filter_)) 

844 

845 return query.StructuredQuery.Filter(composite_filter=composite_filter) 

846 

847 @staticmethod 

848 def _normalize_projection(projection) -> StructuredQuery.Projection: 

849 """Helper: convert field paths to message.""" 

850 if projection is not None: 

851 fields = list(projection.fields) 

852 

853 if not fields: 

854 field_ref = query.StructuredQuery.FieldReference(field_path="__name__") 

855 return query.StructuredQuery.Projection(fields=[field_ref]) 

856 

857 return projection 

858 

859 def _normalize_orders(self) -> list: 

860 """Helper: adjust orders based on cursors, where clauses.""" 

861 orders = list(self._orders) 

862 _has_snapshot_cursor = False 

863 

864 if self._start_at: 

865 if isinstance(self._start_at[0], document.DocumentSnapshot): 

866 _has_snapshot_cursor = True 

867 

868 if self._end_at: 

869 if isinstance(self._end_at[0], document.DocumentSnapshot): 

870 _has_snapshot_cursor = True 

871 if _has_snapshot_cursor: 

872 # added orders should use direction of last order 

873 last_direction = orders[-1].direction if orders else BaseQuery.ASCENDING 

874 order_keys = [order.field.field_path for order in orders] 

875 for filter_ in self._field_filters: 

876 # FieldFilter.Operator should not compare equal to 

877 # UnaryFilter.Operator, but it does 

878 if isinstance(filter_.op, StructuredQuery.FieldFilter.Operator): 

879 field = filter_.field.field_path 

880 # skip equality filters and filters on fields already ordered 

881 if filter_.op in _INEQUALITY_OPERATORS and field not in order_keys: 

882 orders.append(self._make_order(field, last_direction)) 

883 # add __name__ if not already in orders 

884 if "__name__" not in [order.field.field_path for order in orders]: 

885 orders.append(self._make_order("__name__", last_direction)) 

886 

887 return orders 

888 

889 def _normalize_cursor(self, cursor, orders) -> Optional[Tuple[Any, Any]]: 

890 """Helper: convert cursor to a list of values based on orders.""" 

891 if cursor is None: 

892 return None 

893 

894 if not orders: 

895 raise ValueError(_NO_ORDERS_FOR_CURSOR) 

896 

897 document_fields, before = cursor 

898 

899 order_keys = [order.field.field_path for order in orders] 

900 

901 if isinstance(document_fields, document.DocumentSnapshot): 

902 snapshot = document_fields 

903 document_fields = snapshot.to_dict() 

904 document_fields["__name__"] = snapshot.reference 

905 

906 if isinstance(document_fields, dict): 

907 # Transform to list using orders 

908 values = [] 

909 data = document_fields 

910 

911 # It isn't required that all order by have a cursor. 

912 # However, we need to be sure they are specified in order without gaps 

913 for order_key in order_keys[: len(data)]: 

914 try: 

915 if order_key in data: 

916 values.append(data[order_key]) 

917 else: 

918 values.append( 

919 field_path_module.get_nested_value(order_key, data) 

920 ) 

921 except KeyError: 

922 msg = _MISSING_ORDER_BY.format(order_key, data) 

923 raise ValueError(msg) 

924 

925 document_fields = values 

926 

927 if document_fields and len(document_fields) > len(orders): 

928 msg = _MISMATCH_CURSOR_W_ORDER_BY.format(document_fields, order_keys) 

929 raise ValueError(msg) 

930 

931 _transform_bases = (transforms.Sentinel, transforms._ValueList) 

932 

933 for index, key_field in enumerate(zip(order_keys, document_fields)): 

934 key, field = key_field 

935 

936 if isinstance(field, _transform_bases): 

937 msg = _INVALID_CURSOR_TRANSFORM 

938 raise ValueError(msg) 

939 

940 if key == "__name__" and isinstance(field, str): 

941 document_fields[index] = self._parent.document(field) 

942 

943 return document_fields, before 

944 

945 def _to_protobuf(self) -> StructuredQuery: 

946 """Convert the current query into the equivalent protobuf. 

947 

948 Returns: 

949 :class:`google.cloud.firestore_v1.types.StructuredQuery`: 

950 The query protobuf. 

951 """ 

952 projection = self._normalize_projection(self._projection) 

953 orders = self._normalize_orders() 

954 start_at = self._normalize_cursor(self._start_at, orders) 

955 end_at = self._normalize_cursor(self._end_at, orders) 

956 

957 query_kwargs = { 

958 "select": projection, 

959 "from_": [ 

960 query.StructuredQuery.CollectionSelector( 

961 collection_id=self._parent.id, all_descendants=self._all_descendants 

962 ) 

963 ], 

964 "where": self._filters_pb(), 

965 "order_by": orders, 

966 "start_at": _cursor_pb(start_at), 

967 "end_at": _cursor_pb(end_at), 

968 } 

969 if self._offset is not None: 

970 query_kwargs["offset"] = self._offset 

971 if self._limit is not None: 

972 query_kwargs["limit"] = wrappers_pb2.Int32Value(value=self._limit) 

973 return query.StructuredQuery(**query_kwargs) 

974 

975 def count( 

976 self, alias: str | None = None 

977 ) -> Type["firestore_v1.base_aggregation.BaseAggregationQuery"]: 

978 raise NotImplementedError 

979 

980 def sum( 

981 self, field_ref: str | FieldPath, alias: str | None = None 

982 ) -> Type["firestore_v1.base_aggregation.BaseAggregationQuery"]: 

983 raise NotImplementedError 

984 

985 def avg( 

986 self, field_ref: str | FieldPath, alias: str | None = None 

987 ) -> Type["firestore_v1.base_aggregation.BaseAggregationQuery"]: 

988 raise NotImplementedError 

989 

990 def get( 

991 self, 

992 transaction=None, 

993 retry: Optional[retries.Retry] = None, 

994 timeout: Optional[float] = None, 

995 ) -> Iterable[DocumentSnapshot]: 

996 raise NotImplementedError 

997 

998 def _prep_stream( 

999 self, 

1000 transaction=None, 

1001 retry: Optional[retries.Retry] = None, 

1002 timeout: Optional[float] = None, 

1003 ) -> Tuple[dict, str, dict]: 

1004 """Shared setup for async / sync :meth:`stream`""" 

1005 if self._limit_to_last: 

1006 raise ValueError( 

1007 "Query results for queries that include limit_to_last() " 

1008 "constraints cannot be streamed. Use Query.get() instead." 

1009 ) 

1010 

1011 parent_path, expected_prefix = self._parent._parent_info() 

1012 request = { 

1013 "parent": parent_path, 

1014 "structured_query": self._to_protobuf(), 

1015 "transaction": _helpers.get_transaction_id(transaction), 

1016 } 

1017 kwargs = _helpers.make_retry_timeout_kwargs(retry, timeout) 

1018 

1019 return request, expected_prefix, kwargs 

1020 

1021 def stream( 

1022 self, 

1023 transaction=None, 

1024 retry: Optional[retries.Retry] = None, 

1025 timeout: Optional[float] = None, 

1026 ) -> Generator[document.DocumentSnapshot, Any, None]: 

1027 raise NotImplementedError 

1028 

1029 def on_snapshot(self, callback) -> NoReturn: 

1030 raise NotImplementedError 

1031 

1032 def recursive(self: QueryType) -> QueryType: 

1033 """Returns a copy of this query whose iterator will yield all matching 

1034 documents as well as each of their descendent subcollections and documents. 

1035 

1036 This differs from the `all_descendents` flag, which only returns descendents 

1037 whose subcollection names match the parent collection's name. To return 

1038 all descendents, regardless of their subcollection name, use this. 

1039 """ 

1040 copied = self._copy(recursive=True, all_descendants=True) 

1041 if copied._parent and copied._parent.id: 

1042 original_collection_id = "/".join(copied._parent._path) 

1043 

1044 # Reset the parent to nothing so we can recurse through the entire 

1045 # database. This is required to have 

1046 # `CollectionSelector.collection_id` not override 

1047 # `CollectionSelector.all_descendants`, which happens if both are 

1048 # set. 

1049 copied._parent = copied._get_collection_reference_class()("") 

1050 copied._parent._client = self._parent._client 

1051 

1052 # But wait! We don't want to load the entire database; only the 

1053 # collection the user originally specified. To accomplish that, we 

1054 # add the following arcane filters. 

1055 

1056 REFERENCE_NAME_MIN_ID = "__id-9223372036854775808__" 

1057 start_at = f"{original_collection_id}/{REFERENCE_NAME_MIN_ID}" 

1058 

1059 # The backend interprets this null character is flipping the filter 

1060 # to mean the end of the range instead of the beginning. 

1061 nullChar = "\0" 

1062 end_at = f"{original_collection_id}{nullChar}/{REFERENCE_NAME_MIN_ID}" 

1063 

1064 copied = ( 

1065 copied.order_by(field_path_module.FieldPath.document_id()) 

1066 .start_at({field_path_module.FieldPath.document_id(): start_at}) 

1067 .end_at({field_path_module.FieldPath.document_id(): end_at}) 

1068 ) 

1069 

1070 return copied 

1071 

1072 def _comparator(self, doc1, doc2) -> int: 

1073 _orders = self._orders 

1074 

1075 # Add implicit sorting by name, using the last specified direction. 

1076 if len(_orders) == 0: 

1077 lastDirection = BaseQuery.ASCENDING 

1078 else: 

1079 if _orders[-1].direction == 1: 

1080 lastDirection = BaseQuery.ASCENDING 

1081 else: 

1082 lastDirection = BaseQuery.DESCENDING 

1083 

1084 orderBys = list(_orders) 

1085 

1086 order_pb = query.StructuredQuery.Order( 

1087 field=query.StructuredQuery.FieldReference(field_path="id"), 

1088 direction=_enum_from_direction(lastDirection), 

1089 ) 

1090 orderBys.append(order_pb) 

1091 

1092 for orderBy in orderBys: 

1093 if orderBy.field.field_path == "id": 

1094 # If ordering by document id, compare resource paths. 

1095 comp = Order()._compare_to(doc1.reference._path, doc2.reference._path) 

1096 else: 

1097 if ( 

1098 orderBy.field.field_path not in doc1._data 

1099 or orderBy.field.field_path not in doc2._data 

1100 ): 

1101 raise ValueError( 

1102 "Can only compare fields that exist in the " 

1103 "DocumentSnapshot. Please include the fields you are " 

1104 "ordering on in your select() call." 

1105 ) 

1106 v1 = doc1._data[orderBy.field.field_path] 

1107 v2 = doc2._data[orderBy.field.field_path] 

1108 encoded_v1 = _helpers.encode_value(v1) 

1109 encoded_v2 = _helpers.encode_value(v2) 

1110 comp = Order().compare(encoded_v1, encoded_v2) 

1111 

1112 if comp != 0: 

1113 # 1 == Ascending, -1 == Descending 

1114 return orderBy.direction * comp 

1115 

1116 return 0 

1117 

1118 

1119def _enum_from_op_string(op_string: str) -> int: 

1120 """Convert a string representation of a binary operator to an enum. 

1121 

1122 These enums come from the protobuf message definition 

1123 ``StructuredQuery.FieldFilter.Operator``. 

1124 

1125 Args: 

1126 op_string (str): A comparison operation in the form of a string. 

1127 Acceptable values are ``<``, ``<=``, ``==``, ``!=``, ``>=`` 

1128 and ``>``. 

1129 

1130 Returns: 

1131 int: The enum corresponding to ``op_string``. 

1132 

1133 Raises: 

1134 ValueError: If ``op_string`` is not a valid operator. 

1135 """ 

1136 try: 

1137 return _COMPARISON_OPERATORS[op_string] 

1138 except KeyError: 

1139 choices = ", ".join(sorted(_COMPARISON_OPERATORS.keys())) 

1140 msg = _BAD_OP_STRING.format(op_string, choices) 

1141 raise ValueError(msg) 

1142 

1143 

1144def _isnan(value) -> bool: 

1145 """Check if a value is NaN. 

1146 

1147 This differs from ``math.isnan`` in that **any** input type is 

1148 allowed. 

1149 

1150 Args: 

1151 value (Any): A value to check for NaN-ness. 

1152 

1153 Returns: 

1154 bool: Indicates if the value is the NaN float. 

1155 """ 

1156 if isinstance(value, float): 

1157 return math.isnan(value) 

1158 else: 

1159 return False 

1160 

1161 

1162def _enum_from_direction(direction: str) -> int: 

1163 """Convert a string representation of a direction to an enum. 

1164 

1165 Args: 

1166 direction (str): A direction to order by. Must be one of 

1167 :attr:`~google.cloud.firestore.BaseQuery.ASCENDING` or 

1168 :attr:`~google.cloud.firestore.BaseQuery.DESCENDING`. 

1169 

1170 Returns: 

1171 int: The enum corresponding to ``direction``. 

1172 

1173 Raises: 

1174 ValueError: If ``direction`` is not a valid direction. 

1175 """ 

1176 if isinstance(direction, int): 

1177 return direction 

1178 

1179 if direction == BaseQuery.ASCENDING: 

1180 return StructuredQuery.Direction.ASCENDING 

1181 elif direction == BaseQuery.DESCENDING: 

1182 return StructuredQuery.Direction.DESCENDING 

1183 else: 

1184 msg = _BAD_DIR_STRING.format( 

1185 direction, BaseQuery.ASCENDING, BaseQuery.DESCENDING 

1186 ) 

1187 raise ValueError(msg) 

1188 

1189 

1190def _filter_pb(field_or_unary) -> StructuredQuery.Filter: 

1191 """Convert a specific protobuf filter to the generic filter type. 

1192 

1193 Args: 

1194 field_or_unary (Union[google.cloud.proto.firestore.v1.\ 

1195 query.StructuredQuery.FieldFilter, google.cloud.proto.\ 

1196 firestore.v1.query.StructuredQuery.FieldFilter]): A 

1197 field or unary filter to convert to a generic filter. 

1198 

1199 Returns: 

1200 google.cloud.firestore_v1.types.\ 

1201 StructuredQuery.Filter: A "generic" filter. 

1202 

1203 Raises: 

1204 ValueError: If ``field_or_unary`` is not a field or unary filter. 

1205 """ 

1206 if isinstance(field_or_unary, query.StructuredQuery.FieldFilter): 

1207 return query.StructuredQuery.Filter(field_filter=field_or_unary) 

1208 elif isinstance(field_or_unary, query.StructuredQuery.UnaryFilter): 

1209 return query.StructuredQuery.Filter(unary_filter=field_or_unary) 

1210 else: 

1211 raise ValueError("Unexpected filter type", type(field_or_unary), field_or_unary) 

1212 

1213 

1214def _cursor_pb(cursor_pair: Tuple[list, bool]) -> Optional[Cursor]: 

1215 """Convert a cursor pair to a protobuf. 

1216 

1217 If ``cursor_pair`` is :data:`None`, just returns :data:`None`. 

1218 

1219 Args: 

1220 cursor_pair (Optional[Tuple[list, bool]]): Two-tuple of 

1221 

1222 * a list of field values. 

1223 * a ``before`` flag 

1224 

1225 Returns: 

1226 Optional[google.cloud.firestore_v1.types.Cursor]: A 

1227 protobuf cursor corresponding to the values. 

1228 """ 

1229 if cursor_pair is not None: 

1230 data, before = cursor_pair 

1231 value_pbs = [_helpers.encode_value(value) for value in data] 

1232 return query.Cursor(values=value_pbs, before=before) 

1233 

1234 

1235def _query_response_to_snapshot( 

1236 response_pb: RunQueryResponse, collection, expected_prefix: str 

1237) -> Optional[document.DocumentSnapshot]: 

1238 """Parse a query response protobuf to a document snapshot. 

1239 

1240 Args: 

1241 response_pb (google.cloud.proto.firestore.v1.\ 

1242 firestore.RunQueryResponse): A 

1243 collection (:class:`~google.cloud.firestore_v1.collection.CollectionReference`): 

1244 A reference to the collection that initiated the query. 

1245 expected_prefix (str): The expected prefix for fully-qualified 

1246 document names returned in the query results. This can be computed 

1247 directly from ``collection`` via :meth:`_parent_info`. 

1248 

1249 Returns: 

1250 Optional[:class:`~google.cloud.firestore.document.DocumentSnapshot`]: 

1251 A snapshot of the data returned in the query. If 

1252 ``response_pb.document`` is not set, the snapshot will be :data:`None`. 

1253 """ 

1254 if not response_pb._pb.HasField("document"): 

1255 return None 

1256 

1257 document_id = _helpers.get_doc_id(response_pb.document, expected_prefix) 

1258 reference = collection.document(document_id) 

1259 data = _helpers.decode_dict(response_pb.document.fields, collection._client) 

1260 snapshot = document.DocumentSnapshot( 

1261 reference, 

1262 data, 

1263 exists=True, 

1264 read_time=response_pb.read_time, 

1265 create_time=response_pb.document.create_time, 

1266 update_time=response_pb.document.update_time, 

1267 ) 

1268 return snapshot 

1269 

1270 

1271def _collection_group_query_response_to_snapshot( 

1272 response_pb: RunQueryResponse, collection 

1273) -> Optional[document.DocumentSnapshot]: 

1274 """Parse a query response protobuf to a document snapshot. 

1275 

1276 Args: 

1277 response_pb (google.cloud.proto.firestore.v1.\ 

1278 firestore.RunQueryResponse): A 

1279 collection (:class:`~google.cloud.firestore_v1.collection.CollectionReference`): 

1280 A reference to the collection that initiated the query. 

1281 

1282 Returns: 

1283 Optional[:class:`~google.cloud.firestore.document.DocumentSnapshot`]: 

1284 A snapshot of the data returned in the query. If 

1285 ``response_pb.document`` is not set, the snapshot will be :data:`None`. 

1286 """ 

1287 if not response_pb._pb.HasField("document"): 

1288 return None 

1289 reference = collection._client.document(response_pb.document.name) 

1290 data = _helpers.decode_dict(response_pb.document.fields, collection._client) 

1291 snapshot = document.DocumentSnapshot( 

1292 reference, 

1293 data, 

1294 exists=True, 

1295 read_time=response_pb._pb.read_time, 

1296 create_time=response_pb._pb.document.create_time, 

1297 update_time=response_pb._pb.document.update_time, 

1298 ) 

1299 return snapshot 

1300 

1301 

1302class BaseCollectionGroup(BaseQuery): 

1303 """Represents a Collection Group in the Firestore API. 

1304 

1305 This is a specialization of :class:`.Query` that includes all documents in the 

1306 database that are contained in a collection or subcollection of the given 

1307 parent. 

1308 

1309 Args: 

1310 parent (:class:`~google.cloud.firestore_v1.collection.CollectionReference`): 

1311 The collection that this query applies to. 

1312 """ 

1313 

1314 _PARTITION_QUERY_ORDER = ( 

1315 BaseQuery._make_order( 

1316 field_path_module.FieldPath.document_id(), 

1317 BaseQuery.ASCENDING, 

1318 ), 

1319 ) 

1320 

1321 def __init__( 

1322 self, 

1323 parent, 

1324 projection=None, 

1325 field_filters=(), 

1326 orders=(), 

1327 limit=None, 

1328 limit_to_last=False, 

1329 offset=None, 

1330 start_at=None, 

1331 end_at=None, 

1332 all_descendants=True, 

1333 recursive=False, 

1334 ) -> None: 

1335 if not all_descendants: 

1336 raise ValueError("all_descendants must be True for collection group query.") 

1337 

1338 super(BaseCollectionGroup, self).__init__( 

1339 parent=parent, 

1340 projection=projection, 

1341 field_filters=field_filters, 

1342 orders=orders, 

1343 limit=limit, 

1344 limit_to_last=limit_to_last, 

1345 offset=offset, 

1346 start_at=start_at, 

1347 end_at=end_at, 

1348 all_descendants=all_descendants, 

1349 recursive=recursive, 

1350 ) 

1351 

1352 def _validate_partition_query(self): 

1353 if self._field_filters: 

1354 raise ValueError("Can't partition query with filters.") 

1355 

1356 if self._projection: 

1357 raise ValueError("Can't partition query with projection.") 

1358 

1359 if self._limit: 

1360 raise ValueError("Can't partition query with limit.") 

1361 

1362 if self._offset: 

1363 raise ValueError("Can't partition query with offset.") 

1364 

1365 def _get_query_class(self): 

1366 raise NotImplementedError 

1367 

1368 def _prep_get_partitions( 

1369 self, 

1370 partition_count, 

1371 retry: Optional[retries.Retry] = None, 

1372 timeout: Optional[float] = None, 

1373 ) -> Tuple[dict, dict]: 

1374 self._validate_partition_query() 

1375 parent_path, expected_prefix = self._parent._parent_info() 

1376 klass = self._get_query_class() 

1377 query = klass( 

1378 self._parent, 

1379 orders=self._PARTITION_QUERY_ORDER, 

1380 start_at=self._start_at, 

1381 end_at=self._end_at, 

1382 all_descendants=self._all_descendants, 

1383 ) 

1384 request = { 

1385 "parent": parent_path, 

1386 "structured_query": query._to_protobuf(), 

1387 "partition_count": partition_count, 

1388 } 

1389 kwargs = _helpers.make_retry_timeout_kwargs(retry, timeout) 

1390 

1391 return request, kwargs 

1392 

1393 def get_partitions( 

1394 self, 

1395 partition_count, 

1396 retry: Optional[retries.Retry] = None, 

1397 timeout: Optional[float] = None, 

1398 ) -> NoReturn: 

1399 raise NotImplementedError 

1400 

1401 @staticmethod 

1402 def _get_collection_reference_class() -> Type["BaseCollectionGroup"]: 

1403 raise NotImplementedError 

1404 

1405 

1406class QueryPartition: 

1407 """Represents a bounded partition of a collection group query. 

1408 

1409 Contains cursors that can be used in a query as a starting and/or end point for the 

1410 collection group query. The cursors may only be used in a query that matches the 

1411 constraints of the query that produced this partition. 

1412 

1413 Args: 

1414 query (BaseQuery): The original query that this is a partition of. 

1415 start_at (Optional[~google.cloud.firestore_v1.document.DocumentSnapshot]): 

1416 Cursor for first query result to include. If `None`, the partition starts at 

1417 the beginning of the result set. 

1418 end_at (Optional[~google.cloud.firestore_v1.document.DocumentSnapshot]): 

1419 Cursor for first query result after the last result included in the 

1420 partition. If `None`, the partition runs to the end of the result set. 

1421 

1422 """ 

1423 

1424 def __init__(self, query, start_at, end_at): 

1425 self._query = query 

1426 self._start_at = start_at 

1427 self._end_at = end_at 

1428 

1429 @property 

1430 def start_at(self): 

1431 return self._start_at 

1432 

1433 @property 

1434 def end_at(self): 

1435 return self._end_at 

1436 

1437 def query(self): 

1438 """Generate a new query using this partition's bounds. 

1439 

1440 Returns: 

1441 BaseQuery: Copy of the original query with start and end bounds set by the 

1442 cursors from this partition. 

1443 """ 

1444 query = self._query 

1445 start_at = ([self.start_at], True) if self.start_at else None 

1446 end_at = ([self.end_at], True) if self.end_at else None 

1447 

1448 return type(query)( 

1449 query._parent, 

1450 all_descendants=query._all_descendants, 

1451 orders=query._PARTITION_QUERY_ORDER, 

1452 start_at=start_at, 

1453 end_at=end_at, 

1454 )