Skip to content

Commit 8f8471d

Browse files
author
Ralf Schlatterbeck
committed
The whole filter method was replicated in back_mysql.py from rdbms_common.py
I've factored the reason for the difference, mysqls inability to do subselects. We now have a _subselect method that is overridden in back_mysql and most of the filter method stays in rdbms_common. This passed all tests for mysql, postgresql and sqlite with python2.4 and python2.3. I've done this as a prerequisite for doing some surgery on filter.
1 parent c99a675 commit 8f8471d

File tree

2 files changed

+18
-273
lines changed

2 files changed

+18
-273
lines changed

roundup/backends/back_mysql.py

Lines changed: 7 additions & 269 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#$Id: back_mysql.py,v 1.68 2006-04-27 03:40:42 richard Exp $
1+
#$Id: back_mysql.py,v 1.69 2006-07-07 15:04:28 schlatterbeck Exp $
22
#
33
# Copyright (c) 2003 Martynas Sklyzmantas, Andrey Lebedev <[email protected]>
44
#
@@ -550,275 +550,13 @@ def sql_close(self):
550550
raise
551551

552552
class MysqlClass:
553-
# we're overriding this method for ONE missing bit of functionality.
554-
# look for "I can't believe it's not a toy RDBMS" below
555-
def filter(self, search_matches, filterspec, sort=(None,None),
556-
group=(None,None)):
557-
'''Return a list of the ids of the active nodes in this class that
558-
match the 'filter' spec, sorted by the group spec and then the
559-
sort spec
560-
561-
"filterspec" is {propname: value(s)}
562-
563-
"sort" and "group" are (dir, prop) where dir is '+', '-' or None
564-
and prop is a prop name or None
565-
566-
"search_matches" is {nodeid: marker} or None
567-
568-
The filter must match all properties specificed. If the property
569-
value to match is a list:
570-
571-
1. String properties must match all elements in the list, and
572-
2. Other properties must match any of the elements in the list.
553+
def _subselect (self, cn, tn) :
554+
''' "I can't believe it's not a toy RDBMS"
555+
see, even toy RDBMSes like gadfly and sqlite can do sub-selects...
573556
'''
574-
# we can't match anything if search_matches is empty
575-
if search_matches == {}:
576-
return []
577-
578-
if __debug__:
579-
start_t = time.time()
580-
581-
cn = self.classname
582-
583-
# vars to hold the components of the SQL statement
584-
frum = ['_'+cn] # FROM clauses
585-
loj = [] # LEFT OUTER JOIN clauses
586-
where = [] # WHERE clauses
587-
args = [] # *any* positional arguments
588-
a = self.db.arg
589-
590-
# figure the WHERE clause from the filterspec
591-
props = self.getprops()
592-
mlfilt = 0 # are we joining with Multilink tables?
593-
for k, v in filterspec.items():
594-
propclass = props[k]
595-
# now do other where clause stuff
596-
if isinstance(propclass, Multilink):
597-
mlfilt = 1
598-
tn = '%s_%s'%(cn, k)
599-
if v in ('-1', ['-1']):
600-
# only match rows that have count(linkid)=0 in the
601-
# corresponding multilink table)
602-
603-
# "I can't believe it's not a toy RDBMS"
604-
# see, even toy RDBMSes like gadfly and sqlite can do
605-
# sub-selects...
606-
self.db.sql('select nodeid from %s'%tn)
607-
s = ','.join([x[0] for x in self.db.sql_fetchall()])
608-
609-
where.append('_%s.id not in (%s)'%(cn, s))
610-
elif isinstance(v, type([])):
611-
frum.append(tn)
612-
s = ','.join([a for x in v])
613-
where.append('_%s.id=%s.nodeid and %s.linkid in (%s)'%(cn,
614-
tn, tn, s))
615-
args = args + v
616-
else:
617-
frum.append(tn)
618-
where.append('_%s.id=%s.nodeid and %s.linkid=%s'%(cn, tn,
619-
tn, a))
620-
args.append(v)
621-
elif k == 'id':
622-
if isinstance(v, type([])):
623-
s = ','.join([a for x in v])
624-
where.append('_%s.%s in (%s)'%(cn, k, s))
625-
args = args + v
626-
else:
627-
where.append('_%s.%s=%s'%(cn, k, a))
628-
args.append(v)
629-
elif isinstance(propclass, String):
630-
if not isinstance(v, type([])):
631-
v = [v]
632-
633-
# Quote the bits in the string that need it and then embed
634-
# in a "substring" search. Note - need to quote the '%' so
635-
# they make it through the python layer happily
636-
v = ['%%'+self.db.sql_stringquote(s)+'%%' for s in v]
637-
638-
# now add to the where clause
639-
where.append('('
640-
+' and '.join(["_%s._%s LIKE '%s'"%(cn, k, s) for s in v])
641-
+')')
642-
# note: args are embedded in the query string now
643-
elif isinstance(propclass, Link):
644-
if isinstance(v, type([])):
645-
d = {}
646-
for entry in v:
647-
if entry == '-1':
648-
entry = None
649-
d[entry] = entry
650-
l = []
651-
if d.has_key(None) or not d:
652-
del d[None]
653-
l.append('_%s._%s is NULL'%(cn, k))
654-
if d:
655-
v = d.keys()
656-
s = ','.join([a for x in v])
657-
l.append('(_%s._%s in (%s))'%(cn, k, s))
658-
args = args + v
659-
if l:
660-
where.append('(' + ' or '.join(l) +')')
661-
else:
662-
if v in ('-1', None):
663-
v = None
664-
where.append('_%s._%s is NULL'%(cn, k))
665-
else:
666-
where.append('_%s._%s=%s'%(cn, k, a))
667-
args.append(v)
668-
elif isinstance(propclass, Date):
669-
dc = self.db.hyperdb_to_sql_value[hyperdb.Date]
670-
if isinstance(v, type([])):
671-
s = ','.join([a for x in v])
672-
where.append('_%s._%s in (%s)'%(cn, k, s))
673-
args = args + [dc(date.Date(x)) for x in v]
674-
else:
675-
try:
676-
# Try to filter on range of dates
677-
date_rng = propclass.range_from_raw (v, self.db)
678-
if date_rng.from_value:
679-
where.append('_%s._%s >= %s'%(cn, k, a))
680-
args.append(dc(date_rng.from_value))
681-
if date_rng.to_value:
682-
where.append('_%s._%s <= %s'%(cn, k, a))
683-
args.append(dc(date_rng.to_value))
684-
except ValueError:
685-
# If range creation fails - ignore that search parameter
686-
pass
687-
elif isinstance(propclass, Interval):
688-
# filter using the __<prop>_int__ column
689-
if isinstance(v, type([])):
690-
s = ','.join([a for x in v])
691-
where.append('_%s.__%s_int__ in (%s)'%(cn, k, s))
692-
args = args + [date.Interval(x).as_seconds() for x in v]
693-
else:
694-
try:
695-
# Try to filter on range of intervals
696-
date_rng = Range(v, date.Interval)
697-
if date_rng.from_value:
698-
where.append('_%s.__%s_int__ >= %s'%(cn, k, a))
699-
args.append(date_rng.from_value.as_seconds())
700-
if date_rng.to_value:
701-
where.append('_%s.__%s_int__ <= %s'%(cn, k, a))
702-
args.append(date_rng.to_value.as_seconds())
703-
except ValueError:
704-
# If range creation fails - ignore that search parameter
705-
pass
706-
else:
707-
if isinstance(v, type([])):
708-
s = ','.join([a for x in v])
709-
where.append('_%s._%s in (%s)'%(cn, k, s))
710-
args = args + v
711-
else:
712-
where.append('_%s._%s=%s'%(cn, k, a))
713-
args.append(v)
714-
715-
# don't match retired nodes
716-
where.append('_%s.__retired__ <> 1'%cn)
717-
718-
# add results of full text search
719-
if search_matches is not None:
720-
v = search_matches.keys()
721-
s = ','.join([a for x in v])
722-
where.append('_%s.id in (%s)'%(cn, s))
723-
args = args + v
724-
725-
# sanity check: sorting *and* grouping on the same property?
726-
if group[1] == sort[1]:
727-
sort = (None, None)
728-
729-
# "grouping" is just the first-order sorting in the SQL fetch
730-
orderby = []
731-
ordercols = []
732-
mlsort = []
733-
rhsnum = 0
734-
for sortby in group, sort:
735-
sdir, prop = sortby
736-
if sdir and prop:
737-
if isinstance(props[prop], Multilink):
738-
mlsort.append(sortby)
739-
continue
740-
elif isinstance(props[prop], Interval):
741-
# use the int column for sorting
742-
o = '__'+prop+'_int__'
743-
ordercols.append(o)
744-
elif isinstance(props[prop], Link):
745-
# determine whether the linked Class has an order property
746-
lcn = props[prop].classname
747-
link = self.db.classes[lcn]
748-
o = '_%s._%s'%(cn, prop)
749-
op = link.orderprop ()
750-
if op != 'id':
751-
tn = '_' + lcn
752-
rhs = 'rhs%s_'%rhsnum
753-
rhsnum += 1
754-
loj.append('LEFT OUTER JOIN %s as %s on %s=%s.id'%(
755-
tn, rhs, o, rhs))
756-
o = '%s._%s'%(rhs, op)
757-
ordercols.append(o)
758-
elif prop == 'id':
759-
o = '_%s.id'%cn
760-
else:
761-
o = '_%s._%s'%(cn, prop)
762-
ordercols.append(o)
763-
if sdir == '-':
764-
o += ' desc'
765-
orderby.append(o)
766-
767-
# construct the SQL
768-
frum = ','.join(frum)
769-
if where:
770-
where = ' where ' + (' and '.join(where))
771-
else:
772-
where = ''
773-
if mlfilt:
774-
# we're joining tables on the id, so we will get dupes if we
775-
# don't distinct()
776-
cols = ['distinct(_%s.id)'%cn]
777-
else:
778-
cols = ['_%s.id'%cn]
779-
if orderby:
780-
cols = cols + ordercols
781-
order = ' order by %s'%(','.join(orderby))
782-
else:
783-
order = ''
784-
cols = ','.join(cols)
785-
loj = ' '.join(loj)
786-
sql = 'select %s from %s %s %s%s'%(cols, frum, loj, where, order)
787-
args = tuple(args)
788-
self.db.sql(sql, args)
789-
l = self.db.cursor.fetchall()
790-
791-
# return the IDs (the first column)
792-
# XXX numeric ids
793-
l = [str(row[0]) for row in l]
794-
795-
if not mlsort:
796-
if __debug__:
797-
self.db.stats['filtering'] += (time.time() - start_t)
798-
return l
799-
800-
# ergh. someone wants to sort by a multilink.
801-
r = []
802-
for id in l:
803-
m = []
804-
for ml in mlsort:
805-
m.append(self.get(id, ml[1]))
806-
r.append((id, m))
807-
i = 0
808-
for sortby in mlsort:
809-
def sortfun(a, b, dir=sortby[i], i=i):
810-
if dir == '-':
811-
return cmp(b[1][i], a[1][i])
812-
else:
813-
return cmp(a[1][i], b[1][i])
814-
r.sort(sortfun)
815-
i += 1
816-
r = [i[0] for i in r]
817-
818-
if __debug__:
819-
self.db.stats['filtering'] += (time.time() - start_t)
820-
821-
return r
557+
self.db.sql('select nodeid from %s'%tn)
558+
s = ','.join([x[0] for x in self.db.sql_fetchall()])
559+
return '_%s.id not in (%s)'%(cn, s)
822560

823561
class Class(MysqlClass, rdbms_common.Class):
824562
pass

roundup/backends/rdbms_common.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
1616
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
1717
#
18-
#$Id: rdbms_common.py,v 1.170 2006-04-27 05:15:17 richard Exp $
18+
#$Id: rdbms_common.py,v 1.171 2006-07-07 15:04:28 schlatterbeck Exp $
1919
''' Relational database (SQL) backend common code.
2020
2121
Basics:
@@ -2014,6 +2014,14 @@ def getnodeids(self, retired=None):
20142014
ids = [str(x[0]) for x in self.db.cursor.fetchall()]
20152015
return ids
20162016

