Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/db/query.py: 7%

362 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-13 11:04 +0000

1from __future__ import annotations 

2 

3import base64 

4import copy 

5import functools 

6import logging 

7import typing as t 

8 

9from viur.core.config import conf 

10from .transport import count, get, run_single_filter 

11from .types import ( 

12 DATASTORE_BASE_TYPES, 

13 Entity, 

14 KEY_SPECIAL_PROPERTY, 

15 QueryDefinition, 

16 SortOrder, 

17 TFilters, 

18 TOrders, 

19) 

20from . import utils 

21 

22if t.TYPE_CHECKING: 22 ↛ 23line 22 didn't jump to line 23 because the condition on line 22 was never true

23 from viur.core.skeleton import SkeletonInstance, SkelList 

24 

25TOrderHook = t.TypeVar("TOrderHook", bound=t.Callable[["Query", TOrders], TOrders]) 

26TFilterHook = t.TypeVar("TFilterHook", bound=t.Callable[ 

27 ["Query", str, DATASTORE_BASE_TYPES | list[DATASTORE_BASE_TYPES]], TFilters 

28]) 

29 

30 

31def _entryMatchesQuery(entry: Entity, singleFilter: dict) -> bool: 

32 """ 

33 Utility function which checks if the given entity could have been returned by a query filtering by the 

34 properties in singleFilter. This can be used if a list of entities have been retrieved (e.g. by a 3rd party 

35 full text search engine) and these have now to be checked against the filter returned by their modules 

36 :meth:`viur.core.prototypes.list.listFilter` method. 

37 :param entry: The entity which will be tested 

38 :param singleFilter: A dictionary containing all the filters from the query 

39 :return: True if the entity could have been returned by such an query, False otherwise 

40 """ 

41 

42 def doesMatch(entryValue: t.Any, requestedValue: t.Any, opcode: str) -> bool: 

43 if isinstance(entryValue, list): 

44 return any([doesMatch(x, requestedValue, opcode) for x in entryValue]) 

45 if opcode == "=" and entryValue == requestedValue: 

46 return True 

47 elif opcode == "<" and entryValue < requestedValue: 

48 return True 

49 elif opcode == ">" and entryValue > requestedValue: 

50 return True 

51 elif opcode == "<=" and entryValue <= requestedValue: 

52 return True 

53 elif opcode == ">=" and entryValue >= requestedValue: 

54 return True 

55 return False 

56 

57 for filterStr, filterValue in singleFilter.items(): 

58 field, opcode = filterStr.split(" ") 

59 entryValue = entry.get(field) 

60 if not doesMatch(entryValue, filterValue, opcode): 

61 return False 

62 return True 

63 

64 

65class Query(object): 

66 """ 

67 Base Class for querying the datastore. Its API is similar to the google.cloud.datastore.query API, 

68 but it provides the necessary hooks for relational or random queries, the fulltext search as well as support 

69 for IN filters. 

70 """ 

71 

72 def __init__(self, kind: str, srcSkelClass: t.Union["SkeletonInstance", None] = None, *args, **kwargs): 

73 """ 

74 Constructs a new Query. 

75 :param kind: The kind to run this query on. This may be later overridden to run on a different kind (like 

76 viur-relations), but it's guaranteed to return only entities of that kind. 

77 :param srcSkelClass: If set, enables data-model depended queries (like relational queries) as well as the 

78 :meth:fetch method 

79 """ 

80 super().__init__() 

81 self.kind = kind 

82 self.srcSkel = srcSkelClass 

83 self.queries: t.Union[None, QueryDefinition, t.List[QueryDefinition]] = QueryDefinition(kind, {}, []) 

84 self._filterHook: TFilterHook | None = None 

85 self._orderHook: TOrderHook | None = None 

86 # Sometimes, the default merge functionality from MultiQuery is not sufficient 

87 self._customMultiQueryMerge: t.Union[None, t.Callable[[Query, t.List[t.List[Entity]], int], t.List[Entity]]] \ 

