comparison 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
comparison
equal deleted inserted replaced
6325:1a15089c2e49 6500:30358e334232
26 import logging 26 import logging
27 27
28 # roundup modules 28 # roundup modules
29 from . import date, password 29 from . import date, password
30 from .support import ensureParentsExist, PrioList 30 from .support import ensureParentsExist, PrioList
31 from roundup.mlink_expr import Expression
31 from roundup.i18n import _ 32 from roundup.i18n import _
32 from roundup.cgi.exceptions import DetectorError 33 from roundup.cgi.exceptions import DetectorError
33 from roundup.anypy.cmp_ import NoneAndDictComparable 34 from roundup.anypy.cmp_ import NoneAndDictComparable
34 from roundup.anypy.strings import eval_import 35 from roundup.anypy.strings import eval_import
35 36
139 140
140 return len(db.issue.get(nodeid, 'messages')) 141 return len(db.issue.get(nodeid, 'messages'))
141 142
142 class String(_Type): 143 class String(_Type):
143 """An object designating a String property.""" 144 """An object designating a String property."""
144 def __init__(self, indexme='no', required=False, default_value="", 145 def __init__(self, indexme='no', required=False, default_value=None,
145 quiet=False): 146 quiet=False):
146 super(String, self).__init__(required, default_value, quiet) 147 super(String, self).__init__(required, default_value, quiet)
147 self.indexme = indexme == 'yes' 148 self.indexme = indexme == 'yes'
148 149
149 def from_raw(self, value, propname='', **kw): 150 def from_raw(self, value, propname='', **kw):
527 is a dictionary containing one or several of the values 'sort', 528 is a dictionary containing one or several of the values 'sort',
528 'search', 'retrieve'. 529 'search', 'retrieve'.
529 530
530 The Proptree is also used for transitively searching attributes for 531 The Proptree is also used for transitively searching attributes for
531 backends that do not support transitive search (e.g. anydbm). The 532 backends that do not support transitive search (e.g. anydbm). The
532 _val attribute with set_val is used for this. 533 val attribute with set_val is used for this.
533 """ 534 """
534 535
535 def __init__(self, db, cls, name, props, parent=None, retr=False): 536 def __init__(self, db, cls, name, props, parent=None, retr=False):
536 self.db = db 537 self.db = db
537 self.name = name 538 self.name = name
538 self.props = props 539 self.props = props
539 self.parent = parent 540 self.parent = parent
540 self._val = None 541 self.val = None
541 self.has_values = False 542 self.has_values = False
543 self.has_result = False
542 self.cls = cls 544 self.cls = cls
543 self.classname = None 545 self.classname = None
544 self.uniqname = None 546 self.uniqname = None
545 self.children = [] 547 self.children = []
546 self.sortattr = [] 548 self.sortattr = []
580 propclass for the child. 582 propclass for the child.
581 """ 583 """
582 if name in self.propdict: 584 if name in self.propdict:
583 pt = self.propdict[name] 585 pt = self.propdict[name]
584 pt.need_for[need_for] = True 586 pt.need_for[need_for] = True
585 if retr and isinstance(pt.propclass, Link): 587 # For now we do not recursively retrieve Link properties
586 pt.append_retr_props() 588 #if retr and isinstance(pt.propclass, Link):
589 # pt.append_retr_props()
587 return pt 590 return pt
588 propclass = self.props[name] 591 propclass = self.props[name]
589 cls = None 592 cls = None
590 props = None 593 props = None
591 if isinstance(propclass, (Link, Multilink)): 594 if isinstance(propclass, (Link, Multilink)):
599 child.need_retired = True 602 child.need_retired = True
600 else: 603 else:
601 child.need_child_retired = True 604 child.need_child_retired = True
602 self.children.append(child) 605 self.children.append(child)
603 self.propdict[name] = child 606 self.propdict[name] = child
604 if retr and isinstance(child.propclass, Link): 607 # For now we do not recursively retrieve Link properties
605 child.append_retr_props() 608 #if retr and isinstance(child.propclass, Link):
609 # child.append_retr_props()
606 return child 610 return child
607 611
608 def append_retr_props(self): 612 def append_retr_props(self):
609 """Append properties for retrieval.""" 613 """Append properties for retrieval."""
610 for name, prop in self.cls.getprops(protected=1).items(): 614 for name, prop in self.cls.getprops(protected=1).items():
646 """ 650 """
647 filterspec = {} 651 filterspec = {}
648 exact_match_spec = {} 652 exact_match_spec = {}
649 for p in self.children: 653 for p in self.children:
650 if 'search' in p.need_for: 654 if 'search' in p.need_for:
651 if p.children: 655 x = [c for c in p.children if 'search' in c.need_for]
656 if x:
652 p.search(sort=False) 657 p.search(sort=False)
653 if getattr(p.propclass,'rev_property',None): 658 if getattr(p.propclass,'rev_property',None):
654 pn = p.propclass.rev_property.name 659 pn = p.propclass.rev_property.name
655 cl = p.propclass.rev_property.cls 660 cl = p.propclass.rev_property.cls
656 if not isinstance(p.val, type([])): 661 if not isinstance(p.val, type([])):
657 p.val = [p.val] 662 p.val = [p.val]
658 if p.val == ['-1'] : 663 nval = [int(i) for i in p.val]
659 s1 = set(self.cls.getnodeids(retired=False)) 664 pval = [str(i) for i in nval if i >= 0]
660 s2 = set() 665 items = set()
666 if not nval or min(nval) >= -1:
667 if -1 in nval:
668 s1 = set(self.cls.getnodeids(retired=False))
669 s2 = set()
670 for id in cl.getnodeids(retired=False):
671 node = cl.getnode(id)
672 if node[pn]:
673 if isinstance(node[pn], type([])):
674 s2.update(node[pn])
675 else:
676 s2.add(node[pn])
677 items |= s1.difference(s2)
678 if isinstance(p.propclass.rev_property, Link):
679 items |= set(cl.get(x, pn) for x in pval
680 if not cl.is_retired(x))
681 else:
682 items |= set().union(*(cl.get(x, pn) for x in pval
683 if not cl.is_retired(x)))
684 else:
685 # Expression: materialize rev multilinks and run
686 # expression on them
687 expr = Expression(nval)
688 by_id = {}
689 for id in self.cls.getnodeids(retired=False):
690 by_id[id] = set()
691 items = set()
661 for id in cl.getnodeids(retired=False): 692 for id in cl.getnodeids(retired=False):
662 node = cl.getnode(id) 693 node = cl.getnode(id)
663 if node[pn]: 694 if node[pn]:
664 if isinstance(node [pn], type([])): 695 v = node[pn]
665 s2.update(node [pn]) 696 if not isinstance(v, type([])):
666 else: 697 v = [v]
667 s2.add(node [pn]) 698 for x in v:
668 items = s1.difference(s2) 699 if x not in by_id:
669 elif isinstance(p.propclass.rev_property, Link): 700 continue
670 items = set(cl.get(x, pn) for x in p.val 701 by_id[x].add(id)
671 if not cl.is_retired(x)) 702 for k in by_id:
672 else: 703 if expr.evaluate(by_id[k]):
673 items = set().union(*(cl.get(x, pn) for x in p.val 704 items.add(k)
674 if not cl.is_retired(x))) 705
675 filterspec[p.name] = list(sorted(items)) 706 # The subquery has found nothing. So it doesn't make
707 # sense to search further.
708 if not items:
709 self.set_val([], force=True)
710 return self.val
711 filterspec[p.name] = list(sorted(items, key=int))
676 elif isinstance(p.val, type([])): 712 elif isinstance(p.val, type([])):
677 exact = [] 713 exact = []
678 subst = [] 714 subst = []
679 for v in p.val: 715 for v in p.val:
680 if isinstance(v, Exact_Match): 716 if isinstance(v, Exact_Match):
684 if exact: 720 if exact:
685 exact_match_spec[p.name] = exact 721 exact_match_spec[p.name] = exact
686 if subst: 722 if subst:
687 filterspec[p.name] = subst 723 filterspec[p.name] = subst
688 elif not exact: # don't set if we have exact criteria 724 elif not exact: # don't set if we have exact criteria
689 filterspec[p.name] =[ '-1' ] # no match was found 725 if p.has_result:
726 # A subquery already has found nothing. So
727 # it doesn't make sense to search further.
728 self.set_val([], force=True)
729 return self.val
730 else:
731 filterspec[p.name] = ['-1'] # no match was found
690 else: 732 else:
691 assert not isinstance(p.val, Exact_Match) 733 assert not isinstance(p.val, Exact_Match)
692 filterspec[p.name] = p.val 734 filterspec[p.name] = p.val
693 self.val = self.cls._filter(search_matches, filterspec, sort and self, 735 self.set_val(self.cls._filter(search_matches, filterspec, sort and self,
694 retired=retired, 736 retired=retired,
695 exact_match_spec=exact_match_spec) 737 exact_match_spec=exact_match_spec))
696 return self.val 738 return self.val
697 739
698 def sort(self, ids=None): 740 def sort(self, ids=None):
699 """ Sort ids by the order information stored in self. With 741 """ Sort ids by the order information stored in self. With
700 optimisations: Some order attributes may be precomputed (by the 742 optimisations: Some order attributes may be precomputed (by the
778 pt.sort_ids = None 820 pt.sort_ids = None
779 for pt in self.sortattr: 821 for pt in self.sortattr:
780 pt.sort_result = None 822 pt.sort_result = None
781 return ids 823 return ids
782 824
783 def _set_val(self, val): 825 def set_val(self, val, force=False, result=True):
784 """ Check if self._val is already defined. If yes, we compute the 826 """ Check if self.val is already defined (it is not None and
827 has_values is True). If yes, we compute the
785 intersection of the old and the new value(s) 828 intersection of the old and the new value(s)
786 Note: If self is a Leaf node we need to compute a 829 Note: If self is a Leaf node we need to compute a
787 union: Normally we intersect (logical and) different 830 union: Normally we intersect (logical and) different
788 subqueries into a Link or Multilink property. But for 831 subqueries into a Link or Multilink property. But for
789 leaves we might have a part of a query in a filterspec and 832 leaves we might have a part of a query in a filterspec and
790 in an exact_match_spec. These have to be all there, the 833 in an exact_match_spec. These have to be all there, the
791 generated search will ensure a logical and of all tests for 834 generated search will ensure a logical and of all tests for
792 equality/substring search. 835 equality/substring search.
793 """ 836 """
837 if force:
838 assert val == []
839 assert result
840 self.val = val
841 self.has_values = True
842 self.has_result = True
843 return
794 if self.has_values: 844 if self.has_values:
795 v = self._val 845 v = self.val
796 if not isinstance(self._val, type([])): 846 if not isinstance(self.val, type([])):
797 v = [self._val] 847 v = [self.val]
798 vals = set(v) 848 vals = set(v)
799 if not isinstance(val, type([])): 849 if not isinstance(val, type([])):
800 val = [val] 850 val = [val]
851 if self.has_result:
852 assert result
801 # if cls is None we're a leaf 853 # if cls is None we're a leaf
802 if self.cls: 854 if self.cls:
803 vals.intersection_update(val) 855 vals.intersection_update(val)
804 else: 856 else:
805 vals.update(val) 857 vals.update(val)
806 self._val = [v for v in vals] 858 self.val = list(vals)
807 else: 859 else:
808 self._val = val 860 # If a subquery found nothing we don't care if there is an
861 # expression
862 if not self.has_values or not val:
863 self.val = val
864 if result:
865 self.has_result = True
866 else:
867 if not result:
868 assert not self.cls
869 vals.update(val)
870 self.val = list(vals)
871 else:
872 assert self.cls
873 is_expression = \
874 self.val and min(int(i) for i in self.val) < -1
875 if is_expression:
876 # Tag on the ORed values with an AND
877 l = val
878 for i in range(len(val)-1):
879 l.append('-4')
880 l.append('-3')
881 self.val = self.val + l
882 else:
883 vals.intersection_update(val)
884 self.val = list(vals)
885 self.has_result = True
809 self.has_values = True 886 self.has_values = True
810
811 val = property(lambda self: self._val, _set_val)
812 887
813 def _sort(self, val): 888 def _sort(self, val):
814 """Finally sort by the given sortattr.sort_result. Note that we 889 """Finally sort by the given sortattr.sort_result. Note that we
815 do not sort by attrs having attr_sort_done set. The caller is 890 do not sort by attrs having attr_sort_done set. The caller is
816 responsible for setting attr_sort_done only for trailing 891 responsible for setting attr_sort_done only for trailing
1556 def _proptree(self, filterspec, exact_match_spec={}, sortattr=[], 1631 def _proptree(self, filterspec, exact_match_spec={}, sortattr=[],
1557 retr=False): 1632 retr=False):
1558 """Build a tree of all transitive properties in the given 1633 """Build a tree of all transitive properties in the given
1559 exact_match_spec/filterspec. 1634 exact_match_spec/filterspec.
1560 If we retrieve (retr is True) linked items we don't follow 1635 If we retrieve (retr is True) linked items we don't follow
1561 across multilinks. We also don't follow if the searched value 1636 across multilinks or links.
1562 can contain NULL values.
1563 """ 1637 """
1564 proptree = Proptree(self.db, self, '', self.getprops(), retr=retr) 1638 proptree = Proptree(self.db, self, '', self.getprops(), retr=retr)
1565 for exact, spec in enumerate((filterspec, exact_match_spec)): 1639 for exact, spec in enumerate((filterspec, exact_match_spec)):
1566 for key, v in spec.items(): 1640 for key, v in spec.items():
1567 keys = key.split('.') 1641 keys = key.split('.')
1578 if exact: 1652 if exact:
1579 if isinstance(v, type([])): 1653 if isinstance(v, type([])):
1580 vv = [] 1654 vv = []
1581 for x in v: 1655 for x in v:
1582 vv.append(Exact_Match(x)) 1656 vv.append(Exact_Match(x))
1583 p.val = vv 1657 p.set_val(vv, result=False)
1584 else: 1658 else:
1585 p.val = [Exact_Match(v)] 1659 p.set_val([Exact_Match(v)], result=False)
1586 else: 1660 else:
1587 p.val = v 1661 p.set_val(v, result=False)
1588 multilinks = {} 1662 multilinks = {}
1589 for s in sortattr: 1663 for s in sortattr:
1590 keys = s[1].split('.') 1664 keys = s[1].split('.')
1591 p = proptree 1665 p = proptree
1592 mlseen = False 1666 mlseen = False
1693 1767
1694 This also means that for strings in exact_match_spec it doesn't 1768 This also means that for strings in exact_match_spec it doesn't
1695 make sense to specify multiple values because those cannot all 1769 make sense to specify multiple values because those cannot all
1696 be matched exactly. 1770 be matched exactly.
1697 1771
1772 For Link and Multilink properties the special ID value '-1'
1773 matches empty Link or Multilink fields. For Multilinks a postfix
1774 expression syntax using negative ID numbers (as strings) as
1775 operators is supported. Each non-negative number (or '-1') is
1776 pushed on an operand stack. A negative number pops the required
1777 number of arguments from the stack, applies the operator, and
1778 pushes the result. The following operators are supported:
1779 - -2 stands for 'NOT' and takes one argument
1780 - -3 stands for 'AND' and takes two arguments
1781 - -4 stands for 'OR' and takes two arguments
1782 Note that this special handling of ID arguments is applied only
1783 when a negative number smaller than -1 is encountered as an ID
1784 in the filter call. Otherwise the implicit OR default applies.
1785 Examples of using Multilink expressions would be
1786 - '1', '2', '-4', '3', '4', '-4', '-3'
1787 would search for IDs (1 or 2) and (3 or 4)
1788 - '-1' '-2' would search for all non-empty Multilinks
1789
1790
1698 The propname in filterspec and prop in a sort/group spec may be 1791 The propname in filterspec and prop in a sort/group spec may be
1699 transitive, i.e., it may contain properties of the form 1792 transitive, i.e., it may contain properties of the form
1700 link.link.link.name, e.g. you can search for all issues where a 1793 link.link.link.name, e.g. you can search for all issues where a
1701 message was added by a certain user in the last week with a 1794 message was added by a certain user in the last week with a
1702 filterspec of 1795 filterspec of
2094 cl = Class(db, name, name=String(), order=String()) 2187 cl = Class(db, name, name=String(), order=String())
2095 for i in range(len(options)): 2188 for i in range(len(options)):
2096 cl.create(name=options[i], order=i) 2189 cl.create(name=options[i], order=i)
2097 return Link(name) 2190 return Link(name)
2098 2191
2099 # vim: set filetype=python sts=4 sw=4 et si :

Roundup Issue Tracker: http://roundup-tracker.org/