| 4 | | from dejavu import logic, codewalk |
|---|
| 5 | | |
|---|
| 6 | | |
|---|
| 7 | | __all__ = [ |
|---|
| 8 | | # Decompilation |
|---|
| 9 | | 'ConstWrapper', 'ColumnWrapper', 'Sentinel', |
|---|
| 10 | | 'cannot_represent', 'kw_arg', 'SQLDecompiler', |
|---|
| 11 | | |
|---|
| 12 | | # Select (including Join) |
|---|
| 13 | | 'TableWrapper', 'Join', 'SelectWriter', |
|---|
| 14 | | ] |
|---|
| 15 | | |
|---|
| 16 | | |
|---|
| 17 | | # -------------------------- SQL DECOMPILATION -------------------------- # |
|---|
| 18 | | |
|---|
| 19 | | |
|---|
| 20 | | class ConstWrapper(str): |
|---|
| 21 | | """Wraps a constant for use in SQLDecompiler's stack. |
|---|
| 22 | | |
|---|
| 23 | | When we hit LOAD_CONST while decompiling, we occasionally need to keep |
|---|
| 24 | | both the base and the coerced value around (see COMPARE_OP for use |
|---|
| 25 | | of ConstWrapper.basevalue). |
|---|
| 26 | | """ |
|---|
| 27 | | def __new__(self, basevalue, coerced_value): |
|---|
| 28 | | newobj = str.__new__(ConstWrapper, coerced_value) |
|---|
| 29 | | newobj.basevalue = basevalue |
|---|
| 30 | | return newobj |
|---|
| 31 | | |
|---|
| 32 | | |
|---|
| 33 | | class ColumnWrapper(str): |
|---|
| 34 | | """Wraps a column for use in SQLDecompiler's stack. |
|---|
| 35 | | |
|---|
| 36 | | When we hit LOAD_ATTR while decompiling, we occasionally need to keep |
|---|
| 37 | | the column type around (see COMPARE_OP for use of ColumnWrapper.type). |
|---|
| 38 | | """ |
|---|
| 39 | | def __new__(self, value, col): |
|---|
| 40 | | newobj = str.__new__(ColumnWrapper, value) |
|---|
| 41 | | newobj.col = col |
|---|
| 42 | | return newobj |
|---|
| 43 | | |
|---|
| 44 | | |
|---|
| 45 | | # Stack sentinels |
|---|
| 46 | | class Sentinel(object): |
|---|
| 47 | | |
|---|
| 48 | | def __init__(self, name): |
|---|
| 49 | | self.name = name |
|---|
| 50 | | |
|---|
| 51 | | def __repr__(self): |
|---|
| 52 | | return 'Stack Sentinel: %s' % self.name |
|---|
| 53 | | |
|---|
| 54 | | kw_arg = Sentinel('Keyword Arg') |
|---|
| 55 | | # cannot_represent exists so that a portion of an Expression can be |
|---|
| 56 | | # labeled imperfect. For example, the function "iscurrentweek" |
|---|
| 57 | | # rarely has an SQL equivalent. All rows (which match the rest of the |
|---|
| 58 | | # Expression) will be recalled; they can then be compared in expr(unit). |
|---|
| 59 | | cannot_represent = Sentinel('Cannot Repr') |
|---|
| 60 | | |
|---|
| 61 | | |
|---|
| 62 | | class SQLDecompiler(codewalk.LambdaDecompiler): |
|---|
| 63 | | """Produce SQL from a supplied Expression object. |
|---|
| 64 | | |
|---|
| 65 | | Attributes of each argument in the signature will be mapped to table |
|---|
| 66 | | columns. Keyword arguments should be bound using Expression.bind_args |
|---|
| 67 | | before calling this decompiler. |
|---|
| 68 | | """ |
|---|
| 69 | | |
|---|
| 70 | | # Some constants are function or class objects, |
|---|
| 71 | | # which should not be coerced. |
|---|
| 72 | | no_coerce = (FunctionType, |
|---|
| 73 | | type, |
|---|
| 74 | | type(len), # <type 'builtin_function_or_method'> |
|---|
| 75 | | ) |
|---|
| 76 | | |
|---|
| 77 | | sql_cmp_op = ('<', '<=', '=', '!=', '>', '>=', 'in', 'not in') |
|---|
| 78 | | |
|---|
| 79 | | def __init__(self, tables, expr, adapter): |
|---|
| 80 | | self.tables = tables |
|---|
| 81 | | self.expr = expr |
|---|
| 82 | | self.adapter = adapter |
|---|
| 83 | | # Cache coerced booleans |
|---|
| 84 | | self.T = adapter.coerce_bool_to_any(True) |
|---|
| 85 | | self.F = adapter.coerce_bool_to_any(False) |
|---|
| 86 | | obj = expr.func |
|---|
| 87 | | codewalk.LambdaDecompiler.__init__(self, obj) |
|---|
| 88 | | |
|---|
| 89 | | def code(self): |
|---|
| 90 | | self.imperfect = False |
|---|
| 91 | | self.walk() |
|---|
| 92 | | # After walk(), self.stack should be reduced to a single string, |
|---|
| 93 | | # which is the SQL representation of our Expression. |
|---|
| 94 | | result = self.stack[0] |
|---|
| 95 | | if result is cannot_represent: |
|---|
| 96 | | # The entire expression could not be evaluated. |
|---|
| 97 | | result = self.adapter.bool_true |
|---|
| 98 | | if result == self.T: |
|---|
| 99 | | result = self.adapter.bool_true |
|---|
| 100 | | if result == self.F: |
|---|
| 101 | | result = self.adapter.bool_false |
|---|
| 102 | | return result |
|---|
| 103 | | |
|---|
| 104 | | def visit_instruction(self, op, lo=None, hi=None): |
|---|
| 105 | | # Get the instruction pointer for the current instruction. |
|---|
| 106 | | ip = self.cursor - 3 |
|---|
| 107 | | if hi is None: |
|---|
| 108 | | ip += 1 |
|---|
| 109 | | if lo is None: |
|---|
| 110 | | ip += 1 |
|---|
| 111 | | |
|---|
| 112 | | terms = self.targets.get(ip) |
|---|
| 113 | | if terms: |
|---|
| 114 | | trueval = self.adapter.bool_true |
|---|
| 115 | | falseval = self.adapter.bool_false |
|---|
| 116 | | clause = self.stack[-1] |
|---|
| 117 | | while terms: |
|---|
| 118 | | term, oper = terms.pop() |
|---|
| 119 | | if term is cannot_represent: |
|---|
| 120 | | # Use TRUE for the term, so all records are returned. |
|---|
| 121 | | term = trueval |
|---|
| 122 | | if clause is cannot_represent: |
|---|
| 123 | | # Use TRUE for the clause, so all records are returned. |
|---|
| 124 | | clause = trueval |
|---|
| 125 | | |
|---|
| 126 | | # Blurg. SQL Server is *so* picky. |
|---|
| 127 | | if term == self.T: |
|---|
| 128 | | term = trueval |
|---|
| 129 | | elif term == self.F: |
|---|
| 130 | | term = falseval |
|---|
| 131 | | if clause == self.T: |
|---|
| 132 | | clause = trueval |
|---|
| 133 | | elif clause == self.F: |
|---|
| 134 | | clause = falseval |
|---|
| 135 | | |
|---|
| 136 | | clause = "(%s) %s (%s)" % (term, oper.upper(), clause) |
|---|
| 137 | | |
|---|
| 138 | | # Replace TOS with the new clause, so that further |
|---|
| 139 | | # combinations have access to it. |
|---|
| 140 | | self.stack[-1] = clause |
|---|
| 141 | | if self.verbose: |
|---|
| 142 | | self.debug("clause:", clause, "\n") |
|---|
| 143 | | |
|---|
| 144 | | if op == 1: |
|---|
| 145 | | # Py2.4: The current instruction is POP_TOP, which means |
|---|
| 146 | | # the previous is probably JUMP_*. If so, we're going to |
|---|
| 147 | | # pop the value we just placed on the stack and lose it. |
|---|
| 148 | | # We need to replace the entry that the JUMP_* made in |
|---|
| 149 | | # self.targets with our new TOS. |
|---|
| 150 | | target = self.targets[self.last_target_ip] |
|---|
| 151 | | target[-1] = ((clause, target[-1][1])) |
|---|
| 152 | | if self.verbose: |
|---|
| 153 | | self.debug("newtarget:", self.last_target_ip, target) |
|---|
| 154 | | |
|---|
| 155 | | def visit_LOAD_DEREF(self, lo, hi): |
|---|
| 156 | | raise ValueError("Illegal reference found in %s." % self.expr) |
|---|
| 157 | | |
|---|
| 158 | | def visit_LOAD_GLOBAL(self, lo, hi): |
|---|
| 159 | | raise ValueError("Illegal global found in %s." % self.expr) |
|---|
| 160 | | |
|---|
| 161 | | def visit_LOAD_FAST(self, lo, hi): |
|---|
| 162 | | arg_index = lo + (hi << 8) |
|---|
| 163 | | if arg_index < self.co_argcount: |
|---|
| 164 | | # We've hit a reference to a positional arg, which in our case |
|---|
| 165 | | # implies a reference to a DB table. Append the (qname, table) |
|---|
| 166 | | # tuple for later unpacking inside visit_LOAD_ATTR. |
|---|
| 167 | | self.stack.append(self.tables[arg_index]) |
|---|
| 168 | | else: |
|---|
| 169 | | # Since lambdas don't support local bindings, |
|---|
| 170 | | # any remaining local name must be a keyword arg. |
|---|
| 171 | | self.stack.append(kw_arg) |
|---|
| 172 | | |
|---|
| 173 | | def visit_LOAD_ATTR(self, lo, hi): |
|---|
| 174 | | name = self.co_names[lo + (hi << 8)] |
|---|
| 175 | | tos = self.stack.pop() |
|---|
| 176 | | if isinstance(tos, tuple): |
|---|
| 177 | | # The name in question refers to a DB column (see visit_LOAD_FAST). |
|---|
| 178 | | alias, table = tos |
|---|
| 179 | | col = table[name] |
|---|
| 180 | | atom = ColumnWrapper('%s.%s' % (alias, col.qname), col) |
|---|
| 181 | | else: |
|---|
| 182 | | # 'tos.name' will reference an attribute of the tos object. |
|---|
| 183 | | # Stick the tos and name in a tuple for later processing |
|---|
| 184 | | # (for example, in visit_CALL_FUNCTION). |
|---|
| 185 | | atom = (tos, name) |
|---|
| 186 | | self.stack.append(atom) |
|---|
| 187 | | |
|---|
| 188 | | def visit_LOAD_CONST(self, lo, hi): |
|---|
| 189 | | val = self.co_consts[lo + (hi << 8)] |
|---|
| 190 | | if not isinstance(val, self.no_coerce): |
|---|
| 191 | | val = ConstWrapper(val, self.adapter.coerce(val)) |
|---|
| 192 | | self.stack.append(val) |
|---|
| 193 | | |
|---|
| 194 | | def visit_BUILD_TUPLE(self, lo, hi): |
|---|
| 195 | | terms = ", ".join([self.stack.pop() for i in range(lo + hi << 8)]) |
|---|
| 196 | | self.stack.append("(" + terms + ")") |
|---|
| 197 | | |
|---|
| 198 | | visit_BUILD_LIST = visit_BUILD_TUPLE |
|---|
| 199 | | |
|---|
| 200 | | def visit_CALL_FUNCTION(self, lo, hi): |
|---|
| 201 | | kwargs = {} |
|---|
| 202 | | for i in xrange(hi): |
|---|
| 203 | | val = self.stack.pop() |
|---|
| 204 | | key = self.stack.pop() |
|---|
| 205 | | kwargs[key] = val |
|---|
| 206 | | kwargs = [k + "=" + v for k, v in kwargs.iteritems()] |
|---|
| 207 | | |
|---|
| 208 | | args = [] |
|---|
| 209 | | for i in xrange(lo): |
|---|
| 210 | | arg = self.stack.pop() |
|---|
| 211 | | args.append(arg) |
|---|
| 212 | | args.reverse() |
|---|
| 213 | | |
|---|
| 214 | | if kwargs: |
|---|
| 215 | | args += kwargs |
|---|
| 216 | | |
|---|
| 217 | | func = self.stack.pop() |
|---|
| 218 | | |
|---|
| 219 | | # Handle function objects. |
|---|
| 220 | | if isinstance(func, tuple): |
|---|
| 221 | | tos, name = func |
|---|
| 222 | | dispatch = getattr(self, "attr_" + name, None) |
|---|
| 223 | | if dispatch: |
|---|
| 224 | | self.stack.append(dispatch(tos, *args)) |
|---|
| 225 | | return |
|---|
| 226 | | else: |
|---|
| 227 | | funcname = func.__module__ + "_" + func.__name__ |
|---|
| 228 | | funcname = funcname.replace(".", "_") |
|---|
| 229 | | if funcname.startswith("_"): |
|---|
| 230 | | funcname = "func" + funcname |
|---|
| 231 | | dispatch = getattr(self, funcname, None) |
|---|
| 232 | | if dispatch: |
|---|
| 233 | | self.stack.append(dispatch(*args)) |
|---|
| 234 | | return |
|---|
| 235 | | |
|---|
| 236 | | self.stack.append(cannot_represent) |
|---|
| 237 | | self.imperfect = True |
|---|
| 238 | | |
|---|
| 239 | | def visit_COMPARE_OP(self, lo, hi): |
|---|
| 240 | | op2, op1 = self.stack.pop(), self.stack.pop() |
|---|
| 241 | | if op1 is cannot_represent or op2 is cannot_represent: |
|---|
| 242 | | self.stack.append(cannot_represent) |
|---|
| 243 | | return |
|---|
| 244 | | |
|---|
| 245 | | op = lo + (hi << 8) |
|---|
| 246 | | if op in (6, 7): # in, not in |
|---|
| 247 | | value = self.containedby(op1, op2) |
|---|
| 248 | | if op == 7: |
|---|
| 249 | | value = "NOT " + value |
|---|
| 250 | | self.stack.append(value) |
|---|
| 251 | | elif op1 == 'NULL': |
|---|
| 252 | | if op in (2, 8): # '==', is |
|---|
| 253 | | self.stack.append(op2 + " IS NULL") |
|---|
| 254 | | elif op in (3, 9): # '!=', 'is not' |
|---|
| 255 | | self.stack.append(op2 + " IS NOT NULL") |
|---|
| 256 | | else: |
|---|
| 257 | | raise ValueError("Non-equality Null comparisons not allowed.") |
|---|
| 258 | | elif op2 == 'NULL': |
|---|
| 259 | | if op in (2, 8): # '==', 'is' |
|---|
| 260 | | self.stack.append(op1 + " IS NULL") |
|---|
| 261 | | elif op in (3, 9): # '!=', 'is not' |
|---|
| 262 | | self.stack.append(op1 + " IS NOT NULL") |
|---|
| 263 | | else: |
|---|
| 264 | | raise ValueError("Non-equality Null comparisons not allowed.") |
|---|
| 265 | | else: |
|---|
| 266 | | try: |
|---|
| 267 | | op1, op2 = self._compare_constants(op1, op2) |
|---|
| 268 | | except TypeError: |
|---|
| 269 | | self.stack.append(cannot_represent) |
|---|
| 270 | | self.imperfect = True |
|---|
| 271 | | return |
|---|
| 272 | | # Comparison operators for strings are case-sensitive in PG et al. |
|---|
| 273 | | self.stack.append(op1 + " " + self.sql_cmp_op[op] + " " + op2) |
|---|
| 274 | | |
|---|
| 275 | | def _compare_constants(self, op1, op2): |
|---|
| 276 | | """Coerce/cast compared types. |
|---|
| 277 | | |
|---|
| 278 | | If a column value is compared to a constant and no coerce or cast |
|---|
| 279 | | adapter function is available, a TypeError is raised. |
|---|
| 280 | | """ |
|---|
| 281 | | col = getattr(op1, "col", None) |
|---|
| 282 | | if col: |
|---|
| 283 | | if isinstance(op2, ConstWrapper): |
|---|
| 284 | | if col.imperfect_type: |
|---|
| 285 | | # Try to cast the column to op2's type |
|---|
| 286 | | op1 = self.adapter.cast(op1, col.dbtype, |
|---|
| 287 | | type(op2.basevalue)) |
|---|
| 288 | | else: |
|---|
| 289 | | # Try to coerce op2 to the column's type |
|---|
| 290 | | op2 = self.adapter.coerce(op2.basevalue, col.dbtype) |
|---|
| 291 | | else: |
|---|
| 292 | | col = getattr(op2, "col", None) |
|---|
| 293 | | if col and isinstance(op1, ConstWrapper): |
|---|
| 294 | | if col.imperfect_type: |
|---|
| 295 | | # Try to cast the column to op1's type |
|---|
| 296 | | op2 = self.adapter.cast(op2, col.dbtype, |
|---|
| 297 | | type(op1.basevalue)) |
|---|
| 298 | | else: |
|---|
| 299 | | # Try to coerce op1 to the column's type |
|---|
| 300 | | op1 = self.adapter.coerce(op1.basevalue, col.dbtype) |
|---|
| 301 | | return op1, op2 |
|---|
| 302 | | |
|---|
| 303 | | def binary_op(self, op): |
|---|
| 304 | | op2, op1 = self.stack.pop(), self.stack.pop() |
|---|
| 305 | | self.stack.append(op1 + " " + op + " " + op2) |
|---|
| 306 | | |
|---|
| 307 | | def visit_BINARY_SUBSCR(self): |
|---|
| 308 | | # The only BINARY_SUBSCR used in Expressions should be kwargs[key]. |
|---|
| 309 | | name = self.stack.pop() |
|---|
| 310 | | tos = self.stack.pop() |
|---|
| 311 | | if tos is not kw_arg: |
|---|
| 312 | | raise ValueError("Subscript %s of %s object not allowed." |
|---|
| 313 | | % (name, tos)) |
|---|
| 314 | | # name, since formed in LOAD_CONST, may have extraneous quotes. |
|---|
| 315 | | name = name.strip("'\"") |
|---|
| 316 | | value = self.expr.kwargs[name] |
|---|
| 317 | | if not isinstance(value, self.no_coerce): |
|---|
| 318 | | value = ConstWrapper(value, self.adapter.coerce(value)) |
|---|
| 319 | | self.stack.append(value) |
|---|
| 320 | | |
|---|
| 321 | | def visit_UNARY_NOT(self): |
|---|
| 322 | | op = self.stack.pop() |
|---|
| 323 | | if op is cannot_represent: |
|---|
| 324 | | self.stack.append(cannot_represent) |
|---|
| 325 | | else: |
|---|
| 326 | | self.stack.append("NOT (" + op + ")") |
|---|
| 327 | | |
|---|
| 328 | | # --------------------------- Dispatchees --------------------------- # |
|---|
| 329 | | |
|---|
| 330 | | def attr_startswith(self, tos, arg): |
|---|
| 331 | | return tos + " LIKE '" + self.adapter.escape_like(arg) + "%'" |
|---|
| 332 | | |
|---|
| 333 | | def attr_endswith(self, tos, arg): |
|---|
| 334 | | return tos + " LIKE '%" + self.adapter.escape_like(arg) + "'" |
|---|
| 335 | | |
|---|
| 336 | | def containedby(self, op1, op2): |
|---|
| 337 | | if isinstance(op1, ConstWrapper): |
|---|
| 338 | | # Looking for text in a field. Use Like (reverse terms). |
|---|
| 339 | | return op2 + " LIKE '%" + self.adapter.escape_like(op1) + "%'" |
|---|
| 340 | | else: |
|---|
| 341 | | # Looking for field in (a, b, c) |
|---|
| 342 | | atoms = [self.adapter.coerce(x) for x in op2.basevalue] |
|---|
| 343 | | if atoms: |
|---|
| 344 | | return op1 + " IN (" + ", ".join(atoms) + ")" |
|---|
| 345 | | else: |
|---|
| 346 | | # Nothing will match the empty list, so return none. |
|---|
| 347 | | return self.adapter.bool_false |
|---|
| 348 | | |
|---|
| 349 | | def dejavu_icontainedby(self, op1, op2): |
|---|
| 350 | | if isinstance(op1, ConstWrapper): |
|---|
| 351 | | # Looking for text in a field. Use Like (reverse terms). |
|---|
| 352 | | return ("LOWER(" + op2 + ") LIKE '%" + |
|---|
| 353 | | self.adapter.escape_like(op1).lower() + "%'") |
|---|
| 354 | | else: |
|---|
| 355 | | # Looking for field in (a, b, c). |
|---|
| 356 | | # Force all args to lowercase for case-insensitive comparison. |
|---|
| 357 | | atoms = [self.adapter.coerce(x).lower() for x in op2.basevalue] |
|---|
| 358 | | return "LOWER(%s) IN (%s)" % (op1, ", ".join(atoms)) |
|---|
| 359 | | |
|---|
| 360 | | def dejavu_icontains(self, x, y): |
|---|
| 361 | | return self.dejavu_icontainedby(y, x) |
|---|
| 362 | | |
|---|
| 363 | | def dejavu_istartswith(self, x, y): |
|---|
| 364 | | return "LOWER(" + x + ") LIKE '" + self.adapter.escape_like(y) + "%'" |
|---|
| 365 | | |
|---|
| 366 | | def dejavu_iendswith(self, x, y): |
|---|
| 367 | | return "LOWER(" + x + ") LIKE '%" + self.adapter.escape_like(y) + "'" |
|---|
| 368 | | |
|---|
| 369 | | def dejavu_ieq(self, x, y): |
|---|
| 370 | | return "LOWER(" + x + ") = LOWER(" + y + ")" |
|---|
| 371 | | |
|---|
| 372 | | def dejavu_now(self): |
|---|
| 373 | | return "NOW()" |
|---|
| 374 | | |
|---|
| 375 | | def dejavu_today(self): |
|---|
| 376 | | return "CURRENT_DATE" |
|---|
| 377 | | |
|---|
| 378 | | def dejavu_year(self, x): |
|---|
| 379 | | return "YEAR(" + x + ")" |
|---|
| 380 | | |
|---|
| 381 | | def dejavu_month(self, x): |
|---|
| 382 | | return "MONTH(" + x + ")" |
|---|
| 383 | | |
|---|
| 384 | | def dejavu_day(self, x): |
|---|
| 385 | | return "DAY(" + x + ")" |
|---|
| 386 | | |
|---|
| 387 | | def func__builtin___len(self, x): |
|---|
| 388 | | return "LENGTH(" + x + ")" |
|---|
| 389 | | |
|---|
| | 2 | from dejavu import logic |
|---|
| | 3 | |
|---|
| | 4 | |
|---|
| | 5 | __all__ = ['TableWrapper', 'Join', 'SelectWriter'] |
|---|