Edit on GitHub

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})
PythonExecutor(env=None, tables=None)
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 {}
generator
env
tables
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 context(self, tables):
79    def context(self, tables):
80        return Context(tables, env=self.env)
def table(self, expressions):
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        )
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 static(self):
124    def static(self):
125        return self.context({}), [RowReader(())]
def scan_table(self, step):
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)
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
SUPPORTS_COLUMN_JOIN_MARKS = False

Whether the old-style outer join (+) syntax is supported.

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.

INITCAP_SUPPORTS_CUSTOM_DELIMITERS = False
tokenizer_class = <class 'Python.Tokenizer'>
jsonpath_tokenizer_class = <class 'sqlglot.dialects.dialect.JSONPathTokenizer'>
parser_class = <class 'sqlglot.parsers.base.BaseParser'>
generator_class = <class 'sqlglot.generators.python.PythonGenerator'>
TIME_TRIE: dict = {}
FORMAT_TRIE: dict = {}
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}}}}}}}}}
INVERSE_CREATABLE_KIND_MAPPING: dict[str, str] = {}
ESCAPED_SEQUENCES: dict[str, str] = {'\x07': '\\a', '\x08': '\\b', '\x0c': '\\f', '\n': '\\n', '\r': '\\r', '\t': '\\t', '\x0b': '\\v', '\\': '\\\\'}
QUOTE_START = "'"
QUOTE_END = "'"
IDENTIFIER_START = '"'
IDENTIFIER_END = '"'
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'}
BIT_START: str | None = None
BIT_END: str | None = None
HEX_START: str | None = None
HEX_END: str | None = None
BYTE_START: str | None = None
BYTE_END: str | None = None
UNICODE_START: str | None = None
UNICODE_END: str | None = None
class Python.Tokenizer(sqlglot.tokens.Tokenizer):
407    class Tokenizer(tokens.Tokenizer):
408        STRING_ESCAPES = ["\\"]
STRING_ESCAPES = ['\\']
BYTE_STRING_ESCAPES: ClassVar[list[str]] = ['\\']