88 = None 

89 # Some (Multi-)Queries need a different amount of results per subQuery than actually returned 

90 self._calculateInternalMultiQueryLimit: t.Union[None, t.Callable[[Query, int], int]] = None 

91 # Allow carrying custom data along with the query. 

92 # Currently only used by SpatialBone to record the guaranteed correctness 

93 self.customQueryInfo = {} 

94 self.origKind = kind 

95 self._lastEntry = None 

96 self._fulltextQueryString: t.Union[None, str] = None 

97 self.lastCursor = None 

98 # if not kind.startswith("viur") and not kwargs.get("_excludeFromAccessLog"): 

99 # accessLog = current_db_access_log.get() 

100 # if isinstance(accessLog, set): 

101 # accessLog.add(kind) 

102 

103 def setFilterHook(self, hook: TFilterHook) -> TFilterHook | None: 

104 """ 

105 Installs *hook* as a callback function for new filters. 

106 

107 *hook* will be called each time a new filter constrain is added to the query. 

108 This allows e.g. the relationalBone to rewrite constrains added after the initial 

109 processing of the query has been done (e.g. by ``listFilter()`` methods). 

110 

111 :param hook: The function to register as callback. 

112 A value of None removes the currently active hook. 

113 :returns: The previously registered hook (if any), or None. 

114 """ 

115 old = self._filterHook 

116 self._filterHook = hook 

117 return old 

118 

119 def setOrderHook(self, hook: TOrderHook) -> TOrderHook | None: 

120 """ 

121 Installs *hook* as a callback function for new orderings. 

122 

123 *hook* will be called each time a :func:`db.Query.order` is called on this query. 

124 

125 :param hook: The function to register as callback. 

126 A value of None removes the currently active hook. 

127 :returns: The previously registered hook (if any), or None. 

128 """ 

129 old = self._orderHook 

130 self._orderHook = hook 

131 return old 

132 

133 def mergeExternalFilter(self, filters: dict) -> t.Self: 

134 """ 

135 Safely merges filters according to the data model. 

136 

137 Its only valid to call this function if the query has been created using 

138 :func:`core.skeleton.Skeleton.all`. 

139 

140 It's safe to pass filters received from an external source (a user); 

141 unknown/invalid filters will be ignored, so the query-object is kept in a 

142 valid state even when processing malformed data. 

143 

144 If complex queries are needed (e.g. filter by relations), this function 

145 shall also be used. 

146 

147 See also :meth:`filter` for simple filters. 

148 

149 :param filters: A dictionary of attributes and filter pairs. 

150 :returns: Returns the query itself for chaining. 

151 """ 

152 if self.srcSkel is None: 

153 raise NotImplementedError("This query has not been created using skel.all()") 

154 

155 if self.queries is None: # This query is already unsatisfiable and adding more constraints won't change this 

156 return self 

157 

158 skel = self.srcSkel 

159 

160 if "search" in filters: 

161 if self.srcSkel.customDatabaseAdapter and self.srcSkel.customDatabaseAdapter.providesFulltextSearch: 

162 self._fulltextQueryString = str(filters["search"]) 

163 else: 

164 logging.warning( 

165 "Got a fulltext search query for %s which does not have a suitable customDatabaseAdapter" 

166 % self.srcSkel.kindName 

167 ) 

168 self.queries = None 

169 

170 bones = [(y, x) for x, y in skel.items()] 

171 

172 try: 

173 # Process filters first 

174 for bone, key in bones: 

175 bone.buildDBFilter(key, skel, self, filters) 

176 

177 # Parse orders 

178 for bone, key in bones: 

179 bone.buildDBSort(key, skel, self, filters) 

180 

181 except RuntimeError as e: 

182 logging.exception(e) 

183 self.queries = None 

184 return self 

185 

186 startCursor = endCursor = None 

187 

188 if "cursor" in filters and filters["cursor"] and filters["cursor"].lower() != "none": 

189 startCursor = filters["cursor"] 

