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
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-13 11:04 +0000
1from __future__ import annotations
3import base64
4import copy
5import functools
6import logging
7import typing as t
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
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
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])
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 """
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
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
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 """
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)
103 def setFilterHook(self, hook: TFilterHook) -> TFilterHook | None:
104 """
105 Installs *hook* as a callback function for new filters.
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).
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
119 def setOrderHook(self, hook: TOrderHook) -> TOrderHook | None:
120 """
121 Installs *hook* as a callback function for new orderings.
123 *hook* will be called each time a :func:`db.Query.order` is called on this query.
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
133 def mergeExternalFilter(self, filters: dict) -> t.Self:
134 """
135 Safely merges filters according to the data model.
137 Its only valid to call this function if the query has been created using
138 :func:`core.skeleton.Skeleton.all`.
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.
144 If complex queries are needed (e.g. filter by relations), this function
145 shall also be used.
147 See also :meth:`filter` for simple filters.
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()")
155 if self.queries is None: # This query is already unsatisfiable and adding more constraints won't change this
156 return self
158 skel = self.srcSkel
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
170 bones = [(y, x) for x, y in skel.items()]
172 try:
173 # Process filters first
174 for bone, key in bones:
175 bone.buildDBFilter(key, skel, self, filters)
177 # Parse orders
178 for bone, key in bones:
179 bone.buildDBSort(key, skel, self, filters)
181 except RuntimeError as e:
182 logging.exception(e)
183 self.queries = None
184 return self
186 startCursor = endCursor = None
188 if "cursor" in filters and filters["cursor"] and filters["cursor"].lower() != "none":
189 startCursor = filters["cursor"]
191 if "endcursor" in filters and filters["endcursor"] and filters["endcursor"].lower() != "none":
192 endCursor = filters["endcursor"]
194 if startCursor or endCursor:
195 self.setCursor(startCursor, endCursor)
197 if limit := filters.get("limit"):
198 try:
199 limit = int(limit)
201 # disallow limit beyond conf.db.query_external_limit
202 if limit > conf.db.query_external_limit:
203 limit = conf.db.query_external_limit
205 # forbid any limit < 0, which might bypass defaults
206 if limit < 0:
207 limit = 0
209 self.limit(limit)
210 except ValueError:
211 pass # ignore this
213 return self
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.
219 See also :meth:`mergeExternalFilter` for a safer filter implementation.
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
291 def order(self, *orderings: t.Tuple[str, SortOrder]) -> t.Self:
292 """
293 Specify a query sorting.
295 Resulting entities will be sorted by the first property argument, then by the
296 second, and so on.
298 The following example
300 .. code-block:: python
302 query = Query("Person")
303 query.order(("bday" db.SortOrder.Ascending), ("age", db.SortOrder.Descending))
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.
309 ``order()`` may be called multiple times. Each call resets the sort order
310 from scratch.
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.
317 Entities with multiple values for an order property are sorted by their
318 lowest value.
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.
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.
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
338 # Check for correct order subscript
339 orders = []
340 for order in orderings:
341 if isinstance(order, str):
342 order = (order, SortOrder.Ascending)
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)`")
348 orders.append(order)
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
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)
365 return self
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.
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.
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.
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
396 def limit(self, limit: int) -> t.Self:
397 """
398 Sets the query limit to *limit* entities in the result.
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
409 return self
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
423 def getCursor(self) -> t.Optional[str]:
424 """
425 Get a valid cursor from the last run of this query.
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.
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
447 def get_orders(self) -> t.List[t.Tuple[str, SortOrder]] | None:
448 """
449 Get the orders from this query.
451 :returns: The orders form this query as a list if there is no orders set it returns None
452 """
453 q = self.queries
455 if isinstance(q, (list, tuple)):
456 q = q[0]
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 )
463 return q.orders or None
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
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)
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)
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.
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 """
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
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 [])
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
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]))))
581 return resultList
583 def run(self, limit: int = -1) -> t.List[Entity]:
584 """
585 Run this query.
587 It is more efficient to use *limit* if the number of results is known.
589 If queried data is wanted as instances of Skeletons, :meth:`fetch`
590 should be used.
592 :param limit: Limits the query to the defined maximum entities.
594 :returns: The list of found entities
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 []
606 if self._fulltextQueryString:
607 if utils.is_in_transaction():
608 raise ValueError("Can't run fulltextSearch inside transactions!") # InvalidStateError FIXME!
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)
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])]
621 elif isinstance(self.queries, list):
622 limit = limit if limit >= 0 else self.queries[0].limit
624 # We have more than one query to run
625 if self._calculateInternalMultiQueryLimit:
626 limit = self._calculateInternalMultiQueryLimit(self, limit)
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))
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)
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))
646 if res:
647 self._lastEntry = res[-1]
649 return res
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)
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)
668 def fetch(self, limit: int = -1) -> "SkelList":
669 """
670 Run this query and fetch results as :class:`core.skeleton.SkelList`.
672 This function is similar to :meth:`run`, but returns a
673 :class:`core.skeleton.SkelList` instance instead of Entities.
675 :warning: The query must be limited!
677 If queried data is wanted as instances of Entity, :meth:`run`
678 should be used.
680 :param limit: Limits the query to the defined maximum entities.
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
689 if self.srcSkel is None:
690 raise NotImplementedError("This query has not been created using skel.all()")
692 res = SkelList(self.srcSkel)
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)
700 res.getCursor = lambda: self.getCursor()
701 res.get_orders = lambda: self.get_orders()
703 return res
705 def iter(self) -> t.Iterator[Entity]:
706 """
707 Run this query and return an iterator for the results.
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.
713 This function intentionally ignores a limit set by :meth:`limit`.
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
729 def getEntry(self) -> t.Union[None, Entity]:
730 """
731 Returns only the first entity of the current query.
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
741 def getSkel(self) -> t.Optional["SkeletonInstance"]:
742 """
743 Returns a matching :class:`core.db.skeleton.Skeleton` instance for the
744 current query.
746 It's only possible to use this function if this query has been created using
747 :func:`core.skeleton.Skeleton.all`.
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()")
754 if not (res := self.getEntry()):
755 return None
756 self.srcSkel.setEntity(res)
757 return self.srcSkel
759 def clone(self) -> t.Self:
760 """
761 Returns a deep copy of the current query.
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
784 def __repr__(self) -> str:
785 return f"<db.Query on {self.kind} with queries {self.queries}>"