Skip to content

Commit aa442a9

Browse files
committed
Fix rev_multilink properties search/retrieval
The code now only returns live (non-retired) items. Since for reverse multilinks the Link/Multilink property in the retired item cannot be changed, we now only return non-retired items in search (filter) and retrieve (get).
1 parent 445549f commit aa442a9

File tree

6 files changed

+112
-10
lines changed

6 files changed

+112
-10
lines changed

doc/customizing.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,11 @@ behaviour:
864864
automatically adds "1234" to the ``components`` property on
865865
issue3456. You can search the ``components`` multilink just like a
866866
regular multilink, but you can't explicitly assign to it.
867+
Another difference of reverse multilinks to normal multilinks
868+
is that when a linked node is retired, the node vanishes from the
869+
multilink, e.g. in the example above, if an issue with ``part_of``
870+
set to another issue is retired this issue vanishes from the
871+
``components`` multilink of the other issue.
867872

868873
You can also link between different classes. So you can modify
869874
the issue definition to include::

roundup/backends/back_anydbm.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,6 +1138,11 @@ def get(self, nodeid, propname, default=_marker, cache=1):
11381138
# get the property (raises KeyErorr if invalid)
11391139
prop = self.properties[propname]
11401140

1141+
if isinstance(prop, hyperdb.Multilink) and prop.computed:
1142+
cls = self.db.getclass(prop.rev_classname)
1143+
ids = cls.find(**{prop.rev_propname: nodeid})
1144+
return ids
1145+
11411146
if propname not in d:
11421147
if default is _marker:
11431148
if isinstance(prop, hyperdb.Multilink):

roundup/backends/back_mysql.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -606,11 +606,24 @@ def supports_subselects(self):
606606
# TODO: AFAIK its version dependent for MySQL
607607
return False
608608

609-
def _subselect(self, classname, multilink_table, nodeid_name):
609+
def _subselect(self, proptree):
610610
''' "I can't believe it's not a toy RDBMS"
611611
see, even toy RDBMSes like gadfly and sqlite can do sub-selects...
612612
'''
613-
self.db.sql('select %s from %s'%(nodeid_name, multilink_table))
613+
classname = proptree.parent.classname
614+
multilink_table = proptree.propclass.table_name
615+
nodeid_name = proptree.propclass.nodeid_name
616+
linkid_name = proptree.propclass.linkid_name
617+
618+
w = ''
619+
if proptree.need_retired:
620+
w = ' where %s.__retired__=0'%(multilink_table)
621+
if proptree.need_child_retired:
622+
tn1 = multilink_table
623+
tn2 = '_' + proptree.classname
624+
w = ', %s where %s.%s=%s.id and %s.__retired__=0'%(tn2, tn1,
625+
linkid_name, tn2, tn2)
626+
self.db.sql('select %s from %s%s'%(nodeid_name, multilink_table, w))
614627
s = ','.join([str(x[0]) for x in self.db.sql_fetchall()])
615628
return '_%s.id not in (%s)'%(classname, s)
616629

roundup/backends/rdbms_common.py

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1136,7 +1136,18 @@ def _materialize_multilink(self, classname, nodeid, node, propname):
11361136
tn = prop.table_name
11371137
lid = prop.linkid_name
11381138
nid = prop.nodeid_name
1139-
sql = 'select %s from %s where %s=%s' % (lid, tn, nid, self.arg)
1139+
w = ''
1140+
joi = ''
1141+
if prop.computed:
1142+
if isinstance(prop.rev_property, Link):
1143+
w = ' and %s.__retired__=0'%tn
1144+
else:
1145+
tn2 = '_' + prop.classname
1146+
joi = ', %s' % tn2
1147+
w = ' and %s.%s=%s.id and %s.__retired__=0'%(tn, lid,
1148+
tn2, tn2)
1149+
sql = 'select %s from %s%s where %s=%s%s' %(lid, tn, joi, nid,
1150+
self.arg, w)
11401151
self.sql(sql, (nodeid,))
11411152
# extract the first column from the result
11421153
# XXX numeric ids
@@ -2343,14 +2354,26 @@ def getnodeids(self, retired=None):
23432354
ids = [str(x[0]) for x in self.db.cursor.fetchall()]
23442355
return ids
23452356