190 

191 if "endcursor" in filters and filters["endcursor"] and filters["endcursor"].lower() != "none": 

192 endCursor = filters["endcursor"] 

193 

194 if startCursor or endCursor: 

195 self.setCursor(startCursor, endCursor) 

196 

197 if limit := filters.get("limit"): 

198 try: 

199 limit = int(limit) 

200 

201 # disallow limit beyond conf.db.query_external_limit 

202 if limit > conf.db.query_external_limit: 

203 limit = conf.db.query_external_limit 

204 

205 # forbid any limit < 0, which might bypass defaults 

206 if limit < 0: 

207 limit = 0 

208 

209 self.limit(limit) 

210 except ValueError: 

211 pass # ignore this 

212 

213 return self 

214 

215 def filter(self, prop: str, value: DATASTORE_BASE_TYPES | list[DATASTORE_BASE_TYPES]) -> t.Self: 

216 """ 

217 Adds a new constraint to this query. 

218 

219 See also :meth:`mergeExternalFilter` for a safer filter implementation. 

220 

221 :param prop: Name of the property + operation we'll filter by 

222 :param value: The value of that filter. 

223 :returns: Returns the query itself for chaining. 

224 """ 

225 if self.queries is None: 

226 # This query is already unsatisfiable and adding more constrains to this won't change this 

227 return self 

228 if self._filterHook is not None: 

229 try: 

230 r = self._filterHook(self, prop, value) 

231 except RuntimeError: 

232 self.queries = None 

233 return self 

234 if r is None: 

235 # The Hook did something special directly on 'self' to apply that filter, 

236 # no need for us to do anything 

237 return self 

238 prop, value = r 

239 if " " not in prop: 

240 # Ensure that an equality filter is explicitly postfixed with " =" 

241 field = prop 

242 op = "=" 

243 else: 

244 field, op = prop.split(" ") 

245 if op.lower() in {"!=", "in"}: 

246 if isinstance(self.queries, list): 

247 raise NotImplementedError("You cannot use multiple IN or != filter") 

248 origQuery = self.queries 

249 self.queries = [] 

250 if op == "!=": 

251 newFilter = copy.deepcopy(origQuery) 

252 newFilter.filters[f"{field} <"] = value 

253 self.queries.append(newFilter) 

254 newFilter = copy.deepcopy(origQuery) 

255 newFilter.filters[f"{field} >"] = value 

256 self.queries.append(newFilter) 

257 else: # IN filter 

258 if not isinstance(value, (list, tuple)): 

259 raise ValueError("Value must be list or tuple if using IN filter!") 

260 for val in value: 

261 newFilter = copy.deepcopy(origQuery) 

262 newFilter.filters[f"{field} ="] = val 

263 self.queries.append(newFilter) 

264 else: 

265 filterStr = f"{field} {op}" 

266 if isinstance(self.queries, list): 

267 for singeFilter in self.queries: 

268 if filterStr not in singeFilter.filters: 

269 singeFilter.filters[filterStr] = value 

270 else: 

271 if not isinstance(singeFilter.filters[filterStr]): 

272 singeFilter.filters[filterStr] = [singeFilter.filters[filterStr]] 

273 singeFilter.filters[filterStr].append(value) 

274 else: # It must be still a dict (we tested for None already above) 

275 if filterStr not in self.queries.filters: 

276 self.queries.filters[filterStr] = value 

277 else: 

278 if not isinstance(self.queries.filters[filterStr], list): 

279 self.queries.filters[filterStr] = [self.queries.filters[filterStr]] 

280 self.queries.filters[filterStr].append(value) 

281 if op in {"<", "<=", ">", ">="}: 

282 if isinstance(self.queries, list): 

283 for queryObj in self.queries: 

284 if not queryObj.orders or queryObj.orders[0][0] != field: 

285 queryObj.orders = [(field, SortOrder.Ascending)] + (queryObj.orders or []) 

286 else: 

