Skip to content

Commit 141ac43

Browse files
committed
Tests and bug-fix for issue2551119
.. and some other failing tests I came up with when trying to reproduce the problem of the issue.
1 parent 3f2d351 commit 141ac43

File tree

3 files changed

+145
-38
lines changed

3 files changed

+145
-38
lines changed

roundup/backends/rdbms_common.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2585,21 +2585,23 @@ def _filter_sql (self, search_matches, filterspec, srt=[], grp=[], retr=0,
25852585
tn = propclass.table_name
25862586
nid = propclass.nodeid_name
25872587
lid = propclass.linkid_name
2588+
frum.append(tn)
2589+
if p.children or p.need_child_retired:
2590+
frum.append('_%s as _%s' % (cn, ln))
2591+
where.append('%s.%s=_%s.id'%(tn, lid, ln))
2592+
if p.need_child_retired:
2593+
where.append('_%s.__retired__=0'%(ln))
2594+
# Note: need the where-clause if p has
2595+
# children that compute additional restrictions
2596+
if (not p.has_values
2597+
or (not isinstance(v, type([])) and v != '-1')
2598+
or p.children):
2599+
where.append('_%s.id=%s.%s'%(pln, tn, nid))
25882600
if v in ('-1', ['-1'], []):
25892601
# only match rows that have count(linkid)=0 in the
25902602
# corresponding multilink table)
25912603
where.append(self._subselect(p))
25922604
else:
2593-
frum.append(tn)
2594-
2595-
if p.children or p.need_child_retired:
2596-
frum.append('_%s as _%s' % (cn, ln))
2597-
where.append('%s.%s=_%s.id'%(tn, lid, ln))
2598-
if p.need_child_retired:
2599-
where.append('_%s.__retired__=0'%(ln))
2600-
2601-
if not p.has_values or not isinstance(v, type([])):
2602-
where.append('_%s.id=%s.%s'%(pln, tn, nid))
26032605
if p.has_values:
26042606
if isinstance(v, type([])):
26052607
# The where-clause above is conditionally

roundup/hyperdb.py

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -465,16 +465,17 @@ class Proptree(object):
465465
466466
The Proptree is also used for transitively searching attributes for
467467
backends that do not support transitive search (e.g. anydbm). The
468-
_val attribute with set_val is used for this.
468+
val attribute with set_val is used for this.
469469
"""
470470

471471
def __init__(self, db, cls, name, props, parent=None, retr=False):
472472
self.db = db
473473
self.name = name
474474
self.props = props
475475
self.parent = parent
476-
self._val = None
476+
self.val = None
477477
self.has_values = False
478+
self.has_result = False
478479
self.cls = cls
479480
self.classname = None
480481
self.uniqname = None
@@ -634,6 +635,11 @@ def search(self, search_matches=None, sort=True, retired=False):
634635
if expr.evaluate(by_id[k]):
635636
items.add(k)
636637

638+
# The subquery has found nothing. So it doesn't make
639+
# sense to search further.
640+
if not items:
641+
self.set_val([], force=True)
642+
return self.val
637643
filterspec[p.name] = list(sorted(items, key=int))
638644
elif isinstance(p.val, type([])):
639645
exact = []
@@ -648,13 +654,19 @@ def search(self, search_matches=None, sort=True, retired=False):
648654
if subst:
649655
filterspec[p.name] = subst
650656
elif not exact: # don't set if we have exact criteria
651-
filterspec[p.name] =[ '-1' ] # no match was found
657+
if p.has_result:
658+
# A subquery already has found nothing. So
659+
# it doesn't make sense to search further.
660+
self.set_val([], force=True)
661+
return self.val
662+
else:
663+
filterspec[p.name] = ['-1'] # no match was found
652664
else:
653665
assert not isinstance(p.val, Exact_Match)
654666
filterspec[p.name] = p.val
655-
self.val = self.cls._filter(search_matches, filterspec, sort and self,
656-
retired=retired,
657-
exact_match_spec=exact_match_spec)
667+
self.set_val(self.cls._filter(search_matches, filterspec, sort and self,
668+
retired=retired,
669+
exact_match_spec=exact_match_spec))
658670
return self.val
659671

660672
def sort(self, ids=None):
@@ -742,8 +754,9 @@ def _searchsort(self, ids=None, update=True, dosort=True):
742754
pt.sort_result = None
743755
return ids
744756

745-
def _set_val(self, val):
746-
""" Check if self._val is already defined. If yes, we compute the
757+
def set_val(self, val, force=False, result=True):
758+
""" Check if self.val is already defined (it is not None and
759+
has_values is True). If yes, we compute the
747760
intersection of the old and the new value(s)
748761
Note: If self is a Leaf node we need to compute a
749762
union: Normally we intersect (logical and) different
@@ -753,25 +766,56 @@ def _set_val(self, val):
753766
generated search will ensure a logical and of all tests for
754767
equality/substring search.
755768
"""
769+
if force:
770+
assert val == []
771+
assert result
772+
self.val = val
773+
self.has_values = True
774+
self.has_result = True
775+
return
756776
if self.has_values:
757-
v = self._val
758-
if not isinstance(self._val, type([])):
759-
v = [self._val]
777+
v = self.val
778+
if not isinstance(self.val, type([])):
779+
v = [self.val]
760780
vals = set(v)
761781
if not isinstance(val, type([])):
762782
val = [val]
783+
if self.has_result:
784+
assert result
763785
# if cls is None we're a leaf
764786
if self.cls:
765787
vals.intersection_update(val)
766788
else:
767789
vals.update(val)
768-
self._val = [v for v in vals]
790+
self.val = list(vals)
769791
else:
770-
self._val = val
792+
# If a subquery found nothing we don't care if there is an
793+
# expression
794+
if not self.has_values or not val:
795+
self.val = val
796+
if result:
797+
self.has_result = True
798+
else:
799+
if not result:
800+
assert not self.cls
801+
vals.update(val)
802+
self.val = list(vals)
803+
else:
804+
assert self.cls
805+
is_expression = min(int(i) for i in self.val) < -1
806+
if is_expression:
807+
# Tag on the ORed values with an AND
808+
l = val
809+
for i in range(len(val)-1):
810+
l.append('-4')
811+
l.append('-3')
812+
self.val = self.val + l
813+
else:
814+
vals.intersection_update(val)
815+
self.val = list(vals)
816+
self.has_result = True
771817
self.has_values = True
772818

773-
val = property(lambda self: self._val, _set_val)
774-
775819
def _sort(self, val):
776820
"""Finally sort by the given sortattr.sort_result. Note that we
777821
do not sort by attrs having attr_sort_done set. The caller is
@@ -1542,11 +1586,11 @@ def _proptree(self, filterspec, exact_match_spec={}, sortattr=[],
15421586
vv = []
15431587
for x in v:
15441588
vv.append(Exact_Match(x))
1545-
p.val = vv
1589+
p.set_val(vv, result=False)
15461590
else:
1547-
p.val = [Exact_Match(v)]
1591+
p.set_val([Exact_Match(v)], result=False)
15481592
else:
1549-
p.val = v
1593+
p.set_val(v, result=False)
15501594
multilinks = {}
15511595
for s in sortattr:
15521596
keys = s[1].split('.')

test/db_test_base.py

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def setupTracker(dirname, backend="anydbm", optimize=False):
9393
def setupSchema(db, create, module):
9494
mls = module.Class(db, "mls", name=String())
9595
mls.setkey("name")
96-
keyword = module.Class(db, "keyword", name=String())
96+
keyword = module.Class(db, "keyword", name=String(), order=Number())
9797
keyword.setkey("name")
9898
status = module.Class(db, "status", name=String(), mls=Multilink("mls"))
9999
status.setkey("name")
@@ -118,7 +118,7 @@ def setupSchema(db, create, module):
118118
files=Multilink("file"), assignedto=Link('user', quiet=True,
119119
rev_multilink='issues'), priority=Link('priority'),
120120
spam=Multilink('msg'), feedback=Link('msg'),
121-
keywords=Multilink('keyword'))
121+
keywords=Multilink('keyword'), keywords2=Multilink('keyword'))
122122
stuff = module.Class(db, "stuff", stuff=String())
123123
session = module.Class(db, 'session', title=String())
124124
msg = module.FileClass(db, "msg", date=Date(),
@@ -1542,8 +1542,8 @@ def testIndexingPropertiesOnImport(self):
15421542
# import an issue
15431543
title = 'Bzzt'
15441544
nodeid = self.db.issue.import_list(['title', 'messages', 'files',
1545-
'spam', 'nosy', 'superseder', 'keywords'], [repr(title), '[]',
1546-
'[]', '[]', '[]', '[]', '[]'])
1545+
'spam', 'nosy', 'superseder', 'keywords', 'keywords2'],
1546+
[repr(title), '[]', '[]', '[]', '[]', '[]', '[]', '[]'])
15471547
self.db.commit()
15481548

15491549
# Content of title attribute is indexed
@@ -2060,6 +2060,37 @@ def testFilteringMultilinkExpression(self):
20602060
ae(filt(None, {kw: ['4', '-1', '-3', '3', '-1', '-4', '-4']}),
20612061
['1', '2', '3'])
20622062

2063+
def testFilteringTwoMultilinksExpression(self):
2064+
ae, iiter = self.filteringSetup()
2065+
kw1 = self.db.keyword.create(name='Key1', order=10)
2066+
kw2 = self.db.keyword.create(name='Key2', order=20)
2067+
kw3 = self.db.keyword.create(name='Key3', order=30)
2068+
kw4 = self.db.keyword.create(name='Key4', order=40)
2069+
self.db.issue.set('1', keywords=[kw1, kw2])
2070+
self.db.issue.set('2', keywords=[kw1, kw3])
2071+
self.db.issue.set('3', keywords=[kw2, kw3, kw4])
2072+
self.db.issue.set('4', keywords=[])
2073+
self.db.issue.set('1', keywords2=[kw3, kw4])
2074+
self.db.issue.set('2', keywords2=[kw2, kw3])
2075+
self.db.issue.set('3', keywords2=[kw1, kw3, kw4])
2076+
self.db.issue.set('4', keywords2=[])
2077+
self.db.commit()
2078+
kw = 'keywords'
2079+
kw2 = 'keywords2'
2080+
for filt in iiter():
2081+
# kw: '1' and '3' kw2: '2' and '3'
2082+
ae(filt(None, {kw: ['1', '3', '-3'], kw2: ['2', '3', '-3']}), ['2'])
2083+
# kw: empty kw2: empty
2084+
ae(filt(None, {kw: ['-1'], kw2: ['-1']}), ['4'])
2085+
# kw: empty kw2: empty
2086+
ae(filt(None, {kw: [], kw2: []}), ['4'])
2087+
# look for both keyword name and order
2088+
ae(filt(None, {'keywords.name': 'y4', 'keywords.order': 40}), ['3'])
2089+
# look for both keyword and order non-matching
2090+
ae(filt(None, {kw: '3', 'keywords.order': 40}), [])
2091+
# look for both keyword and order non-matching with kw and kw2
2092+
ae(filt(None, {kw: '3', 'keywords2.order': 40}), ['3'])
2093+
20632094
def testFilteringRevMultilink(self):
20642095
ae, iiter = self.filteringSetupTransitiveSearch('user')
20652096
ni = 'nosy_issues'
@@ -2102,6 +2133,35 @@ def ls(x):
21022133
self.assertEqual(ls(self.db.user.get('4', ni)), ['1'])
21032134
self.assertEqual(ls(self.db.user.get('5', ni)), ['7'])
21042135

2136+
def testFilteringRevMultilinkQ2(self):
2137+
ae, iiter = self.filteringSetupTransitiveSearch('user')
2138+
ni = 'nosy_issues'
2139+
nis = 'nosy_issues.status'
2140+
self.db.issue.set('6', nosy=['3', '4', '5'])
2141+
self.db.issue.set('7', nosy=['5'])
2142+
self.db.commit()
2143+
# After this setup we have the following values for nosy:
2144+
# The issues '1', '3', '5', '7' have status '2'
2145+
# issue nosy
2146+
# 1: 4
2147+
# 2: 5
2148+
# 3:
2149+
# 4:
2150+
# 5:
2151+
# 6: 3, 4, 5
2152+
# 7: 5
2153+
# 8:
2154+
for filt in iiter():
2155+
# status of issue is '2'
2156+
ae(filt(None, {nis: ['2']}),
2157+
['4', '5'])
2158+
# Issue non-empty and status of issue is '2'
2159+
ae(filt(None, {nis: ['2'], ni:['-1', '-2']}),
2160+
['4', '5'])
2161+
# empty and status '2'
2162+
# This is the test-case for issue2551119
2163+
ae(filt(None, {nis: ['2'], ni:['-1']}), [])
2164+
21052165
def testFilteringRevMultilinkExpression(self):
21062166
ae, iiter = self.filteringSetupTransitiveSearch('user')
21072167
ni = 'nosy_issues'
@@ -2911,6 +2971,7 @@ def stdoutwrite(s):
29112971
'feedback: <roundup.hyperdb.Link to "msg">\n',
29122972
'files: <roundup.hyperdb.Multilink to "file">\n',
29132973
'foo: <roundup.hyperdb.Interval>\n',
2974+
'keywords2: <roundup.hyperdb.Multilink to "keyword">\n',
29142975
'keywords: <roundup.hyperdb.Multilink to "keyword">\n',
29152976
'messages: <roundup.hyperdb.Multilink to "msg">\n',
29162977
'nosy: <roundup.hyperdb.Multilink to "user">\n',
@@ -3018,8 +3079,8 @@ def testAddProperty(self):
30183079
keys = sorted(props.keys())
30193080
self.assertEqual(keys, ['activity', 'actor', 'assignedto', 'creation',
30203081
'creator', 'deadline', 'feedback', 'files', 'fixer', 'foo',
3021-
'id', 'keywords', 'messages', 'nosy', 'priority', 'spam',
3022-
'status', 'superseder', 'title'])
3082+
'id', 'keywords', 'keywords2', 'messages', 'nosy', 'priority',
3083+
'spam', 'status', 'superseder', 'title'])
30233084
self.assertEqual(self.db.issue.get('1', "fixer"), None)
30243085

30253086
def testRemoveProperty(self):
@@ -3032,8 +3093,8 @@ def testRemoveProperty(self):
30323093
keys = sorted(props.keys())
30333094
self.assertEqual(keys, ['activity', 'actor', 'assignedto', 'creation',
30343095
'creator', 'deadline', 'feedback', 'files', 'foo', 'id',
3035-
'keywords', 'messages', 'nosy', 'priority', 'spam', 'status',
3036-
'superseder'])
3096+
'keywords', 'keywords2', 'messages', 'nosy', 'priority', 'spam',
3097+
'status', 'superseder'])
30373098
self.assertEqual(self.db.issue.list(), ['1'])
30383099

30393100
def testAddRemoveProperty(self):
@@ -3047,8 +3108,8 @@ def testAddRemoveProperty(self):
30473108
keys = sorted(props.keys())
30483109
self.assertEqual(keys, ['activity', 'actor', 'assignedto', 'creation',
30493110
'creator', 'deadline', 'feedback', 'files', 'fixer', 'foo', 'id',
3050-
'keywords', 'messages', 'nosy', 'priority', 'spam', 'status',
3051-
'superseder'])
3111+
'keywords', 'keywords2', 'messages', 'nosy', 'priority', 'spam',
3112+
'status', 'superseder'])
30523113
self.assertEqual(self.db.issue.list(), ['1'])
30533114

30543115
def testNosyMail(self) :

0 commit comments

Comments
 (0)