|
71 | 71 | from roundup.backends.sessions_rdbms import Sessions, OneTimeKeys |
72 | 72 | from roundup.date import Range |
73 | 73 |
|
| 74 | +from roundup.backends.back_anydbm import compile_expression |
| 75 | + |
| 76 | + |
74 | 77 | # dummy value meaning "argument not passed" |
75 | 78 | _marker = [] |
76 | 79 |
|
@@ -100,6 +103,54 @@ def connection_dict(config, dbnamestr=None): |
100 | 103 | d[name] = config[cvar] |
101 | 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 | 154 | class Database(FileStorage, hyperdb.Database, roundupdb.Database): |
104 | 155 | """ Wrapper around an SQL database that presents a hyperdb interface. |
105 | 156 |
|
@@ -170,6 +221,14 @@ def sql_fetchall(self): |
170 | 221 | """ |
171 | 222 | return self.cursor.fetchall() |
172 | 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 |
| 231 | + |
173 | 232 | def sql_stringquote(self, value): |
174 | 233 | """ Quote the string so it's safe to put in the 'sql quotes' |
175 | 234 | """ |
@@ -2134,6 +2193,95 @@ def _subselect(self, classname, multilink_table): |
2134 | 2193 | # The format parameter is replaced with the attribute. |
2135 | 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 | 2285 | def filter(self, search_matches, filterspec, sort=[], group=[]): |
2138 | 2286 | """Return a list of the ids of the active nodes in this class that |
2139 | 2287 | match the 'filter' spec, sorted by the group spec and then the |
@@ -2213,15 +2361,24 @@ def filter(self, search_matches, filterspec, sort=[], group=[]): |
2213 | 2361 | where.append(self._subselect(pcn, tn)) |
2214 | 2362 | else: |
2215 | 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 | 2374 | if p.children: |
2218 | 2375 | frum.append('_%s as _%s' % (cn, ln)) |
2219 | 2376 | where.append('%s.linkid=_%s.id'%(tn, ln)) |
| 2377 | + |
2220 | 2378 | if p.has_values: |
2221 | 2379 | if isinstance(v, type([])): |
2222 | | - s = ','.join([a for x in v]) |
2223 | | - where.append('%s.linkid in (%s)'%(tn, s)) |
2224 | | - args = args + v |
| 2380 | + where.append(result[0]) |
| 2381 | + args += result[1] |
2225 | 2382 | else: |
2226 | 2383 | where.append('%s.linkid=%s'%(tn, a)) |
2227 | 2384 | args.append(v) |
|
0 commit comments