287 if not self.queries.orders or self.queries.orders[0][0] != field: 

288 self.queries.orders = [(field, SortOrder.Ascending)] + (self.queries.orders or []) 

289 return self 

290 

291 def order(self, *orderings: t.Tuple[str, SortOrder]) -> t.Self: 

292 """ 

293 Specify a query sorting. 

294 

295 Resulting entities will be sorted by the first property argument, then by the 

296 second, and so on. 

297 

298 The following example 

299 

300 .. code-block:: python 

301 

302 query = Query("Person") 

303 query.order(("bday" db.SortOrder.Ascending), ("age", db.SortOrder.Descending)) 

304 

305 sorts every Person in order of their birthday, starting with January 1. 

306 People with the same birthday are sorted by age, oldest to youngest. 

307 

308 

309 ``order()`` may be called multiple times. Each call resets the sort order 

310 from scratch. 

311 

312 If an inequality filter exists in this Query it must be the first property 

313 passed to ``order()``. t.Any number of sort orders may be used after the 

314 inequality filter property. Without inequality filters, any number of 

315 filters with different orders may be specified. 

316 

317 Entities with multiple values for an order property are sorted by their 

318 lowest value. 

319 

320 Note that a sort order implies an existence filter! In other words, 

321 Entities without the sort order property are filtered out, and *not* 

322 included in the query results. 

323 

324 If the sort order property has different types in different entities - 

325 e.g. if bob["id"] is an int and fred["id"] is a string - the entities will be 

326 grouped first by the property type, then sorted within type. No attempt is 

327 made to compare property values across types. 

328 

329 

330 :param orderings: The properties to sort by, in sort order. 

331 Each argument must be a (name, direction) 2-tuple. 

332 :returns: Returns the query itself for chaining. 

333 """ 

334 if self.queries is None: 

335 # This Query is unsatisfiable - don't try to bother 

336 return self 

337 

338 # Check for correct order subscript 

339 orders = [] 

340 for order in orderings: 

341 if isinstance(order, str): 

342 order = (order, SortOrder.Ascending) 

343 

344 if not (isinstance(order[0], str) and isinstance(order[1], SortOrder)): 

345 raise TypeError( 

346 f"Invalid ordering {order}, it has to be a tuple. Try: `(\"{order}\", SortOrder.Ascending)`") 

347 

348 orders.append(order) 

349 

350 if self._orderHook is not None: 

351 try: 

352 orders = self._orderHook(self, orders) 

353 except RuntimeError: 

354 self.queries = None 

355 return self 

356 if orders is None: 

357 return self 

358 

359 if isinstance(self.queries, list): 

360 for query in self.queries: 

361 query.orders = list(orders) 

362 else: 

363 self.queries.orders = list(orders) 

364 

365 return self 

366 

367 def setCursor(self, startCursor: str, endCursor: t.Optional[str] = None) -> t.Self: 

368 """ 

369 Sets the start and optionally end cursor for this query. 

370 

371 The result set will only include results between these cursors. 

372 The cursor is generated by an earlier query with exactly the same configuration. 

373 

374 It's safe to use client-supplied cursors, a cursor can't be abused to access entities 

375 which don't match the current filters. 

376 

377 :param startCursor: The start cursor for this query. 

378 :param endCursor: The end cursor for this query. 

379 :returns: Returns the query itself for chaining. 

380 """ 

381 if isinstance(self.queries, list): 

382 for query in self.queries: 

383 assert isinstance(query, QueryDefinition) 

384 if startCursor: 

385 query.startCursor = base64.urlsafe_b64decode(startCursor.encode("ASCII")).decode("ASCII") 

386 if endCursor: 

387 query.endCursor = base64.urlsafe_b64decode(endCursor.encode("ASCII")).decode("ASCII") 

388 else: 

389 assert isinstance(self.queries, QueryDefinition) 

390 if startCursor: 

391 self.queries.startCursor = base64.urlsafe_b64decode(startCursor.encode("ASCII")).decode("ASCII") 

