Mercurial > p > roundup > code
diff roundup/hyperdb.py @ 6500:30358e334232 issue2550923_computed_property
merge trunk changes into this branch
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Thu, 16 Sep 2021 14:30:56 -0400 |
| parents | 1e5ed659e8ca 8e06194ff0b0 |
| children | e1588ae185dc |
line wrap: on
line diff
--- a/roundup/hyperdb.py Sat Feb 06 20:15:26 2021 -0500 +++ b/roundup/hyperdb.py Thu Sep 16 14:30:56 2021 -0400 @@ -28,6 +28,7 @@ # roundup modules from . import date, password from .support import ensureParentsExist, PrioList +from roundup.mlink_expr import Expression from roundup.i18n import _ from roundup.cgi.exceptions import DetectorError from roundup.anypy.cmp_ import NoneAndDictComparable @@ -141,7 +142,7 @@ class String(_Type): """An object designating a String property.""" - def __init__(self, indexme='no', required=False, default_value="", + def __init__(self, indexme='no', required=False, default_value=None, quiet=False): super(String, self).__init__(required, default_value, quiet) self.indexme = indexme == 'yes' @@ -529,7 +530,7 @@ The Proptree is also used for transitively searching attributes for backends that do not support transitive search (e.g. anydbm). The - _val attribute with set_val is used for this. + val attribute with set_val is used for this. """ def __init__(self, db, cls, name, props, parent=None, retr=False): @@ -537,8 +538,9 @@ self.name = name self.props = props self.parent = parent - self._val = None + self.val = None self.has_values = False + self.has_result = False self.cls = cls self.classname = None self.uniqname = None @@ -582,8 +584,9 @@ if name in self.propdict: pt = self.propdict[name] pt.need_for[need_for] = True - if retr and isinstance(pt.propclass, Link): - pt.append_retr_props() + # For now we do not recursively retrieve Link properties + #if retr and isinstance(pt.propclass, Link): + # pt.append_retr_props() return pt propclass = self.props[name] cls = None @@ -601,8 +604,9 @@ child.need_child_retired = True self.children.append(child) self.propdict[name] = child - if retr and isinstance(child.propclass, Link): - child.append_retr_props() + # For now we do not recursively retrieve Link properties + #if retr and isinstance(child.propclass, Link): + # child.append_retr_props() return child def append_retr_props(self): @@ -648,31 +652,63 @@ exact_match_spec = {} for p in self.children: if 'search' in p.need_for: - if p.children: + x = [c for c in p.children if 'search' in c.need_for] + if x: p.search(sort=False) if getattr(p.propclass,'rev_property',None): pn = p.propclass.rev_property.name cl = p.propclass.rev_property.cls if not isinstance(p.val, type([])): p.val = [p.val] - if p.val == ['-1'] : - s1 = set(self.cls.getnodeids(retired=False)) - s2 = set() + nval = [int(i) for i in p.val] + pval = [str(i) for i in nval if i >= 0] + items = set() + if not nval or min(nval) >= -1: + if -1 in nval: + s1 = set(self.cls.getnodeids(retired=False)) + s2 = set() + for id in cl.getnodeids(retired=False): + node = cl.getnode(id) + if node[pn]: + if isinstance(node[pn], type([])): + s2.update(node[pn]) + else: + s2.add(node[pn]) + items |= s1.difference(s2) + if isinstance(p.propclass.rev_property, Link): + items |= set(cl.get(x, pn) for x in pval + if not cl.is_retired(x)) + else: + items |= set().union(*(cl.get(x, pn) for x in pval + if not cl.is_retired(x))) + else: + # Expression: materialize rev multilinks and run + # expression on them + expr = Expression(nval) + by_id = {} + for id in self.cls.getnodeids(retired=False): + by_id[id] = set() + items = set() for id in cl.getnodeids(retired=False): node = cl.getnode(id) if node[pn]: - if isinstance(node [pn], type([])): - s2.update(node [pn]) - else: - s2.add(node [pn]) - items = s1.difference(s2) - elif isinstance(p.propclass.rev_property, Link): - items = set(cl.get(x, pn) for x in p.val - if not cl.is_retired(x)) - else: - items = set().union(*(cl.get(x, pn) for x in p.val - if not cl.is_retired(x))) - filterspec[p.name] = list(sorted(items)) + v = node[pn] + if not isinstance(v, type([])): + v = [v] + for x in v: + if x not in by_id: + continue + by_id[x].add(id) + for k in by_id: + if expr.evaluate(by_id[k]): + items.add(k) + + # The subquery has found nothing. So it doesn't make + # sense to search further. + if not items: + self.set_val([], force=True) + return self.val + filterspec[p.name] = list(sorted(items, key=int)) elif isinstance(p.val, type([])): exact = [] subst = [] @@ -686,13 +722,19 @@ if subst: filterspec[p.name] = subst elif not exact: # don't set if we have exact criteria - filterspec[p.name] =[ '-1' ] # no match was found + if p.has_result: + # A subquery already has found nothing. So + # it doesn't make sense to search further. + self.set_val([], force=True) + return self.val + else: + filterspec[p.name] = ['-1'] # no match was found else: assert not isinstance(p.val, Exact_Match) filterspec[p.name] = p.val - self.val = self.cls._filter(search_matches, filterspec, sort and self, - retired=retired, - exact_match_spec=exact_match_spec) + self.set_val(self.cls._filter(search_matches, filterspec, sort and self, + retired=retired, + exact_match_spec=exact_match_spec)) return self.val def sort(self, ids=None): @@ -780,8 +822,9 @@ pt.sort_result = None return ids - def _set_val(self, val): - """ Check if self._val is already defined. If yes, we compute the + def set_val(self, val, force=False, result=True): + """ Check if self.val is already defined (it is not None and + has_values is True). If yes, we compute the intersection of the old and the new value(s) Note: If self is a Leaf node we need to compute a union: Normally we intersect (logical and) different @@ -791,25 +834,57 @@ generated search will ensure a logical and of all tests for equality/substring search. """ + if force: + assert val == [] + assert result + self.val = val + self.has_values = True + self.has_result = True + return if self.has_values: - v = self._val - if not isinstance(self._val, type([])): - v = [self._val] + v = self.val + if not isinstance(self.val, type([])): + v = [self.val] vals = set(v) if not isinstance(val, type([])): val = [val] + if self.has_result: + assert result # if cls is None we're a leaf if self.cls: vals.intersection_update(val) else: vals.update(val) - self._val = [v for v in vals] + self.val = list(vals) else: - self._val = val + # If a subquery found nothing we don't care if there is an + # expression + if not self.has_values or not val: + self.val = val + if result: + self.has_result = True + else: + if not result: + assert not self.cls + vals.update(val) + self.val = list(vals) + else: + assert self.cls + is_expression = \ + self.val and min(int(i) for i in self.val) < -1 + if is_expression: + # Tag on the ORed values with an AND + l = val + for i in range(len(val)-1): + l.append('-4') + l.append('-3') + self.val = self.val + l + else: + vals.intersection_update(val) + self.val = list(vals) + self.has_result = True self.has_values = True - val = property(lambda self: self._val, _set_val) - def _sort(self, val): """Finally sort by the given sortattr.sort_result. Note that we do not sort by attrs having attr_sort_done set. The caller is @@ -1558,8 +1633,7 @@ """Build a tree of all transitive properties in the given exact_match_spec/filterspec. If we retrieve (retr is True) linked items we don't follow - across multilinks. We also don't follow if the searched value - can contain NULL values. + across multilinks or links. """ proptree = Proptree(self.db, self, '', self.getprops(), retr=retr) for exact, spec in enumerate((filterspec, exact_match_spec)): @@ -1580,11 +1654,11 @@ vv = [] for x in v: vv.append(Exact_Match(x)) - p.val = vv + p.set_val(vv, result=False) else: - p.val = [Exact_Match(v)] + p.set_val([Exact_Match(v)], result=False) else: - p.val = v + p.set_val(v, result=False) multilinks = {} for s in sortattr: keys = s[1].split('.') @@ -1695,6 +1769,25 @@ make sense to specify multiple values because those cannot all be matched exactly. + For Link and Multilink properties the special ID value '-1' + matches empty Link or Multilink fields. For Multilinks a postfix + expression syntax using negative ID numbers (as strings) as + operators is supported. Each non-negative number (or '-1') is + pushed on an operand stack. A negative number pops the required + number of arguments from the stack, applies the operator, and + pushes the result. The following operators are supported: + - -2 stands for 'NOT' and takes one argument + - -3 stands for 'AND' and takes two arguments + - -4 stands for 'OR' and takes two arguments + Note that this special handling of ID arguments is applied only + when a negative number smaller than -1 is encountered as an ID + in the filter call. Otherwise the implicit OR default applies. + Examples of using Multilink expressions would be + - '1', '2', '-4', '3', '4', '-4', '-3' + would search for IDs (1 or 2) and (3 or 4) + - '-1' '-2' would search for all non-empty Multilinks + + The propname in filterspec and prop in a sort/group spec may be transitive, i.e., it may contain properties of the form link.link.link.name, e.g. you can search for all issues where a @@ -2096,4 +2189,3 @@ cl.create(name=options[i], order=i) return Link(name) -# vim: set filetype=python sts=4 sw=4 et si :
