Mercurial > p > roundup > code
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 : |