392 if endCursor: 

393 self.queries.endCursor = base64.urlsafe_b64decode(endCursor.encode("ASCII")).decode("ASCII") 

394 return self 

395 

396 def limit(self, limit: int) -> t.Self: 

397 """ 

398 Sets the query limit to *limit* entities in the result. 

399 

400 :param limit: The maximum number of entities per batch. 

401 :returns: Returns the query itself for chaining. 

402 """ 

403 if isinstance(self.queries, QueryDefinition): 

404 self.queries.limit = limit 

405 elif isinstance(self.queries, list): 

406 for query in self.queries: 

407 query.limit = limit 

408 

409 return self 

410 

411 def distinctOn(self, keyList: t.List[str]) -> t.Self: 

412 """ 

413 Ensure only entities with distinct values on the fields listed are returned. 

414 This will implicitly override your SortOrder as all fields listed in keyList have to be sorted first. 

415 """ 

416 if isinstance(self.queries, QueryDefinition): 

417 self.queries.distinct = keyList 

418 elif isinstance(self.queries, list): 

419 for query in self.queries: 

420 query.distinct = keyList 

421 return self 

422 

423 def getCursor(self) -> t.Optional[str]: 

424 """ 

425 Get a valid cursor from the last run of this query. 

426 

427 The source of this cursor varies depending on what the last call was: 

428 - :meth:`run`: A cursor that points immediately behind the 

429 last result pulled off the returned iterator. 

430 - :meth:`get`: A cursor that points immediately behind the 

431 last result in the returned list. 

432 

433 :returns: A cursor that can be used in subsequent query requests or None if that query does not support 

434 cursors or there are no more elements to fetch 

435 """ 

436 if isinstance(self.queries, QueryDefinition): 

437 q = self.queries 

438 elif isinstance(self.queries, list): 

439 for query in self.queries: 

440 if query.currentCursor: 

441 q = query 

442 break 

443 else: 

444 q = self.queries[0] 

445 return base64.urlsafe_b64encode(q.currentCursor).decode("ASCII") if q.currentCursor else None 

446 

447 def get_orders(self) -> t.List[t.Tuple[str, SortOrder]] | None: 

448 """ 

449 Get the orders from this query. 

450 

451 :returns: The orders form this query as a list if there is no orders set it returns None 

452 """ 

453 q = self.queries 

454 

455 if isinstance(q, (list, tuple)): 

456 q = q[0] 

457 

458 if not isinstance(q, QueryDefinition): 

459 raise ValueError( 

460 f"self.queries can only be a 'QueryDefinition' or a list of, but found {self.queries!r}" 

461 ) 

462 

463 return q.orders or None 

464 

465 # TODO We need this the kind is already public. 

466 def getKind(self) -> str: 

467 """ 

468 :returns: the *current* kind of this query. 

469 This may not be the kind this query has been constructed with 

470 as relational bones may rewrite this. 

471 """ 

472 return self.kind 

473 

474 def _run_single_filter_query(self, query: QueryDefinition, limit: int) -> t.List[Entity]: 

475 """ 

476 Internal helper function that runs a single query definition on the datastore and returns a list of 

477 entities found. 

478 :param query: The querydefinition (filters, orders, distinct etc.) to run against the datastore 

479 :param limit: How many results should at most be returned 

480 :return: The first *limit* entities that matches this query 

481 """ 

482 return run_single_filter(query, limit) 

483 

484 def _merge_multi_query_results(self, input_result: t.List[t.List[Entity]]) -> t.List[Entity]: 

485 """ 

486 Merge the lists of entries into a single list; removing duplicates and restoring sort-order 

487 :param input_result: Nested Lists of Entries returned by each individual query run 

488 :return: Sorted & deduplicated list of entries 

489 """ 

490 seen_keys = set() 

491 res = [] 

492 for subList in input_result: 

493 for entry in subList: 

494 key = entry.key 

495 if key in seen_keys: 

496 continue 

497 seen_keys.add(key) 

