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