2346-
def _subselect(self, classname, multilink_table, nodeid_name):
2357+
def _subselect(self, proptree):
23472358
"""Create a subselect. This is factored out because some
23482359
databases (hmm only one, so far) doesn't support subselects
23492360
look for "I can't believe it's not a toy RDBMS" in the mysql
23502361
backend.
23512362
"""
2352-
return '_%s.id not in (select %s from %s)'%(classname, nodeid_name,
2353-
multilink_table)
2363+
multilink_table = proptree.propclass.table_name
2364+
nodeid_name = proptree.propclass.nodeid_name
2365+
linkid_name = proptree.propclass.linkid_name
2366+
w = ''
2367+
if proptree.need_retired:
2368+
w = ' where %s.__retired__=0'%(multilink_table)
2369+
if proptree.need_child_retired:
2370+
tn1 = multilink_table
2371+
tn2 = '_' + proptree.classname
2372+
w = ', %s where %s.%s=%s.id and %s.__retired__=0'%(tn2,
2373+
tn1, linkid_name, tn2, tn2)
2374+
classname = proptree.parent.classname
2375+
return '_%s.id not in (select %s from %s%s)'%(classname, nodeid_name,
2376+
multilink_table, w)
23542377

23552378
# Some DBs order NULL values last. Set this variable in the backend
23562379
# for prepending an order by clause for each attribute that causes
@@ -2516,7 +2539,7 @@ def _filter_sql (self, search_matches, filterspec, srt=[], grp=[], retr=0,
25162539
if v in ('-1', ['-1'], []):
25172540
# only match rows that have count(linkid)=0 in the
25182541
# corresponding multilink table)
2519-
where.append(self._subselect(pcn, tn, nid))
2542+
where.append(self._subselect(p))
25202543
else:
25212544
frum.append(tn)
25222545
gen_join = True
@@ -2530,9 +2553,11 @@ def _filter_sql (self, search_matches, filterspec, srt=[], grp=[], retr=0,
25302553
if gen_join:
25312554
where.append('_%s.id=%s.%s'%(pln, tn, nid))
25322555

2533-
if p.children:
2556+
if p.children or p.need_child_retired:
25342557
frum.append('_%s as _%s' % (cn, ln))
25352558
where.append('%s.%s=_%s.id'%(tn, lid, ln))
2559+
if p.need_child_retired:
2560+
where.append('_%s.__retired__=0'%(ln))
25362561

25372562
if p.has_values:
25382563
if isinstance(v, type([])):
@@ -2541,6 +2566,9 @@ def _filter_sql (self, search_matches, filterspec, srt=[], grp=[], retr=0,
25412566
else:
25422567
where.append('%s.%s=%s'%(tn, lid, a))
25432568
args.append(v)
2569+
# Don't match retired nodes if rev_multilink
2570+
if p.need_retired:
2571+
where.append('%s.__retired__=0'%(tn))
25442572
if 'sort' in p.need_for:
25452573
assert not p.attr_sort_done and not p.sort_ids_needed
25462574
elif k == 'id':

roundup/hyperdb.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,8 @@ def __init__(self, db, cls, name, props, parent=None, retr=False):
489489
self.propclass = None
490490
self.orderby = []
491491
self.sql_idx = None # index of retrieved column in sql result
492+
self.need_retired = False
493+
self.need_child_retired = False
492494
if parent:
493495
self.root = parent.root
494496
self.depth = parent.depth + 1
@@ -526,6 +528,11 @@ def append(self, name, need_for='search', retr=False):
526528
child = self.__class__(self.db, cls, name, props, parent=self)
527529
child.need_for = {need_for: True}
528530
child.propclass = propclass
531+
if isinstance(propclass, Multilink) and self.props[name].computed:
532+
if isinstance(self.props[name].rev_property, Link):
533+
child.need_retired = True
534+
else:
535+
child.need_child_retired = True
529536
self.children.append(child)
530537
self.propdict[name] = child
531538
if retr and isinstance(child.propclass, Link):
@@ -594,9 +601,11 @@ def search(self, search_matches=None, sort=True, retired=False):
594601
s2.add(node [pn])
595602
items = s1.difference(s2)
596603
elif isinstance(p.propclass.rev_property, Link):
597-
items = set(cl.get(x, pn) for x in p.val)
604+
items = set(cl.get(x, pn) for x in p.val
605+
if not cl.is_retired(x))
598606
else:
599-
items = set().union(*(cl.get(x, pn) for x in p.val))
607+
items = set().union(*(cl.get(x, pn) for x in p.val
608+
if not cl.is_retired(x)))
600609
filterspec[p.name] = list(sorted(items))
601610
elif isinstance(p.val, type([])):
602611
exact = []

test/db_test_base.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1862,6 +1862,27 @@ def testFilteringRevLink(self):
18621862
ae(filt(None, {'issues.title': ['ts2']}), ['6'])
18631863
ae(filt(None, {'issues': ['-1']}), ['1', '2', '3', '4', '5'])
18641864
ae(filt(None, {'issues': '-1'}), ['1', '2', '3', '4', '5'])
1865+
def ls(x):
1866+
return list(sorted(x))
1867+
self.assertEqual(ls(self.db.user.get('6', 'issues')), ['1', '2'])
1868+
self.assertEqual(ls(self.db.user.get('7', 'issues')), ['3'])
1869+
self.assertEqual(ls(self.db.user.get('10', 'issues')), ['6', '7', '8'])
1870+
n = self.db.user.getnode('6')
1871+
self.assertEqual(ls(n.issues), ['1', '2'])
1872+
# Now retire some linked-to issues and retry
1873+
self.db.issue.retire('6')
1874+
self.db.issue.retire('2')
1875+
self.db.issue.retire('3')
1876+
self.db.commit()
1877+
for filt in filter, filter_iter:
1878+
ae(filt(None, {'issues': ['3', '4']}), ['8'])
1879+
ae(filt(None, {'issues': ['1', '4', '8']}), ['6', '8', '10'])
1880+
ae(filt(None, {'issues.title': ['ts2']}), [])
1881+
ae(filt(None, {'issues': ['-1']}), ['1', '2', '3', '4', '5', '7'])
1882+
ae(filt(None, {'issues': '-1'}), ['1', '2', '3', '4', '5', '7'])
1883+
self.assertEqual(ls(self.db.user.get('6', 'issues')), ['1'])
1884+
self.assertEqual(ls(self.db.user.get('7', 'issues')), [])
1885+
self.assertEqual(ls(self.db.user.get('10', 'issues')), ['7', '8'])
18651886

18661887
def testFilteringLinkSortSearchMultilink(self):
18671888
ae, filter, filter_iter = self.filteringSetup()
@@ -1912,8 +1933,29 @@ def testFilteringRevMultilink(self):
19121933
for filt in filter, filter_iter:
19131934
ae(filt(None, {ni: ['1', '2']}), ['4', '5'])
19141935
ae(filt(None, {ni: ['6','7']}), ['3', '4', '5'])
1936+
ae(filt(None, {'nosy_issues.title': ['ts2']}), ['5'])
19151937
ae(filt(None, {ni: ['-1']}), ['1', '2', '6', '7', '8', '9', '10'])
19161938
ae(filt(None, {ni: '-1'}), ['1', '2', '6', '7', '8', '9', '10'])
1939+
def ls(x):
1940+
return list(sorted(x))
1941+
self.assertEqual(ls(self.db.user.get('4', ni)), ['1', '6'])
1942+
self.assertEqual(ls(self.db.user.get('5', ni)), ['2', '6', '7'])
1943+
n = self.db.user.getnode('4')
1944+
self.assertEqual(ls(n.nosy_issues), ['1', '6'])
1945+
# Now retire some linked-to issues and retry
1946+
self.db.issue.retire('2')
1947+
self.db.issue.retire('6')
1948+
self.db.commit()
1949+
for filt in filter, filter_iter:
1950+
ae(filt(None, {ni: ['1', '2']}), ['4'])
1951+
ae(filt(None, {ni: ['6','7']}), ['5'])
1952+
ae(filt(None, {'nosy_issues.title': ['ts2']}), [])
1953+
ae(filt(None, {ni: ['-1']}),
1954+
['1', '2', '3', '6', '7', '8', '9', '10'])
1955+
ae(filt(None, {ni: '-1'}),
1956+
['1', '2', '3', '6', '7', '8', '9', '10'])
1957+
self.assertEqual(ls(self.db.user.get('4', ni)), ['1'])
1958+
self.assertEqual(ls(self.db.user.get('5', ni)), ['7'])
19171959

19181960
def testFilteringMany(self):
19191961
ae, filter, filter_iter = self.filteringSetup()

0 commit comments

Comments
 (0)