498 res.append(entry) 

499 # FIXME: What about filters that mix different inequality filters? 

500 # Currently, we'll now simply ignore any implicit sortorder. 

501 return self._resort_result(res, {}, self.queries[0].orders) 

502 

503 def _resort_result( 

504 self, 

505 entities: t.List[Entity], 

506 filters: t.Dict[str, DATASTORE_BASE_TYPES], 

507 orders: t.List[t.Tuple[str, SortOrder]], 

508 ) -> t.List[Entity]: 

509 """ 

510 Internal helper that takes a (deduplicated) list of entities that has been fetched from different internal 

511 queries (the datastore does not support IN filters itself, so we have to query each item in that array 

512 separately) and resorts the list so it matches the query again. 

513 

514 :param entities: t.List of entities to resort 

515 :param filters: The filter used in the query (used to determine implicit sort order by an inequality filter) 

516 :param orders: The sort-orders to apply 

517 :return: The sorted list 

518 """ 

519 

520 def getVal(src: Entity, fieldVars: t.Union[str, t.Tuple[str]], direction: SortOrder) -> t.Any: 

521 # Descent into the target until we reach the property we're looking for 

522 if isinstance(fieldVars, tuple): 

523 for fv in fieldVars: 

524 if fv not in src: 

525 return None 

526 src = src[fv] 

527 else: 

528 if fieldVars not in src: 

529 return (str(type(None)), 0) 

530 src = src[fieldVars] 

531 # Lists are handled differently, here the smallest or largest value determines it's position in the result 

532 if isinstance(src, list) and len(src): 

533 try: 

534 src.sort() 

535 except TypeError: 

536 # It's a list of dicts or the like for which no useful sort-order is specified 

537 pass 

538 if direction == SortOrder.Ascending: 

539 src = src[0] 

540 else: 

541 src = src[-1] 

542 # We must return this tuple because inter-type comparison isn't possible in Python3 anymore 

543 return str(type(src)), src if src is not None else 0 

544 

545 # Check if we have an inequality filter which implies a sortorder 

546 ineqFilter = None 

547 for k, _ in filters.items(): 

548 end = k[-2:] 

549 if "<" in end or ">" in end: 

550 ineqFilter = k.split(" ")[0] 

551 break 

552 if ineqFilter and (not orders or not orders[0][0] == ineqFilter): 

553 orders = [(ineqFilter, SortOrder.Ascending)] + (orders or []) 

554 

555 for orderField, direction in orders[::-1]: 

556 if orderField == KEY_SPECIAL_PROPERTY: 

557 pass # FIXME !! 

558 # entities.sort(key=lambda x: x.key, reverse=direction == SortOrder.Descending) 

559 else: 

560 try: 

561 entities.sort(key=functools.partial(getVal, fieldVars=orderField, direction=direction), 

562 reverse=direction == SortOrder.Descending) 

563 except TypeError: 

564 # We hit some incomparable types 

565 pass 

566 return entities 

567 

568 def _fixKind(self, resultList: t.List[Entity]) -> t.List[Entity]: 

569 """ 

570 Jump to parentKind if necessary (used in relations) 

571 """ 

572 resultList = list(resultList) 

573 if ( 

574 resultList 

575 and resultList[0].key.kind != self.origKind 

576 and resultList[0].key.parent 

577 and resultList[0].key.parent.kind == self.origKind 

578 ): 

579 return list(get(list(dict.fromkeys([x.key.parent for x in resultList])))) 

580 

581 return resultList 

582 

583 def run(self, limit: int = -1) -> t.List[Entity]: 

584 """ 

585 Run this query. 

586 

587 It is more efficient to use *limit* if the number of results is known. 

588 

589 If queried data is wanted as instances of Skeletons, :meth:`fetch` 

590 should be used. 

591 

592 :param limit: Limits the query to the defined maximum entities. 

593 

594 :returns: The list of found entities 

595 

596 :raises: :exc:`BadFilterError` if a filter string is invalid 

597 :raises: :exc:`BadValueError` if a filter value is invalid. 

598 :raises: :exc:`BadQueryError` if an IN filter in combination with a sort order on\ 

599 another property is provided 

600 """ 