2017+
def _subselect (self, cn, tn) :
2018+
'''Create a subselect. This is factored out because some
2019+
databases (hmm only one, so far) doesn't support subselects
2020+
look for "I can't believe it's not a toy RDBMS" in the mysql
2021+
backend.
2022+
'''
2023+
return '_%s.id not in (select nodeid from %s)'%(cn, tn)
2024+
20172025
def filter(self, search_matches, filterspec, sort=(None,None),
20182026
group=(None,None)):
20192027
'''Return a list of the ids of the active nodes in this class that
@@ -2061,8 +2069,7 @@ def filter(self, search_matches, filterspec, sort=(None,None),
20612069
if v in ('-1', ['-1']):
20622070
# only match rows that have count(linkid)=0 in the
20632071
# corresponding multilink table)
2064-
where.append('_%s.id not in (select nodeid from %s)'%(cn,
2065-
tn))
2072+
where.append(self._subselect(cn, tn))
20662073
elif isinstance(v, type([])):
20672074
frum.append(tn)
20682075
s = ','.join([a for x in v])
@@ -2126,7 +2133,7 @@ def filter(self, search_matches, filterspec, sort=(None,None),
21262133
if isinstance(v, type([])):
21272134
s = ','.join([a for x in v])
21282135
where.append('_%s._%s in (%s)'%(cn, k, s))
2129-
args = args + [dc(date.Date(v)) for x in v]
2136+
args = args + [dc(date.Date(x)) for x in v]
21302137
else:
21312138
try:
21322139
# Try to filter on range of dates

0 commit comments

Comments
 (0)