comparison roundup/backends/rdbms_common.py @ 4466:f1fe6fd0aa61

Multilinks can be filtered by combining elements with AND, OR and NOT now. A javascript gui was added for "keywords", see issue2550648. Developed by Sascha Teichmann; funded by Intevation. (Bernhard Reiter)
author Bernhard Reiter <Bernhard.Reiter@intevation.de>
date Mon, 08 Nov 2010 16:21:02 +0000
parents 0f4a8fcb9a1d
children 34dce76bb202
comparison
equal deleted inserted replaced
4465:605f31a596b8 4466:f1fe6fd0aa61
69 except ImportError: 69 except ImportError:
70 from roundup.backends.indexer_rdbms import Indexer 70 from roundup.backends.indexer_rdbms import Indexer
71 from roundup.backends.sessions_rdbms import Sessions, OneTimeKeys 71 from roundup.backends.sessions_rdbms import Sessions, OneTimeKeys
72 from roundup.date import Range 72 from roundup.date import Range
73 73
74 from roundup.backends.back_anydbm import compile_expression
75
76
74 # dummy value meaning "argument not passed" 77 # dummy value meaning "argument not passed"
75 _marker = [] 78 _marker = []
76 79
77 def _num_cvt(num): 80 def _num_cvt(num):
78 num = str(num) 81 num = str(num)
98 cvar = 'RDBMS_'+name.upper() 101 cvar = 'RDBMS_'+name.upper()
99 if config[cvar] is not None: 102 if config[cvar] is not None:
100 d[name] = config[cvar] 103 d[name] = config[cvar]
101 return d 104 return d
102 105
106
107 class IdListOptimizer:
108 """ To prevent flooding the SQL parser of the underlaying
109 db engine with "x IN (1, 2, 3, ..., <large number>)" collapses
110 these cases to "x BETWEEN 1 AND <large number>".
111 """
112
113 def __init__(self):
114 self.ranges = []
115 self.singles = []
116
117 def append(self, nid):
118 """ Invariant: nid are ordered ascending """
119 if self.ranges:
120 last = self.ranges[-1]
121 if last[1] == nid-1:
122 last[1] = nid
123 return
124 if self.singles:
125 last = self.singles[-1]
126 if last == nid-1:
127 self.singles.pop()
128 self.ranges.append([last, nid])
129 return
130 self.singles.append(nid)
131
132 def where(self, field, placeholder):
133 ranges = self.ranges
134 singles = self.singles
135
136 if not singles and not ranges: return "(1=0)", []
137
138 if ranges:
139 between = '%s BETWEEN %s AND %s' % (
140 field, placeholder, placeholder)
141 stmnt = [between] * len(ranges)
142 else:
143 stmnt = []
144 if singles:
145 stmnt.append('%s in (%s)' % (
146 field, ','.join([placeholder]*len(singles))))
147
148 return '(%s)' % ' OR '.join(stmnt), sum(ranges, []) + singles
149
150 def __str__(self):
151 return "ranges: %r / singles: %r" % (self.ranges, self.singles)
152
153
103 class Database(FileStorage, hyperdb.Database, roundupdb.Database): 154 class Database(FileStorage, hyperdb.Database, roundupdb.Database):
104 """ Wrapper around an SQL database that presents a hyperdb interface. 155 """ Wrapper around an SQL database that presents a hyperdb interface.
105 156
106 - some functionality is specific to the actual SQL database, hence 157 - some functionality is specific to the actual SQL database, hence
107 the sql_* methods that are NotImplemented 158 the sql_* methods that are NotImplemented
167 218
168 def sql_fetchall(self): 219 def sql_fetchall(self):
169 """ Fetch all rows. If there's nothing to fetch, return []. 220 """ Fetch all rows. If there's nothing to fetch, return [].
170 """ 221 """
171 return self.cursor.fetchall() 222 return self.cursor.fetchall()
223
224 def sql_fetchiter(self):
225 """ Fetch all row as a generator
226 """
227 while True:
228 row = self.cursor.fetchone()
229 if not row: break
230 yield row
172 231
173 def sql_stringquote(self, value): 232 def sql_stringquote(self, value):
174 """ Quote the string so it's safe to put in the 'sql quotes' 233 """ Quote the string so it's safe to put in the 'sql quotes'
175 """ 234 """
176 return re.sub("'", "''", str(value)) 235 return re.sub("'", "''", str(value))
2132 # order_by_null_values = '(%s is not NULL)' 2191 # order_by_null_values = '(%s is not NULL)'
2133 # order_by_null_values = 'notnull(%s)' 2192 # order_by_null_values = 'notnull(%s)'
2134 # The format parameter is replaced with the attribute. 2193 # The format parameter is replaced with the attribute.
2135 order_by_null_values = None 2194 order_by_null_values = None
2136 2195
2196 def supports_subselects(self):
2197 '''Assuming DBs can do subselects, overwrite if they cannot.
2198 '''
2199 return True
2200
2201 def _filter_multilink_expression_fallback(
2202 self, classname, multilink_table, expr):
2203 '''This is a fallback for database that do not support
2204 subselects.'''
2205
2206 is_valid = expr.evaluate
2207
2208 last_id, kws = None, []
2209
2210 ids = IdListOptimizer()
2211 append = ids.append
2212
2213 # This join and the evaluation in program space
2214 # can be expensive for larger databases!
2215 # TODO: Find a faster way to collect the data needed
2216 # to evalute the expression.
2217 # Moving the expression evaluation into the database
2218 # would be nice but this tricky: Think about the cases
2219 # where the multilink table does not have join values
2220 # needed in evaluation.
2221
2222 stmnt = "SELECT c.id, m.linkid FROM _%s c " \
2223 "LEFT OUTER JOIN %s m " \
2224 "ON c.id = m.nodeid ORDER BY c.id" % (
2225 classname, multilink_table)
2226 self.db.sql(stmnt)
2227
2228 # collect all multilink items for a class item
2229 for nid, kw in self.db.sql_fetchiter():
2230 if nid != last_id:
2231 if last_id is None:
2232 last_id = nid
2233 else:
2234 # we have all multilink items -> evaluate!
2235 if is_valid(kws): append(last_id)
2236 last_id, kws = nid, []
2237 if kw is not None:
2238 kws.append(kw)
2239
2240 if last_id is not None and is_valid(kws):
2241 append(last_id)
2242
2243 # we have ids of the classname table
2244 return ids.where("_%s.id" % classname, self.db.arg)
2245
2246 def _filter_multilink_expression(self, classname, multilink_table, v):
2247 """ Filters out elements of the classname table that do not
2248 match the given expression.
2249 Returns tuple of 'WHERE' introns for the overall filter.
2250 """
2251 try:
2252 opcodes = [int(x) for x in v]
2253 if min(opcodes) >= -1: raise ValueError()
2254
2255 expr = compile_expression(opcodes)
2256
2257 if not self.supports_subselects():
2258 # We heavily rely on subselects. If there is
2259 # no decent support fall back to slower variant.
2260 return self._filter_multilink_expression_fallback(
2261 classname, multilink_table, expr)
2262
2263 atom = \
2264 "%s IN(SELECT linkid FROM %s WHERE nodeid=a.id)" % (
2265 self.db.arg,
2266 multilink_table)
2267
2268 intron = \
2269 "_%(classname)s.id in (SELECT id " \
2270 "FROM _%(classname)s AS a WHERE %(condition)s) " % {
2271 'classname' : classname,
2272 'condition' : expr.generate(lambda n: atom) }
2273
2274 values = []
2275 def collect_values(n): values.append(n.x)
2276 expr.visit(collect_values)
2277
2278 return intron, values
2279 except:
2280 # original behavior
2281 where = "%s.linkid in (%s)" % (
2282 multilink_table, ','.join([self.db.arg] * len(v)))
2283 return where, v, True # True to indicate original
2284
2137 def filter(self, search_matches, filterspec, sort=[], group=[]): 2285 def filter(self, search_matches, filterspec, sort=[], group=[]):
2138 """Return a list of the ids of the active nodes in this class that 2286 """Return a list of the ids of the active nodes in this class that
2139 match the 'filter' spec, sorted by the group spec and then the 2287 match the 'filter' spec, sorted by the group spec and then the
2140 sort spec 2288 sort spec
2141 2289
2211 # only match rows that have count(linkid)=0 in the 2359 # only match rows that have count(linkid)=0 in the
2212 # corresponding multilink table) 2360 # corresponding multilink table)
2213 where.append(self._subselect(pcn, tn)) 2361 where.append(self._subselect(pcn, tn))
2214 else: 2362 else:
2215 frum.append(tn) 2363 frum.append(tn)
2216 where.append('_%s.id=%s.nodeid'%(pln,tn)) 2364 gen_join = True
2365
2366 if p.has_values and isinstance(v, type([])):
2367 result = self._filter_multilink_expression(pln, tn, v)
2368 # XXX: We dont need an id join if we used the filter
2369 gen_join = len(result) == 3
2370
2371 if gen_join:
2372 where.append('_%s.id=%s.nodeid'%(pln,tn))
2373
2217 if p.children: 2374 if p.children:
2218 frum.append('_%s as _%s' % (cn, ln)) 2375 frum.append('_%s as _%s' % (cn, ln))
2219 where.append('%s.linkid=_%s.id'%(tn, ln)) 2376 where.append('%s.linkid=_%s.id'%(tn, ln))
2377
2220 if p.has_values: 2378 if p.has_values:
2221 if isinstance(v, type([])): 2379 if isinstance(v, type([])):
2222 s = ','.join([a for x in v]) 2380 where.append(result[0])
2223 where.append('%s.linkid in (%s)'%(tn, s)) 2381 args += result[1]
2224 args = args + v
2225 else: 2382 else:
2226 where.append('%s.linkid=%s'%(tn, a)) 2383 where.append('%s.linkid=%s'%(tn, a))
2227 args.append(v) 2384 args.append(v)
2228 if p.sort_type > 0: 2385 if p.sort_type > 0:
2229 assert not p.attr_sort_done and not p.sort_ids_needed 2386 assert not p.attr_sort_done and not p.sort_ids_needed

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