601 if self.queries is None: 

602 if conf.debug.trace_queries: 

603 logging.debug(f"Query on {self.kind} aborted as being not satisfiable") 

604 return [] 

605 

606 if self._fulltextQueryString: 

607 if utils.is_in_transaction(): 

608 raise ValueError("Can't run fulltextSearch inside transactions!") # InvalidStateError FIXME! 

609 

610 qryStr = self._fulltextQueryString 

611 self._fulltextQueryString = None # Reset, so the adapter can still work with this query 

612 res = self.srcSkel.customDatabaseAdapter.fulltextSearch(qryStr, self) 

613 

614 if not self.srcSkel.customDatabaseAdapter.fulltextSearchGuaranteesQueryConstrains: 

615 # Search might yield results that are not included in the listfilter 

616 if isinstance(self.queries, QueryDefinition): # Just one 

617 res = [x for x in res if _entryMatchesQuery(x, self.queries.filters)] 

618 else: # Multi-Query, must match at least one 

619 res = [x for x in res if any([_entryMatchesQuery(x, y.filters) for y in self.queries])] 

620 

621 elif isinstance(self.queries, list): 

622 limit = limit if limit >= 0 else self.queries[0].limit 

623 

624 # We have more than one query to run 

625 if self._calculateInternalMultiQueryLimit: 

626 limit = self._calculateInternalMultiQueryLimit(self, limit) 

627 

628 res = [] 

629 # We run all queries first (preventing multiple round-trips to the server) 

630 for singleQuery in self.queries: 

631 res.append(self._run_single_filter_query(singleQuery, limit)) 

632 

633 # Wait for the actual results to arrive and convert the protobuffs to Entries 

634 res = [self._fixKind(x) for x in res] 

635 if self._customMultiQueryMerge: 

636 # We have a custom merge function, use that 

637 res = self._customMultiQueryMerge(self, res, limit) 

638 else: 

639 # We must merge (and sort) the results ourself 

640 res = self._merge_multi_query_results(res) 

641 

642 else: # We have just one single query 

643 res = self._fixKind(self._run_single_filter_query( 

644 self.queries, limit if limit >= 0 else self.queries.limit)) 

645 

646 if res: 

647 self._lastEntry = res[-1] 

648 

649 return res 

650 

651 def count(self, up_to: int = 2 ** 63 - 1) -> int: 

652 """ 

653 The count operation cost one entity read for up to 1,000 index entries matched 

654 (https://cloud.google.com/datastore/docs/aggregation-queries#pricing) 

655 :param up_to can be sigend int 64 bit (max positive 2^31-1) 

656 

657 :returns: Count entries for this query. 

658 """ 

659 if self.queries is None: 

660 if conf.debug.trace_queries: 

661 logging.debug(f"Query on {self.kind} aborted as being not satisfiable") 

662 return -1 

663 elif isinstance(self.queries, list): 

664 raise ValueError("No count on Multiqueries") 

665 else: 

666 return count(queryDefinition=self.queries, up_to=up_to) 

667 

668 def fetch(self, limit: int = -1) -> "SkelList": 

669 """ 

670 Run this query and fetch results as :class:`core.skeleton.SkelList`. 

671 

672 This function is similar to :meth:`run`, but returns a 

673 :class:`core.skeleton.SkelList` instance instead of Entities. 

674 

675 :warning: The query must be limited! 

676 

677 If queried data is wanted as instances of Entity, :meth:`run` 

678 should be used. 

679 

680 :param limit: Limits the query to the defined maximum entities. 

681 

682 :raises: :exc:`BadFilterError` if a filter string is invalid 

683 :raises: :exc:`BadValueError` if a filter value is invalid. 

684 :raises: :exc:`BadQueryError` if an IN filter in combination with a sort order on 

685 another property is provided 

686 """ 

