sqlglot.executor.python
1import collections 2import itertools 3import math 4 5from sqlglot import exp, planner, tokens 6from sqlglot.dialects.dialect import Dialect 7from sqlglot.errors import ExecuteError 8from sqlglot.executor.context import Context 9from sqlglot.executor.env import ENV 10from sqlglot.executor.table import RowReader, Table 11from sqlglot.generators.python import PythonGenerator 12 13 14class PythonExecutor: 15 def __init__(self, env=None, tables=None): 16 self.generator = Python().generator(identify=True, comments=False) 17 self.env = {**ENV, **(env or {})} 18 self.tables = tables or {} 19 20 def execute(self, plan): 21 finished = set() 22 queue = set(plan.leaves) 23 contexts = {} 24 25 while queue: 26 node = queue.pop() 27 try: 28 context = self.context( 29 { 30 name: table 31 for dep in node.dependencies 32 for name, table in contexts[dep].tables.items() 33 } 34 ) 35 36 if isinstance(node, planner.Scan): 37 contexts[node] = self.scan(node, context) 38 elif isinstance(node, planner.Aggregate): 39 contexts[node] = self.aggregate(node, context) 40 elif isinstance(node, planner.Join): 41 contexts[node] = self.join(node, context) 42 elif isinstance(node, planner.Sort): 43 contexts[node] = self.sort(node, context) 44 elif isinstance(node, planner.SetOperation): 45 contexts[node] = self.set_operation(node, context) 46 else: 47 raise NotImplementedError 48 49 finished.add(node) 50 51 for dep in node.dependents: 52 if all(d in contexts for d in dep.dependencies): 53 queue.add(dep) 54 55 for dep in node.dependencies: 56 if all(d in finished for d in dep.dependents): 57 contexts.pop(dep) 58 except Exception as e: 59 raise ExecuteError(f"Step '{node.id}' failed: {e}") from e 60 61 root = plan.root 62 return contexts[root].tables[root.name] 63 64 def generate(self, expression): 65 """Convert a SQL expression into literal Python code and compile it into bytecode.""" 66 if not expression: 67 return None 68 69 sql = self.generator.generate(expression) 70 return compile(sql, sql, "eval", optimize=2) 71 72 def generate_tuple(self, expressions): 73 """Convert an array of SQL expressions into tuple of Python byte code.""" 74 if not expressions: 75 return tuple() 76 return tuple(self.generate(expression) for expression in expressions) 77 78 def context(self, tables): 79 return Context(tables, env=self.env) 80 81 def table(self, expressions): 82 return Table( 83 expression.alias_or_name if isinstance(expression, exp.Expr) else expression 84 for expression in expressions 85 ) 86 87 def scan(self, step, context): 88 source = step.source 89 90 if source and isinstance(source, exp.Expr): 91 source = source.name or source.alias 92 93 if source is None: 94 context, table_iter = self.static() 95 elif source in context: 96 if not step.projections and not step.condition: 97 return self.context({step.name: context.tables[source]}) 98 table_iter = context.table_iter(source) 99 else: 100 context, table_iter = self.scan_table(step) 101 102 return self.context({step.name: self._project_and_filter(context, step, table_iter)}) 103 104 def _project_and_filter(self, context, step, table_iter): 105 sink = self.table(step.projections if step.projections else context.columns) 106 condition = self.generate(step.condition) 107 projections = self.generate_tuple(step.projections) 108 109 for reader in table_iter: 110 if len(sink) >= step.limit: 111 break 112 113 if condition and not context.eval(condition): 114 continue 115 116 if projections: 117 sink.append(context.eval_tuple(projections)) 118 else: 119 sink.append(reader.row) 120 121 return sink 122 123 def static(self): 124 return self.context({}), [RowReader(())] 125 126 def scan_table(self, step): 127 table = self.tables.find(step.source) 128 context = self.context({step.source.alias_or_name: table}) 129 return context, iter(table) 130 131 def join(self, step, context): 132 source = step.source_name 133 134 source_table = context.tables[source] 135 source_context = self.context({source: source_table}) 136 column_ranges = {source: range(0, len(source_table.columns))} 137 138 for name, join in step.joins.items(): 139 table = context.tables[name] 140 start = max(r.stop for r in column_ranges.values()) 141 column_ranges[name] = range(start, len(table.columns) + start) 142 join_context = self.context({name: table}) 143 condition = self.generate(join["condition"]) 144 condition_context = ( 145 self.context( 146 { 147 name: Table( 148 source_context.columns + join_context.columns, 149 column_range=column_range, 150 ) 151 for name, column_range in column_ranges.items() 152 } 153 ) 154 if condition 155 else None 156 ) 157 158 if join.get("source_key"): 159 table = self.hash_join( 160 join, source_context, join_context, condition, condition_context 161 ) 162 else: 163 table = self.nested_loop_join( 164 join, source_context, join_context, condition, condition_context 165 ) 166 167 source_context = self.context( 168 { 169 name: Table(table.columns, table.rows, column_range) 170 for name, column_range in column_ranges.items() 171 } 172 ) 173 if not step.condition and not step.projections: 174 return source_context 175 176 sink = self._project_and_filter( 177 source_context, 178 step, 179 (reader for reader, _ in iter(source_context)), 180 ) 181 182 if step.projections: 183 return self.context({step.name: sink}) 184 else: 185 return self.context( 186 { 187 name: Table(table.columns, sink.rows, table.column_range) 188 for name, table in source_context.tables.items() 189 } 190 ) 191 192 @staticmethod 193 def _join_matches(row, condition, condition_context): 194 if not condition: 195 return True 196 197 condition_context.set_row(row) 198 return condition_context.eval(condition) is True 199 200 def nested_loop_join(self, join, source_context, join_context, condition, condition_context): 201 table = Table(source_context.columns + join_context.columns) 202 source_rows = source_context.table.rows 203 join_rows = join_context.table.rows 204 matched_source = set() 205 matched_join = set() 206 207 for source_index, source_row in enumerate(source_rows): 208 for join_index, join_row in enumerate(join_rows): 209 row = source_row + join_row 210 if self._join_matches(row, condition, condition_context): 211 table.append(row) 212 matched_source.add(source_index) 213 matched_join.add(join_index) 214 215 self._append_unmatched_join_rows( 216 table, join, source_rows, join_rows, matched_source, matched_join 217 ) 218 219 return table 220 221 def hash_join(self, join, source_context, join_context, condition, condition_context): 222 source_key = self.generate_tuple(join["source_key"]) 223 join_key = self.generate_tuple(join["join_key"]) 224 results = collections.defaultdict(lambda: ([], [])) 225 226 for index, (reader, ctx) in enumerate(source_context): 227 key = ctx.eval_tuple(source_key) 228 if all(value is not None for value in key): 229 results[key][0].append((index, reader.row)) 230 for index, (reader, ctx) in enumerate(join_context): 231 key = ctx.eval_tuple(join_key) 232 if all(value is not None for value in key): 233 results[key][1].append((index, reader.row)) 234 235 table = Table(source_context.columns + join_context.columns) 236 matched_source = set() 237 matched_join = set() 238 239 for source_group, join_group in results.values(): 240 for (source_index, source_row), (join_index, join_row) in itertools.product( 241 source_group, join_group 242 ): 243 row = source_row + join_row 244 if self._join_matches(row, condition, condition_context): 245 table.append(row) 246 matched_source.add(source_index) 247 matched_join.add(join_index) 248 249 self._append_unmatched_join_rows( 250 table, 251 join, 252 source_context.table.rows, 253 join_context.table.rows, 254 matched_source, 255 matched_join, 256 ) 257 258 return table 259 260 @staticmethod 261 def _append_unmatched_join_rows( 262 table, join, source_rows, join_rows, matched_source, matched_join 263 ): 264 side = join.get("side") 265 if side in ("LEFT", "FULL"): 266 join_nulls = (None,) * (len(table.columns) - len(source_rows[0]) if source_rows else 0) 267 for index, row in enumerate(source_rows): 268 if index not in matched_source: 269 table.append(row + join_nulls) 270 271 if side in ("RIGHT", "FULL"): 272 source_width = len(table.columns) - (len(join_rows[0]) if join_rows else 0) 273 source_nulls = (None,) * source_width 274 for index, row in enumerate(join_rows): 275 if index not in matched_join: 276 table.append(source_nulls + row) 277 278 def aggregate(self, step, context): 279 group_by = self.generate_tuple(step.group.values()) 280 aggregations = self.generate_tuple(step.aggregations) 281 operands = self.generate_tuple(step.operands) 282 283 if operands: 284 operand_table = Table(self.table(step.operands).columns) 285 286 for reader, ctx in context: 287 operand_table.append(ctx.eval_tuple(operands)) 288 289 for i, (a, b) in enumerate(zip(context.table.rows, operand_table.rows)): 290 context.table.rows[i] = a + b 291 292 width = len(context.columns) 293 context.add_columns(*operand_table.columns) 294 295 operand_table = Table( 296 context.columns, 297 context.table.rows, 298 range(width, width + len(operand_table.columns)), 299 ) 300 301 context = self.context( 302 { 303 None: operand_table, 304 **context.tables, 305 } 306 ) 307 308 context.sort(group_by) 309 310 group = None 311 start = 0 312 end = 1 313 length = len(context.table) 314 table = self.table(list(step.group) + step.aggregations) 315 316 def add_row(): 317 table.append(group + context.eval_tuple(aggregations)) 318 319 if length: 320 for i in range(length): 321 context.set_index(i) 322 key = context.eval_tuple(group_by) 323 group = key if group is None else group 324 end += 1 325 if key != group: 326 context.set_range(start, end - 2) 327 add_row() 328 group = key 329 start = end - 2 330 if len(table.rows) >= step.limit: 331 break 332 if i == length - 1: 333 context.set_range(start, end - 1) 334 add_row() 335 elif step.limit > 0 and not group_by: 336 context.set_range(0, 0) 337 table.append(context.eval_tuple(aggregations)) 338 339 context = self.context({step.name: table, **{name: table for name in context.tables}}) 340 341 if step.projections or step.condition: 342 return self.scan(step, context) 343 return context 344 345 def sort(self, step, context): 346 projections = self.generate_tuple(step.projections) 347 projection_columns = [p.alias_or_name for p in step.projections] 348 all_columns = list(context.columns) + projection_columns 349 sink = self.table(all_columns) 350 for reader, ctx in context: 351 sink.append(reader.row + ctx.eval_tuple(projections)) 352 353 sort_ctx = self.context( 354 { 355 None: sink, 356 **{table: sink for table in context.tables}, 357 } 358 ) 359 sort_ctx.sort(self.generate_tuple(step.key)) 360 361 if not math.isinf(step.limit): 362 sort_ctx.table.rows = sort_ctx.table.rows[0 : step.limit] 363 364 output = Table( 365 projection_columns, 366 rows=[r[len(context.columns) : len(all_columns)] for r in sort_ctx.table.rows], 367 ) 368 return self.context({step.name: output}) 369 370 def set_operation(self, step, context): 371 left = context.tables[step.left] 372 right = context.tables[step.right] 373 374 sink = self.table(left.columns) 375 376 if issubclass(step.op, exp.Intersect): 377 right_counts = collections.Counter(right.rows) 378 seen = set() 379 for row in left.rows: 380 if right_counts[row] and (not step.distinct or row not in seen): 381 sink.append(row) 382 seen.add(row) 383 if not step.distinct: 384 right_counts[row] -= 1 385 elif issubclass(step.op, exp.Except): 386 right_counts = collections.Counter(right.rows) 387 seen = set() 388 for row in left.rows: 389 if right_counts[row] and not step.distinct: 390 right_counts[row] -= 1 391 elif not right_counts[row] and (not step.distinct or row not in seen): 392 sink.append(row) 393 seen.add(row) 394 elif issubclass(step.op, exp.Union) and step.distinct: 395 sink.rows = list(set(left.rows).union(set(right.rows))) 396 else: 397 sink.rows = left.rows + right.rows 398 399 if not math.isinf(step.limit): 400 sink.rows = sink.rows[0 : step.limit] 401 402 return self.context({step.name: sink}) 403 404 405class Python(Dialect): 406 class Tokenizer(tokens.Tokenizer): 407 STRING_ESCAPES = ["\\"] 408 409 Generator = PythonGenerator
class
PythonExecutor:
15class PythonExecutor: 16 def __init__(self, env=None, tables=None): 17 self.generator = Python().generator(identify=True, comments=False) 18 self.env = {**ENV, **(env or {})} 19 self.tables = tables or {} 20 21 def execute(self, plan): 22 finished = set() 23 queue = set(plan.leaves) 24 contexts = {} 25 26 while queue: 27 node = queue.pop() 28 try: 29 context = self.context( 30 { 31 name: table 32 for dep in node.dependencies 33 for name, table in contexts[dep].tables.items() 34 } 35 ) 36 37 if isinstance(node, planner.Scan): 38 contexts[node] = self.scan(node, context) 39 elif isinstance(node, planner.Aggregate): 40 contexts[node] = self.aggregate(node, context) 41 elif isinstance(node, planner.Join): 42 contexts[node] = self.join(node, context) 43 elif isinstance(node, planner.Sort): 44 contexts[node] = self.sort(node, context) 45 elif isinstance(node, planner.SetOperation): 46 contexts[node] = self.set_operation(node, context) 47 else: 48 raise NotImplementedError 49 50 finished.add(node) 51 52 for dep in node.dependents: 53 if all(d in contexts for d in dep.dependencies): 54 queue.add(dep) 55 56 for dep in node.dependencies: 57 if all(d in finished for d in dep.dependents): 58 contexts.pop(dep) 59 except Exception as e: 60 raise ExecuteError(f"Step '{node.id}' failed: {e}") from e 61 62 root = plan.root 63 return contexts[root].tables[root.name] 64 65 def generate(self, expression): 66 """Convert a SQL expression into literal Python code and compile it into bytecode.""" 67 if not expression: 68 return None 69 70 sql = self.generator.generate(expression) 71 return compile(sql, sql, "eval", optimize=2) 72 73 def generate_tuple(self, expressions): 74 """Convert an array of SQL expressions into tuple of Python byte code.""" 75 if not expressions: 76 return tuple() 77 return tuple(self.generate(expression) for expression in expressions) 78 79 def context(self, tables): 80 return Context(tables, env=self.env) 81 82 def table(self, expressions): 83 return Table( 84 expression.alias_or_name if isinstance(expression, exp.Expr) else expression 85 for expression in expressions 86 ) 87 88 def scan(self, step, context): 89 source = step.source 90 91 if source and isinstance(source, exp.Expr): 92 source = source.name or source.alias 93 94 if source is None: 95 context, table_iter = self.static() 96 elif source in context: 97 if not step.projections and not step.condition: 98 return self.context({step.name: context.tables[source]}) 99 table_iter = context.table_iter(source) 100 else: 101 context, table_iter = self.scan_table(step) 102 103 return self.context({step.name: self._project_and_filter(context, step, table_iter)}) 104 105 def _project_and_filter(self, context, step, table_iter): 106 sink = self.table(step.projections if step.projections else context.columns) 107 condition = self.generate(step.condition) 108 projections = self.generate_tuple(step.projections) 109 110 for reader in table_iter: 111 if len(sink) >= step.limit: 112 break 113 114 if condition and not context.eval(condition): 115 continue 116 117 if projections: 118 sink.append(context.eval_tuple(projections)) 119 else: 120 sink.append(reader.row) 121 122 return sink 123 124 def static(self): 125 return self.context({}), [RowReader(())] 126 127 def scan_table(self, step): 128 table = self.tables.find(step.source) 129 context = self.context({step.source.alias_or_name: table}) 130 return context, iter(table) 131 132 def join(self, step, context): 133 source = step.source_name 134 135 source_table = context.tables[source] 136 source_context = self.context({source: source_table}) 137 column_ranges = {source: range(0, len(source_table.columns))} 138 139 for name, join in step.joins.items(): 140 table = context.tables[name] 141 start = max(r.stop for r in column_ranges.values()) 142 column_ranges[name] = range(start, len(table.columns) + start) 143 join_context = self.context({name: table}) 144 condition = self.generate(join["condition"]) 145 condition_context = ( 146 self.context( 147 { 148 name: Table( 149 source_context.columns + join_context.columns, 150 column_range=column_range, 151 ) 152 for name, column_range in column_ranges.items() 153 } 154 ) 155 if condition 156 else None 157 ) 158 159 if join.get("source_key"): 160 table = self.hash_join( 161 join, source_context, join_context, condition, condition_context 162 ) 163 else: 164 table = self.nested_loop_join( 165 join, source_context, join_context, condition, condition_context 166 ) 167 168 source_context = self.context( 169 { 170 name: Table(table.columns, table.rows, column_range) 171 for name, column_range in column_ranges.items() 172 } 173 ) 174 if not step.condition and not step.projections: 175 return source_context 176 177 sink = self._project_and_filter( 178 source_context, 179 step, 180 (reader for reader, _ in iter(source_context)), 181 ) 182 183 if step.projections: 184 return self.context({step.name: sink}) 185 else: 186 return self.context( 187 { 188 name: Table(table.columns, sink.rows, table.column_range) 189 for name, table in source_context.tables.items() 190 } 191 ) 192 193 @staticmethod 194 def _join_matches(row, condition, condition_context): 195 if not condition: 196 return True 197 198 condition_context.set_row(row) 199 return condition_context.eval(condition) is True 200 201 def nested_loop_join(self, join, source_context, join_context, condition, condition_context): 202 table = Table(source_context.columns + join_context.columns) 203 source_rows = source_context.table.rows 204 join_rows = join_context.table.rows 205 matched_source = set() 206 matched_join = set() 207 208 for source_index, source_row in enumerate(source_rows): 209 for join_index, join_row in enumerate(join_rows): 210 row = source_row + join_row 211 if self._join_matches(row, condition, condition_context): 212 table.append(row) 213 matched_source.add(source_index) 214 matched_join.add(join_index) 215 216 self._append_unmatched_join_rows( 217 table, join, source_rows, join_rows, matched_source, matched_join 218 ) 219 220 return table 221 222 def hash_join(self, join, source_context, join_context, condition, condition_context): 223 source_key = self.generate_tuple(join["source_key"]) 224 join_key = self.generate_tuple(join["join_key"]) 225 results = collections.defaultdict(lambda: ([], [])) 226 227 for index, (reader, ctx) in enumerate(source_context): 228 key = ctx.eval_tuple(source_key) 229 if all(value is not None for value in key): 230 results[key][0].append((index, reader.row)) 231 for index, (reader, ctx) in enumerate(join_context): 232 key = ctx.eval_tuple(join_key) 233 if all(value is not None for value in key): 234 results[key][1].append((index, reader.row)) 235 236 table = Table(source_context.columns + join_context.columns) 237 matched_source = set() 238 matched_join = set() 239 240 for source_group, join_group in results.values(): 241 for (source_index, source_row), (join_index, join_row) in itertools.product( 242 source_group, join_group 243 ): 244 row = source_row + join_row 245 if self._join_matches(row, condition, condition_context): 246 table.append(row) 247 matched_source.add(source_index) 248 matched_join.add(join_index) 249 250 self._append_unmatched_join_rows( 251 table, 252 join, 253 source_context.table.rows, 254 join_context.table.rows, 255 matched_source, 256 matched_join, 257 ) 258 259 return table 260 261 @staticmethod 262 def _append_unmatched_join_rows( 263 table, join, source_rows, join_rows, matched_source, matched_join 264 ): 265 side = join.get("side") 266 if side in ("LEFT", "FULL"): 267 join_nulls = (None,) * (len(table.columns) - len(source_rows[0]) if source_rows else 0) 268 for index, row in enumerate(source_rows): 269 if index not in matched_source: 270 table.append(row + join_nulls) 271 272 if side in ("RIGHT", "FULL"): 273 source_width = len(table.columns) - (len(join_rows[0]) if join_rows else 0) 274 source_nulls = (None,) * source_width 275 for index, row in enumerate(join_rows): 276 if index not in matched_join: 277 table.append(source_nulls + row) 278 279 def aggregate(self, step, context): 280 group_by = self.generate_tuple(step.group.values()) 281 aggregations = self.generate_tuple(step.aggregations) 282 operands = self.generate_tuple(step.operands) 283 284 if operands: 285 operand_table = Table(self.table(step.operands).columns) 286 287 for reader, ctx in context: 288 operand_table.append(ctx.eval_tuple(operands)) 289 290 for i, (a, b) in enumerate(zip(context.table.rows, operand_table.rows)): 291 context.table.rows[i] = a + b 292 293 width = len(context.columns) 294 context.add_columns(*operand_table.columns) 295 296 operand_table = Table( 297 context.columns, 298 context.table.rows, 299 range(width, width + len(operand_table.columns)), 300 ) 301 302 context = self.context( 303 { 304 None: operand_table, 305 **context.tables, 306 } 307 ) 308 309 context.sort(group_by) 310 311 group = None 312 start = 0 313 end = 1 314 length = len(context.table) 315 table = self.table(list(step.group) + step.aggregations) 316 317 def add_row(): 318 table.append(group + context.eval_tuple(aggregations)) 319 320 if length: 321 for i in range(length): 322 context.set_index(i) 323 key = context.eval_tuple(group_by) 324 group = key if group is None else group 325 end += 1 326 if key != group: 327 context.set_range(start, end - 2) 328 add_row() 329 group = key 330 start = end - 2 331 if len(table.rows) >= step.limit: 332 break 333 if i == length - 1: 334 context.set_range(start, end - 1) 335 add_row() 336 elif step.limit > 0 and not group_by: 337 context.set_range(0, 0) 338 table.append(context.eval_tuple(aggregations)) 339 340 context = self.context({step.name: table, **{name: table for name in context.tables}}) 341 342 if step.projections or step.condition: 343 return self.scan(step, context) 344 return context 345 346 def sort(self, step, context): 347 projections = self.generate_tuple(step.projections) 348 projection_columns = [p.alias_or_name for p in step.projections] 349 all_columns = list(context.columns) + projection_columns 350 sink = self.table(all_columns) 351 for reader, ctx in context: 352 sink.append(reader.row + ctx.eval_tuple(projections)) 353 354 sort_ctx = self.context( 355 { 356 None: sink, 357 **{table: sink for table in context.tables}, 358 } 359 ) 360 sort_ctx.sort(self.generate_tuple(step.key)) 361 362 if not math.isinf(step.limit): 363 sort_ctx.table.rows = sort_ctx.table.rows[0 : step.limit] 364 365 output = Table( 366 projection_columns, 367 rows=[r[len(context.columns) : len(all_columns)] for r in sort_ctx.table.rows], 368 ) 369 return self.context({step.name: output}) 370 371 def set_operation(self, step, context): 372 left = context.tables[step.left] 373 right = context.tables[step.right] 374 375 sink = self.table(left.columns) 376 377 if issubclass(step.op, exp.Intersect): 378 right_counts = collections.Counter(right.rows) 379 seen = set() 380 for row in left.rows: 381 if right_counts[row] and (not step.distinct or row not in seen): 382 sink.append(row) 383 seen.add(row) 384 if not step.distinct: 385 right_counts[row] -= 1 386 elif issubclass(step.op, exp.Except): 387 right_counts = collections.Counter(right.rows) 388 seen = set() 389 for row in left.rows: 390 if right_counts[row] and not step.distinct: 391 right_counts[row] -= 1 392 elif not right_counts[row] and (not step.distinct or row not in seen): 393 sink.append(row) 394 seen.add(row) 395 elif issubclass(step.op, exp.Union) and step.distinct: 396 sink.rows = list(set(left.rows).union(set(right.rows))) 397 else: 398 sink.rows = left.rows + right.rows 399 400 if not math.isinf(step.limit): 401 sink.rows = sink.rows[0 : step.limit] 402 403 return self.context({step.name: sink})
def
execute(self, plan):
21 def execute(self, plan): 22 finished = set() 23 queue = set(plan.leaves) 24 contexts = {} 25 26 while queue: 27 node = queue.pop() 28 try: 29 context = self.context( 30 { 31 name: table 32 for dep in node.dependencies 33 for name, table in contexts[dep].tables.items() 34 } 35 ) 36 37 if isinstance(node, planner.Scan): 38 contexts[node] = self.scan(node, context) 39 elif isinstance(node, planner.Aggregate): 40 contexts[node] = self.aggregate(node, context) 41 elif isinstance(node, planner.Join): 42 contexts[node] = self.join(node, context) 43 elif isinstance(node, planner.Sort): 44 contexts[node] = self.sort(node, context) 45 elif isinstance(node, planner.SetOperation): 46 contexts[node] = self.set_operation(node, context) 47 else: 48 raise NotImplementedError 49 50 finished.add(node) 51 52 for dep in node.dependents: 53 if all(d in contexts for d in dep.dependencies): 54 queue.add(dep) 55 56 for dep in node.dependencies: 57 if all(d in finished for d in dep.dependents): 58 contexts.pop(dep) 59 except Exception as e: 60 raise ExecuteError(f"Step '{node.id}' failed: {e}") from e 61 62 root = plan.root 63 return contexts[root].tables[root.name]
def
generate(self, expression):
65 def generate(self, expression): 66 """Convert a SQL expression into literal Python code and compile it into bytecode.""" 67 if not expression: 68 return None 69 70 sql = self.generator.generate(expression) 71 return compile(sql, sql, "eval", optimize=2)
Convert a SQL expression into literal Python code and compile it into bytecode.
def
generate_tuple(self, expressions):
73 def generate_tuple(self, expressions): 74 """Convert an array of SQL expressions into tuple of Python byte code.""" 75 if not expressions: 76 return tuple() 77 return tuple(self.generate(expression) for expression in expressions)
Convert an array of SQL expressions into tuple of Python byte code.
def
scan(self, step, context):
88 def scan(self, step, context): 89 source = step.source 90 91 if source and isinstance(source, exp.Expr): 92 source = source.name or source.alias 93 94 if source is None: 95 context, table_iter = self.static() 96 elif source in context: 97 if not step.projections and not step.condition: 98 return self.context({step.name: context.tables[source]}) 99 table_iter = context.table_iter(source) 100 else: 101 context, table_iter = self.scan_table(step) 102 103 return self.context({step.name: self._project_and_filter(context, step, table_iter)})
def
join(self, step, context):
132 def join(self, step, context): 133 source = step.source_name 134 135 source_table = context.tables[source] 136 source_context = self.context({source: source_table}) 137 column_ranges = {source: range(0, len(source_table.columns))} 138 139 for name, join in step.joins.items(): 140 table = context.tables[name] 141 start = max(r.stop for r in column_ranges.values()) 142 column_ranges[name] = range(start, len(table.columns) + start) 143 join_context = self.context({name: table}) 144 condition = self.generate(join["condition"]) 145 condition_context = ( 146 self.context( 147 { 148 name: Table( 149 source_context.columns + join_context.columns, 150 column_range=column_range, 151 ) 152 for name, column_range in column_ranges.items() 153 } 154 ) 155 if condition 156 else None 157 ) 158 159 if join.get("source_key"): 160 table = self.hash_join( 161 join, source_context, join_context, condition, condition_context 162 ) 163 else: 164 table = self.nested_loop_join( 165 join, source_context, join_context, condition, condition_context 166 ) 167 168 source_context = self.context( 169 { 170 name: Table(table.columns, table.rows, column_range) 171 for name, column_range in column_ranges.items() 172 } 173 ) 174 if not step.condition and not step.projections: 175 return source_context 176 177 sink = self._project_and_filter( 178 source_context, 179 step, 180 (reader for reader, _ in iter(source_context)), 181 ) 182 183 if step.projections: 184 return self.context({step.name: sink}) 185 else: 186 return self.context( 187 { 188 name: Table(table.columns, sink.rows, table.column_range) 189 for name, table in source_context.tables.items() 190 } 191 )
def
nested_loop_join( self, join, source_context, join_context, condition, condition_context):
201 def nested_loop_join(self, join, source_context, join_context, condition, condition_context): 202 table = Table(source_context.columns + join_context.columns) 203 source_rows = source_context.table.rows 204 join_rows = join_context.table.rows 205 matched_source = set() 206 matched_join = set() 207 208 for source_index, source_row in enumerate(source_rows): 209 for join_index, join_row in enumerate(join_rows): 210 row = source_row + join_row 211 if self._join_matches(row, condition, condition_context): 212 table.append(row) 213 matched_source.add(source_index) 214 matched_join.add(join_index) 215 216 self._append_unmatched_join_rows( 217 table, join, source_rows, join_rows, matched_source, matched_join 218 ) 219 220 return table
def
hash_join( self, join, source_context, join_context, condition, condition_context):
222 def hash_join(self, join, source_context, join_context, condition, condition_context): 223 source_key = self.generate_tuple(join["source_key"]) 224 join_key = self.generate_tuple(join["join_key"]) 225 results = collections.defaultdict(lambda: ([], [])) 226 227 for index, (reader, ctx) in enumerate(source_context): 228 key = ctx.eval_tuple(source_key) 229 if all(value is not None for value in key): 230 results[key][0].append((index, reader.row)) 231 for index, (reader, ctx) in enumerate(join_context): 232 key = ctx.eval_tuple(join_key) 233 if all(value is not None for value in key): 234 results[key][1].append((index, reader.row)) 235 236 table = Table(source_context.columns + join_context.columns) 237 matched_source = set() 238 matched_join = set() 239 240 for source_group, join_group in results.values(): 241 for (source_index, source_row), (join_index, join_row) in itertools.product( 242 source_group, join_group 243 ): 244 row = source_row + join_row 245 if self._join_matches(row, condition, condition_context): 246 table.append(row) 247 matched_source.add(source_index) 248 matched_join.add(join_index) 249 250 self._append_unmatched_join_rows( 251 table, 252 join, 253 source_context.table.rows, 254 join_context.table.rows, 255 matched_source, 256 matched_join, 257 ) 258 259 return table
def
aggregate(self, step, context):
279 def aggregate(self, step, context): 280 group_by = self.generate_tuple(step.group.values()) 281 aggregations = self.generate_tuple(step.aggregations) 282 operands = self.generate_tuple(step.operands) 283 284 if operands: 285 operand_table = Table(self.table(step.operands).columns) 286 287 for reader, ctx in context: 288 operand_table.append(ctx.eval_tuple(operands)) 289 290 for i, (a, b) in enumerate(zip(context.table.rows, operand_table.rows)): 291 context.table.rows[i] = a + b 292 293 width = len(context.columns) 294 context.add_columns(*operand_table.columns) 295 296 operand_table = Table( 297 context.columns, 298 context.table.rows, 299 range(width, width + len(operand_table.columns)), 300 ) 301 302 context = self.context( 303 { 304 None: operand_table, 305 **context.tables, 306 } 307 ) 308 309 context.sort(group_by) 310 311 group = None 312 start = 0 313 end = 1 314 length = len(context.table) 315 table = self.table(list(step.group) + step.aggregations) 316 317 def add_row(): 318 table.append(group + context.eval_tuple(aggregations)) 319 320 if length: 321 for i in range(length): 322 context.set_index(i) 323 key = context.eval_tuple(group_by) 324 group = key if group is None else group 325 end += 1 326 if key != group: 327 context.set_range(start, end - 2) 328 add_row() 329 group = key 330 start = end - 2 331 if len(table.rows) >= step.limit: 332 break 333 if i == length - 1: 334 context.set_range(start, end - 1) 335 add_row() 336 elif step.limit > 0 and not group_by: 337 context.set_range(0, 0) 338 table.append(context.eval_tuple(aggregations)) 339 340 context = self.context({step.name: table, **{name: table for name in context.tables}}) 341 342 if step.projections or step.condition: 343 return self.scan(step, context) 344 return context
def
sort(self, step, context):
346 def sort(self, step, context): 347 projections = self.generate_tuple(step.projections) 348 projection_columns = [p.alias_or_name for p in step.projections] 349 all_columns = list(context.columns) + projection_columns 350 sink = self.table(all_columns) 351 for reader, ctx in context: 352 sink.append(reader.row + ctx.eval_tuple(projections)) 353 354 sort_ctx = self.context( 355 { 356 None: sink, 357 **{table: sink for table in context.tables}, 358 } 359 ) 360 sort_ctx.sort(self.generate_tuple(step.key)) 361 362 if not math.isinf(step.limit): 363 sort_ctx.table.rows = sort_ctx.table.rows[0 : step.limit] 364 365 output = Table( 366 projection_columns, 367 rows=[r[len(context.columns) : len(all_columns)] for r in sort_ctx.table.rows], 368 ) 369 return self.context({step.name: output})
def
set_operation(self, step, context):
371 def set_operation(self, step, context): 372 left = context.tables[step.left] 373 right = context.tables[step.right] 374 375 sink = self.table(left.columns) 376 377 if issubclass(step.op, exp.Intersect): 378 right_counts = collections.Counter(right.rows) 379 seen = set() 380 for row in left.rows: 381 if right_counts[row] and (not step.distinct or row not in seen): 382 sink.append(row) 383 seen.add(row) 384 if not step.distinct: 385 right_counts[row] -= 1 386 elif issubclass(step.op, exp.Except): 387 right_counts = collections.Counter(right.rows) 388 seen = set() 389 for row in left.rows: 390 if right_counts[row] and not step.distinct: 391 right_counts[row] -= 1 392 elif not right_counts[row] and (not step.distinct or row not in seen): 393 sink.append(row) 394 seen.add(row) 395 elif issubclass(step.op, exp.Union) and step.distinct: 396 sink.rows = list(set(left.rows).union(set(right.rows))) 397 else: 398 sink.rows = left.rows + right.rows 399 400 if not math.isinf(step.limit): 401 sink.rows = sink.rows[0 : step.limit] 402 403 return self.context({step.name: sink})
class
Python(sqlglot.dialects.dialect.Dialect):
406class Python(Dialect): 407 class Tokenizer(tokens.Tokenizer): 408 STRING_ESCAPES = ["\\"] 409 410 Generator = PythonGenerator
Generator =
<class 'sqlglot.generators.python.PythonGenerator'>
UNESCAPED_SEQUENCES: dict[str, str] =
{'\\a': '\x07', '\\b': '\x08', '\\f': '\x0c', '\\n': '\n', '\\r': '\r', '\\t': '\t', '\\v': '\x0b', '\\\\': '\\'}
Mapping of an escaped sequence (\n) to its unescaped version (
).
STRINGS_SUPPORT_ESCAPED_SEQUENCES: bool =
True
Whether string literals support escape sequences (e.g. \n). Set by the metaclass based on the tokenizer's STRING_ESCAPES.
BYTE_STRINGS_SUPPORT_ESCAPED_SEQUENCES: bool =
True
Whether byte string literals support escape sequences. Set by the metaclass based on the tokenizer's BYTE_STRING_ESCAPES.
tokenizer_class =
<class 'Python.Tokenizer'>
parser_class =
<class 'sqlglot.parsers.base.BaseParser'>
generator_class =
<class 'sqlglot.generators.python.PythonGenerator'>
INVERSE_TIME_MAPPING: dict[str, str] =
{'%mstrict': '%m', '%dstrict': '%d', '%Hstrict': '%H', '%Istrict': '%I', '%Mstrict': '%M', '%Sstrict': '%S'}
INVERSE_TIME_TRIE: dict =
{'%': {'m': {'s': {'t': {'r': {'i': {'c': {'t': {0: True}}}}}}}, 'd': {'s': {'t': {'r': {'i': {'c': {'t': {0: True}}}}}}}, 'H': {'s': {'t': {'r': {'i': {'c': {'t': {0: True}}}}}}}, 'I': {'s': {'t': {'r': {'i': {'c': {'t': {0: True}}}}}}}, 'M': {'s': {'t': {'r': {'i': {'c': {'t': {0: True}}}}}}}, 'S': {'s': {'t': {'r': {'i': {'c': {'t': {0: True}}}}}}}}}
INVERSE_FORMAT_MAPPING: dict[str, str] =
{'%mstrict': '%m', '%dstrict': '%d', '%Hstrict': '%H', '%Istrict': '%I', '%Mstrict': '%M', '%Sstrict': '%S'}
INVERSE_FORMAT_TRIE: dict =
{'%': {'m': {'s': {'t': {'r': {'i': {'c': {'t': {0: True}}}}}}}, 'd': {'s': {'t': {'r': {'i': {'c': {'t': {0: True}}}}}}}, 'H': {'s': {'t': {'r': {'i': {'c': {'t': {0: True}}}}}}}, 'I': {'s': {'t': {'r': {'i': {'c': {'t': {0: True}}}}}}}, 'M': {'s': {'t': {'r': {'i': {'c': {'t': {0: True}}}}}}}, 'S': {'s': {'t': {'r': {'i': {'c': {'t': {0: True}}}}}}}}}
ESCAPED_SEQUENCES: dict[str, str] =
{'\x07': '\\a', '\x08': '\\b', '\x0c': '\\f', '\n': '\\n', '\r': '\\r', '\t': '\\t', '\x0b': '\\v', '\\': '\\\\'}
VALID_INTERVAL_UNITS: set[str] =
{'D', 'MILS', 'DECADES', 'MINS', 'DW_ISO', 'MILLISECOND', 'MICROSECOND', 'CENTURIES', 'DAYOFWEEK_ISO', 'YEAR', 'MM', 'USECS', 'MILLENIA', 'MONTH', 'W', 'Y', 'MIL', 'DW', 'HR', 'SECONDS', 'MILLISECONDS', 'DAYOFYEAR', 'CENT', 'TZM', 'Q', 'EPOCH_MICROSECONDS', 'NANOSEC', 'QTR', 'NSEC', 'YY', 'NANOSECS', 'MI', 'CENTS', 'YRS', 'QUARTER', 'NANOSECOND', 'TZH', 'DAY OF YEAR', 'DOW_ISO', 'H', 'DAYOFWEEK', 'SECOND', 'EPOCH_MILLISECONDS', 'WEEKOFYEAR_ISO', 'WEEK', 'TIMEZONE_HOUR', 'MILLENNIUM', 'M', 'MSECOND', 'MILLISECON', 'WOY', 'EPOCH_SECONDS', 'MILLISECS', 'QUARTERS', 'EPOCH_SECOND', 'MILLISEC', 'HOURS', 'WEEK_ISO', 'SECS', 'MINUTE', 'C', 'SEC', 'WEEKDAY_ISO', 'YR', 'MONTHS', 'DECADE', 'HOUR', 'MICROSECS', 'MSECONDS', 'DD', 'MONS', 'WY', 'DAYOFMONTH', 'WEEKDAY', 'MINUTES', 'MICROSECONDS', 'MICROSEC', 'EPOCH_MICROSECOND', 'S', 'NS', 'USECOND', 'HH', 'DAY OF WEEK', 'YEARS', 'MSEC', 'DOW', 'US', 'NSECOND', 'WK', 'WEEKOFYEAR', 'MSECS', 'USECONDS', 'NSECONDS', 'MS', 'MON', 'YYY', 'WEEKISO', 'QTRS', 'DY', 'EPOCH_MILLISECOND', 'DOY', 'EPOCH_NANOSECOND', 'YYYY', 'EPOCH_NANOSECONDS', 'WEEKOFYEARISO', 'HRS', 'EPOCH', 'DAYOFWEEKISO', 'CENTURY', 'TIMEZONE_MINUTE', 'USEC', 'DECS', 'DAY', 'DEC', 'MIN', 'DAYS'}
Inherited Members
- sqlglot.tokens.Tokenizer
- Tokenizer
- SINGLE_TOKENS
- BIT_STRINGS
- BYTE_STRINGS
- HEX_STRINGS
- RAW_STRINGS
- HEREDOC_STRINGS
- UNICODE_STRINGS
- IDENTIFIERS
- QUOTES
- VAR_SINGLE_TOKENS
- ESCAPE_FOLLOW_CHARS
- IDENTIFIER_ESCAPES
- HEREDOC_TAG_IS_IDENTIFIER
- HEREDOC_STRING_ALTERNATIVE
- STRING_ESCAPES_ALLOWED_IN_RAW_STRINGS
- NESTED_COMMENTS
- HINT_START
- TOKENS_PRECEDING_HINT
- KEYWORDS
- COMMANDS
- COMMAND_PREFIX_TOKENS
- NUMERIC_LITERALS
- NUMBERS_CAN_HAVE_DECIMALS
- COMMENTS
- dialect
- tokenize
- sql
- size
- tokens