687 from viur.core.skeleton import SkelList, SkeletonInstance 

688 

689 if self.srcSkel is None: 

690 raise NotImplementedError("This query has not been created using skel.all()") 

691 

692 res = SkelList(self.srcSkel) 

693 

694 # FIXME: Why is this not like in ViUR2? 

695 for entity in self.run(limit): 

696 skel_instance = SkeletonInstance(self.srcSkel.skeletonCls, bone_map=self.srcSkel.boneMap) 

697 skel_instance.dbEntity = entity 

698 res.append(skel_instance) 

699 

700 res.getCursor = lambda: self.getCursor() 

701 res.get_orders = lambda: self.get_orders() 

702 

703 return res 

704 

705 def iter(self) -> t.Iterator[Entity]: 

706 """ 

707 Run this query and return an iterator for the results. 

708 

709 The advantage of this function is, that it allows for iterating 

710 over a large result-set, as it hasn't have to be pulled in advance 

711 from the datastore. 

712 

713 This function intentionally ignores a limit set by :meth:`limit`. 

714 

715 :warning: If iterating over a large result set, make sure the query supports cursors. \ 

716 Otherwise, it might not return all results as the AppEngine doesn't maintain the view \ 

717 for a query for more than ~30 seconds. 

718 """ 

719 if self.queries is None: # Noting to pull here 

720 raise StopIteration() 

721 elif isinstance(self.queries, list): 

722 raise ValueError("No iter on Multiqueries") 

723 while True: 

724 yield from self._run_single_filter_query(self.queries, 100) 

725 if not self.queries.currentCursor: # We reached the end of that query 

726 break 

727 self.queries.startCursor = self.queries.currentCursor 

728 

729 def getEntry(self) -> t.Union[None, Entity]: 

730 """ 

731 Returns only the first entity of the current query. 

732 

733 :returns: The first entity on success, or None if the result-set is empty. 

734 """ 

735 try: 

736 res = list(self.run(limit=1))[0] 

737 return res 

738 except (IndexError, TypeError): # Empty result-set 

739 return None 

740 

741 def getSkel(self) -> t.Optional["SkeletonInstance"]: 

742 """ 

743 Returns a matching :class:`core.db.skeleton.Skeleton` instance for the 

744 current query. 

745 

746 It's only possible to use this function if this query has been created using 

747 :func:`core.skeleton.Skeleton.all`. 

748 

749 :returns: The Skeleton or None if the result-set is empty. 

750 """ 

751 if self.srcSkel is None: 

752 raise NotImplementedError("This query has not been created using skel.all()") 

753 

754 if not (res := self.getEntry()): 

755 return None 

756 self.srcSkel.setEntity(res) 

757 return self.srcSkel 

758 

759 def clone(self) -> t.Self: 

760 """ 

761 Returns a deep copy of the current query. 

762 

763 :returns: The cloned query. 

764 """ 

765 res = Query(self.getKind(), self.srcSkel) 

766 res.kind = self.kind 

767 res.queries = copy.deepcopy(self.queries) 

768 # res.filters = copy.deepcopy(self.filters) 

769 # res.orders = copy.deepcopy(self.orders) 

770 # res._limit = self._limit 

771 res._filterHook = self._filterHook 

772 res._orderHook = self._orderHook 

773 # FIXME: Why is this disabled ??? 

774 # res._startCursor = self._startCursor 

775 # res._endCursor = self._endCursor 

776 res._customMultiQueryMerge = self._customMultiQueryMerge 

777 res._calculateInternalMultiQueryLimit = self._calculateInternalMultiQueryLimit 

778 res.customQueryInfo = self.customQueryInfo 

779 res.origKind = self.origKind 

780 res._fulltextQueryString = self._fulltextQueryString 

781 # res._distinct = self._distinct 

782 return res 

783 

784 def __repr__(self) -> str: 

785 return f"<db.Query on {self.kind} with queries {self.queries}>"