collate-data-diff 0.11.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. collate_data_diff-0.11.2.dist-info/LICENSE +18 -0
  2. collate_data_diff-0.11.2.dist-info/METADATA +77 -0
  3. collate_data_diff-0.11.2.dist-info/RECORD +54 -0
  4. collate_data_diff-0.11.2.dist-info/WHEEL +4 -0
  5. collate_data_diff-0.11.2.dist-info/entry_points.txt +3 -0
  6. data_diff/__init__.py +180 -0
  7. data_diff/__main__.py +618 -0
  8. data_diff/abcs/__init__.py +0 -0
  9. data_diff/abcs/compiler.py +13 -0
  10. data_diff/abcs/database_types.py +308 -0
  11. data_diff/cloud/__init__.py +2 -0
  12. data_diff/cloud/data_source.py +318 -0
  13. data_diff/cloud/datafold_api.py +304 -0
  14. data_diff/config.py +127 -0
  15. data_diff/databases/__init__.py +17 -0
  16. data_diff/databases/_connect.py +306 -0
  17. data_diff/databases/base.py +1291 -0
  18. data_diff/databases/bigquery.py +315 -0
  19. data_diff/databases/clickhouse.py +203 -0
  20. data_diff/databases/databricks.py +248 -0
  21. data_diff/databases/duckdb.py +192 -0
  22. data_diff/databases/mssql.py +229 -0
  23. data_diff/databases/mysql.py +159 -0
  24. data_diff/databases/oracle.py +195 -0
  25. data_diff/databases/postgresql.py +258 -0
  26. data_diff/databases/presto.py +197 -0
  27. data_diff/databases/redshift.py +217 -0
  28. data_diff/databases/snowflake.py +207 -0
  29. data_diff/databases/trino.py +50 -0
  30. data_diff/databases/vertica.py +160 -0
  31. data_diff/dbt.py +604 -0
  32. data_diff/dbt_config_validators.py +65 -0
  33. data_diff/dbt_parser.py +523 -0
  34. data_diff/diff_tables.py +416 -0
  35. data_diff/errors.py +74 -0
  36. data_diff/format.py +359 -0
  37. data_diff/hashdiff_tables.py +264 -0
  38. data_diff/info_tree.py +62 -0
  39. data_diff/joindiff_tables.py +399 -0
  40. data_diff/lexicographic_space.py +240 -0
  41. data_diff/parse_time.py +74 -0
  42. data_diff/py.typed +0 -0
  43. data_diff/queries/__init__.py +0 -0
  44. data_diff/queries/api.py +200 -0
  45. data_diff/queries/ast_classes.py +798 -0
  46. data_diff/queries/base.py +24 -0
  47. data_diff/queries/extras.py +29 -0
  48. data_diff/query_utils.py +56 -0
  49. data_diff/schema.py +52 -0
  50. data_diff/table_segment.py +286 -0
  51. data_diff/thread_utils.py +98 -0
  52. data_diff/tracking.py +237 -0
  53. data_diff/utils.py +625 -0
  54. data_diff/version.py +1 -0
@@ -0,0 +1,798 @@
1
+ from datetime import datetime
2
+ from typing import Any, Generator, List, Optional, Sequence, Union, Dict
3
+
4
+ import attrs
5
+ from typing_extensions import Self
6
+
7
+ from data_diff.utils import ArithString
8
+ from data_diff.abcs.compiler import Compilable
9
+ from data_diff.schema import Schema
10
+
11
+ from data_diff.queries.base import SKIP, args_as_tuple, SqeletonError
12
+ from data_diff.abcs.database_types import DbPath
13
+
14
+
15
+ class QueryBuilderError(SqeletonError):
16
+ pass
17
+
18
+
19
+ class QB_TypeError(QueryBuilderError):
20
+ pass
21
+
22
+
23
+ @attrs.define(frozen=True)
24
+ class Root:
25
+ "Nodes inheriting from Root can be used as root statements in SQL (e.g. SELECT yes, RANDOM() no)"
26
+
27
+
28
+ @attrs.define(frozen=False, eq=False)
29
+ class ExprNode(Compilable):
30
+ "Base class for query expression nodes"
31
+
32
+ @property
33
+ def type(self) -> Optional[type]:
34
+ return None
35
+
36
+ def _dfs_values(self):
37
+ yield self
38
+ for k, vs in attrs.asdict(self, recurse=False).items():
39
+ if k == "source_table":
40
+ # Skip data-sources, we're only interested in data-parameters
41
+ continue
42
+ if not isinstance(vs, (list, tuple)):
43
+ vs = [vs]
44
+ for v in vs:
45
+ if isinstance(v, ExprNode):
46
+ yield from v._dfs_values()
47
+
48
+ def cast_to(self, to) -> "Cast":
49
+ return Cast(self, to)
50
+
51
+
52
+ # Query expressions can only interact with objects that are an instance of 'Expr'
53
+ Expr = Union[ExprNode, str, bool, int, float, datetime, ArithString, None]
54
+
55
+
56
+ @attrs.define(frozen=True, eq=False)
57
+ class Code(ExprNode, Root):
58
+ code: str
59
+ args: Optional[Dict[str, Expr]] = None
60
+
61
+
62
+ def _expr_type(e: Expr) -> type:
63
+ if isinstance(e, ExprNode):
64
+ return e.type
65
+ return type(e)
66
+
67
+
68
+ @attrs.define(frozen=True, eq=False)
69
+ class Alias(ExprNode):
70
+ expr: Expr
71
+ name: str
72
+
73
+ @property
74
+ def type(self):
75
+ return _expr_type(self.expr)
76
+
77
+
78
+ def _drop_skips(exprs):
79
+ return [e for e in exprs if e is not SKIP]
80
+
81
+
82
+ def _drop_skips_dict(exprs_dict):
83
+ return {k: v for k, v in exprs_dict.items() if v is not SKIP}
84
+
85
+
86
+ @attrs.define(frozen=True)
87
+ class ITable:
88
+ @property
89
+ def source_table(self) -> "ITable": # not always Self, it can be a substitute
90
+ return self
91
+
92
+ @property
93
+ def schema(self) -> Optional[Schema]:
94
+ return None
95
+
96
+ def select(self, *exprs, distinct=SKIP, optimizer_hints=SKIP, **named_exprs) -> "ITable":
97
+ """Choose new columns, based on the old ones. (aka Projection)
98
+
99
+ Parameters:
100
+ exprs: List of expressions to constitute the columns of the new table.
101
+ If not provided, returns all columns in source table (i.e. ``select *``)
102
+ distinct: 'select' or 'select distinct'
103
+ named_exprs: More expressions to constitute the columns of the new table, aliased to keyword name.
104
+
105
+ """
106
+ exprs = args_as_tuple(exprs)
107
+ exprs = _drop_skips(exprs)
108
+ named_exprs = _drop_skips_dict(named_exprs)
109
+ exprs += _named_exprs_as_aliases(named_exprs)
110
+ resolve_names(self.source_table, exprs)
111
+ return Select.make(self, columns=exprs, distinct=distinct, optimizer_hints=optimizer_hints)
112
+
113
+ def where(self, *exprs) -> "Select":
114
+ """Filter the rows, based on the given predicates. (aka Selection)"""
115
+ exprs = args_as_tuple(exprs)
116
+ exprs = _drop_skips(exprs)
117
+ if not exprs:
118
+ return self
119
+
120
+ resolve_names(self.source_table, exprs)
121
+ return Select.make(self, where_exprs=exprs)
122
+
123
+ def order_by(self, *exprs) -> "Select":
124
+ """Order the rows lexicographically, according to the given expressions."""
125
+ exprs = _drop_skips(exprs)
126
+ if not exprs:
127
+ return self
128
+
129
+ resolve_names(self.source_table, exprs)
130
+ return Select.make(self, order_by_exprs=exprs)
131
+
132
+ def limit(self, limit: int) -> "Select":
133
+ """Stop yielding rows after the given limit. i.e. take the first 'n=limit' rows"""
134
+ if limit is SKIP:
135
+ return self
136
+
137
+ return Select.make(self, limit_expr=limit)
138
+
139
+ def join(self, target: "ITable") -> "Join":
140
+ """Join the current table with the target table, returning a new table containing both side-by-side.
141
+
142
+ When joining, it's recommended to use explicit tables names, instead of `this`, in order to avoid potential name collisions.
143
+
144
+ Example:
145
+ ::
146
+
147
+ person = table('person')
148
+ city = table('city')
149
+
150
+ name_and_city = (
151
+ person
152
+ .join(city)
153
+ .on(person['city_id'] == city['id'])
154
+ .select(person['id'], city['name'])
155
+ )
156
+ """
157
+ return Join([self, target])
158
+
159
+ def group_by(self, *keys) -> "GroupBy":
160
+ """Behaves like in SQL, except for a small change in syntax:
161
+
162
+ A call to `.agg()` must follow every call to `.group_by()`.
163
+
164
+ Example:
165
+ ::
166
+
167
+ # SELECT a, sum(b) FROM tmp GROUP BY 1
168
+ table('tmp').group_by(this.a).agg(this.b.sum())
169
+
170
+ # SELECT a, sum(b) FROM a GROUP BY 1 HAVING (b > 10)
171
+ (table('tmp')
172
+ .group_by(this.a)
173
+ .agg(this.b.sum())
174
+ .having(this.b > 10)
175
+ )
176
+
177
+ """
178
+ keys = _drop_skips(keys)
179
+ resolve_names(self.source_table, keys)
180
+
181
+ return GroupBy(self, keys)
182
+
183
+ def _get_column(self, name: str) -> "Column":
184
+ if self.schema:
185
+ name = self.schema.get_key(name) # Get the actual name. Might be case-insensitive.
186
+ return Column(self, name)
187
+
188
+ # def __getattr__(self, column):
189
+ # return self._get_column(column)
190
+
191
+ def __getitem__(self, column) -> "Column":
192
+ if not isinstance(column, str):
193
+ raise TypeError()
194
+ return self._get_column(column)
195
+
196
+ def count(self) -> "Select":
197
+ """SELECT count() FROM self"""
198
+ return Select(self, [Count()])
199
+
200
+ def union(self, other: "ITable") -> "TableOp":
201
+ """SELECT * FROM self UNION other"""
202
+ return TableOp("UNION", self, other)
203
+
204
+ def union_all(self, other: "ITable") -> "TableOp":
205
+ """SELECT * FROM self UNION ALL other"""
206
+ return TableOp("UNION ALL", self, other)
207
+
208
+ def minus(self, other: "ITable") -> "TableOp":
209
+ """SELECT * FROM self EXCEPT other"""
210
+ # aka
211
+ return TableOp("EXCEPT", self, other)
212
+
213
+ def intersect(self, other: "ITable") -> "TableOp":
214
+ """SELECT * FROM self INTERSECT other"""
215
+ return TableOp("INTERSECT", self, other)
216
+
217
+
218
+ @attrs.define(frozen=True, eq=False)
219
+ class Concat(ExprNode):
220
+ exprs: list
221
+ sep: Optional[str] = None
222
+
223
+
224
+ @attrs.define(frozen=True, eq=False)
225
+ class Count(ExprNode):
226
+ expr: Expr = None
227
+ distinct: bool = False
228
+
229
+ @property
230
+ def type(self) -> Optional[type]:
231
+ return int
232
+
233
+
234
+ @attrs.define(frozen=False, eq=False)
235
+ class LazyOps:
236
+ def __add__(self, other) -> "BinOp":
237
+ return BinOp("+", [self, other])
238
+
239
+ def __sub__(self, other) -> "BinOp":
240
+ return BinOp("-", [self, other])
241
+
242
+ def __neg__(self) -> "UnaryOp":
243
+ return UnaryOp("-", self)
244
+
245
+ def __gt__(self, other) -> "BinBoolOp":
246
+ return BinBoolOp(">", [self, other])
247
+
248
+ def __ge__(self, other) -> "BinBoolOp":
249
+ return BinBoolOp(">=", [self, other])
250
+
251
+ def __eq__(self, other) -> "BinBoolOp":
252
+ if other is None:
253
+ return BinBoolOp("IS", [self, None])
254
+ return BinBoolOp("=", [self, other])
255
+
256
+ def __lt__(self, other) -> "BinBoolOp":
257
+ return BinBoolOp("<", [self, other])
258
+
259
+ def __le__(self, other) -> "BinBoolOp":
260
+ return BinBoolOp("<=", [self, other])
261
+
262
+ def __or__(self, other) -> "BinBoolOp":
263
+ return BinBoolOp("OR", [self, other])
264
+
265
+ def __and__(self, other) -> "BinBoolOp":
266
+ return BinBoolOp("AND", [self, other])
267
+
268
+ def is_distinct_from(self, other) -> "IsDistinctFrom":
269
+ return IsDistinctFrom(self, other)
270
+
271
+ def like(self, other) -> "BinBoolOp":
272
+ return BinBoolOp("LIKE", [self, other])
273
+
274
+ def sum(self) -> "Func":
275
+ return Func("SUM", [self])
276
+
277
+ def max(self) -> "Func":
278
+ return Func("MAX", [self])
279
+
280
+ def min(self) -> "Func":
281
+ return Func("MIN", [self])
282
+
283
+
284
+ @attrs.define(frozen=True, eq=False)
285
+ class Func(LazyOps, ExprNode):
286
+ name: str
287
+ args: Sequence[Expr]
288
+
289
+
290
+ @attrs.define(frozen=True, eq=False)
291
+ class WhenThen(ExprNode):
292
+ when: Expr
293
+ then: Expr
294
+
295
+
296
+ @attrs.define(frozen=True, eq=False)
297
+ class CaseWhen(ExprNode):
298
+ cases: Sequence[WhenThen]
299
+ else_expr: Optional[Expr] = None
300
+
301
+ @property
302
+ def type(self):
303
+ then_types = {_expr_type(case.then) for case in self.cases}
304
+ if self.else_expr:
305
+ then_types |= {_expr_type(self.else_expr)}
306
+ if len(then_types) > 1:
307
+ raise QB_TypeError(f"Non-matching types in when: {then_types}")
308
+ (t,) = then_types
309
+ return t
310
+
311
+ def when(self, *whens: Expr) -> "QB_When":
312
+ """Add a new 'when' clause to the case expression
313
+
314
+ Must be followed by a call to `.then()`
315
+ """
316
+ whens = args_as_tuple(whens)
317
+ whens = _drop_skips(whens)
318
+ if not whens:
319
+ raise QueryBuilderError("Expected valid whens")
320
+
321
+ # XXX reimplementing api.and_()
322
+ if len(whens) == 1:
323
+ return QB_When(self, whens[0])
324
+ return QB_When(self, BinBoolOp("AND", whens))
325
+
326
+ def else_(self, then: Expr) -> Self:
327
+ """Add an 'else' clause to the case expression.
328
+
329
+ Can only be called once!
330
+ """
331
+ if self.else_expr is not None:
332
+ raise QueryBuilderError(f"Else clause already specified in {self}")
333
+
334
+ return attrs.evolve(self, else_expr=then)
335
+
336
+
337
+ @attrs.define(frozen=True, eq=False)
338
+ class QB_When:
339
+ "Partial case-when, used for query-building"
340
+
341
+ casewhen: CaseWhen
342
+ when: Expr
343
+
344
+ def then(self, then: Expr) -> CaseWhen:
345
+ """Add a 'then' clause after a 'when' was added."""
346
+ case = WhenThen(self.when, then)
347
+ return attrs.evolve(self.casewhen, cases=self.casewhen.cases + [case])
348
+
349
+
350
+ @attrs.define(frozen=True, eq=False)
351
+ class IsDistinctFrom(LazyOps, ExprNode):
352
+ a: Expr
353
+ b: Expr
354
+
355
+ @property
356
+ def type(self) -> Optional[type]:
357
+ return bool
358
+
359
+
360
+ @attrs.define(frozen=True, eq=False)
361
+ class BinOp(LazyOps, ExprNode):
362
+ op: str
363
+ args: Sequence[Expr]
364
+
365
+ @property
366
+ def type(self):
367
+ types = {_expr_type(i) for i in self.args}
368
+ if len(types) > 1:
369
+ raise TypeError(f"Expected all args to have the same type, got {types}")
370
+ (t,) = types
371
+ return t
372
+
373
+
374
+ @attrs.define(frozen=True, eq=False)
375
+ class UnaryOp(LazyOps, ExprNode):
376
+ op: str
377
+ expr: Expr
378
+
379
+
380
+ @attrs.define(frozen=True)
381
+ class BinBoolOp(BinOp):
382
+ @property
383
+ def type(self) -> Optional[type]:
384
+ return bool
385
+
386
+
387
+ @attrs.define(frozen=True, eq=False)
388
+ class Column(LazyOps, ExprNode):
389
+ source_table: ITable
390
+ name: str
391
+
392
+ @property
393
+ def type(self):
394
+ if self.source_table.schema is None:
395
+ raise QueryBuilderError(f"Schema required for table {self.source_table}")
396
+ return self.source_table.schema[self.name]
397
+
398
+
399
+ @attrs.define(frozen=False, eq=False)
400
+ class TablePath(ExprNode, ITable):
401
+ path: DbPath
402
+ schema: Optional[Schema] = None # overrides the inherited property
403
+
404
+ # Statement shorthands
405
+ def create(self, source_table: ITable = None, *, if_not_exists: bool = False, primary_keys: List[str] = None):
406
+ """Returns a query expression to create a new table.
407
+
408
+ Parameters:
409
+ source_table: a table expression to use for initializing the table.
410
+ If not provided, the table must have a schema specified.
411
+ if_not_exists: Add a 'if not exists' clause or not. (note: not all dbs support it!)
412
+ primary_keys: List of column names which define the primary key
413
+ """
414
+
415
+ if source_table is None and not self.schema:
416
+ raise ValueError("Either schema or source table needed to create table")
417
+ if isinstance(source_table, TablePath):
418
+ source_table = source_table.select()
419
+ return CreateTable(self, source_table, if_not_exists=if_not_exists, primary_keys=primary_keys)
420
+
421
+ def drop(self, if_exists=False):
422
+ """Returns a query expression to delete the table.
423
+
424
+ Parameters:
425
+ if_not_exists: Add a 'if not exists' clause or not. (note: not all dbs support it!)
426
+ """
427
+ return DropTable(self, if_exists=if_exists)
428
+
429
+ def truncate(self):
430
+ """Returns a query expression to truncate the table. (remove all rows)"""
431
+ return TruncateTable(self)
432
+
433
+ def insert_rows(self, rows: Sequence, *, columns: List[str] = None):
434
+ """Returns a query expression to insert rows to the table, given as Python values.
435
+
436
+ Parameters:
437
+ rows: A list of tuples. Must all have the same width.
438
+ columns: Names of columns being populated. If specified, must have the same length as the tuples.
439
+ """
440
+ rows = list(rows)
441
+ return InsertToTable(self, ConstantTable(rows), columns=columns)
442
+
443
+ def insert_row(self, *values, columns: List[str] = None):
444
+ """Returns a query expression to insert a single row to the table, given as Python values.
445
+
446
+ Parameters:
447
+ columns: Names of columns being populated. If specified, must have the same length as 'values'
448
+ """
449
+ return InsertToTable(self, ConstantTable([values]), columns=columns)
450
+
451
+ def insert_expr(self, expr: Expr):
452
+ """Returns a query expression to insert rows to the table, given as a query expression.
453
+
454
+ Parameters:
455
+ expr: query expression to from which to read the rows
456
+ """
457
+ if isinstance(expr, TablePath):
458
+ expr = expr.select()
459
+ return InsertToTable(self, expr)
460
+
461
+
462
+ @attrs.define(frozen=True, eq=False)
463
+ class TableAlias(ExprNode, ITable):
464
+ table: ITable
465
+ name: str
466
+
467
+ @property
468
+ def source_table(self) -> ITable:
469
+ return self.table
470
+
471
+ @property
472
+ def schema(self) -> Schema:
473
+ return self.table.schema
474
+
475
+
476
+ @attrs.define(frozen=True, eq=False)
477
+ class Join(ExprNode, ITable, Root):
478
+ source_tables: Sequence[ITable]
479
+ op: Optional[str] = None
480
+ on_exprs: Optional[Sequence[Expr]] = None
481
+ columns: Optional[Sequence[Expr]] = None
482
+
483
+ @property
484
+ def schema(self) -> Schema:
485
+ assert self.columns # TODO Implement SELECT *
486
+ s = self.source_tables[0].schema # TODO validate types match between both tables
487
+ return type(s)({c.name: c.type for c in self.columns})
488
+
489
+ def on(self, *exprs) -> Self:
490
+ """Add an ON clause, for filtering the result of the cartesian product (i.e. the JOIN)"""
491
+ if len(exprs) == 1:
492
+ (e,) = exprs
493
+ if isinstance(e, Generator):
494
+ exprs = tuple(e)
495
+
496
+ exprs = _drop_skips(exprs)
497
+ if not exprs:
498
+ return self
499
+
500
+ return attrs.evolve(self, on_exprs=(self.on_exprs or []) + exprs)
501
+
502
+ def select(self, *exprs, **named_exprs) -> Union[Self, ITable]:
503
+ """Select fields to return from the JOIN operation
504
+
505
+ See Also: ``ITable.select()``
506
+ """
507
+ if self.columns is not None:
508
+ # join-select already applied
509
+ return super().select(*exprs, **named_exprs)
510
+
511
+ exprs = _drop_skips(exprs)
512
+ named_exprs = _drop_skips_dict(named_exprs)
513
+ exprs += _named_exprs_as_aliases(named_exprs)
514
+ resolve_names(self.source_table, exprs)
515
+ # TODO Ensure exprs <= self.columns ?
516
+ return attrs.evolve(self, columns=exprs)
517
+
518
+
519
+ @attrs.define(frozen=True, eq=False)
520
+ class GroupBy(ExprNode, ITable, Root):
521
+ table: ITable
522
+ keys: Optional[Sequence[Expr]] = None # IKey?
523
+ values: Optional[Sequence[Expr]] = None
524
+ having_exprs: Optional[Sequence[Expr]] = None
525
+
526
+ def __attrs_post_init__(self) -> None:
527
+ assert self.keys or self.values
528
+
529
+ def having(self, *exprs) -> Self:
530
+ """Add a 'HAVING' clause to the group-by"""
531
+ exprs = args_as_tuple(exprs)
532
+ exprs = _drop_skips(exprs)
533
+ if not exprs:
534
+ return self
535
+
536
+ resolve_names(self.table, exprs)
537
+ return attrs.evolve(self, having_exprs=(self.having_exprs or []) + exprs)
538
+
539
+ def agg(self, *exprs) -> Self:
540
+ """Select aggregated fields for the group-by."""
541
+ exprs = args_as_tuple(exprs)
542
+ exprs = _drop_skips(exprs)
543
+ resolve_names(self.table, exprs)
544
+ return attrs.evolve(self, values=(self.values or []) + exprs)
545
+
546
+
547
+ @attrs.define(frozen=True, eq=False)
548
+ class TableOp(ExprNode, ITable, Root):
549
+ op: str
550
+ table1: ITable
551
+ table2: ITable
552
+
553
+ @property
554
+ def type(self):
555
+ # TODO ensure types of both tables are compatible
556
+ return self.table1.type
557
+
558
+ @property
559
+ def schema(self) -> Schema:
560
+ s1 = self.table1.schema
561
+ s2 = self.table2.schema
562
+ assert len(s1) == len(s2)
563
+ return s1
564
+
565
+
566
+ @attrs.define(frozen=True, eq=False)
567
+ class Select(ExprNode, ITable, Root):
568
+ table: Optional[Expr] = None
569
+ columns: Optional[Sequence[Expr]] = None
570
+ where_exprs: Optional[Sequence[Expr]] = None
571
+ order_by_exprs: Optional[Sequence[Expr]] = None
572
+ group_by_exprs: Optional[Sequence[Expr]] = None
573
+ having_exprs: Optional[Sequence[Expr]] = None
574
+ limit_expr: Optional[int] = None
575
+ distinct: bool = False
576
+ optimizer_hints: Optional[Sequence[Expr]] = None
577
+
578
+ @property
579
+ def schema(self) -> Schema:
580
+ s = self.table.schema
581
+ if s is None or self.columns is None:
582
+ return s
583
+ return type(s)({c.name: c.type for c in self.columns})
584
+
585
+ @classmethod
586
+ def make(cls, table: ITable, distinct: bool = SKIP, optimizer_hints: str = SKIP, **kwargs):
587
+ assert "table" not in kwargs
588
+
589
+ if not isinstance(table, cls): # If not Select
590
+ if distinct is not SKIP:
591
+ kwargs["distinct"] = distinct
592
+ if optimizer_hints is not SKIP:
593
+ kwargs["optimizer_hints"] = optimizer_hints
594
+ return cls(table, **kwargs)
595
+
596
+ # We can safely assume isinstance(table, Select)
597
+ if optimizer_hints is not SKIP:
598
+ kwargs["optimizer_hints"] = optimizer_hints
599
+
600
+ if distinct is not SKIP:
601
+ if distinct == False and table.distinct:
602
+ return cls(table, **kwargs)
603
+ kwargs["distinct"] = distinct
604
+
605
+ if table.limit_expr or table.group_by_exprs:
606
+ return cls(table, **kwargs)
607
+
608
+ # Fill in missing attributes, instead of nesting instances
609
+ for k, v in kwargs.items():
610
+ if getattr(table, k) is not None:
611
+ if k == "where_exprs": # Additive attribute
612
+ kwargs[k] = getattr(table, k) + v
613
+ elif k in ["distinct", "optimizer_hints"]:
614
+ pass
615
+ else:
616
+ raise ValueError(k)
617
+
618
+ return attrs.evolve(table, **kwargs)
619
+
620
+
621
+ @attrs.define(frozen=True, eq=False)
622
+ class Cte(ExprNode, ITable):
623
+ table: Expr
624
+ name: Optional[str] = None
625
+ params: Optional[Sequence[str]] = None
626
+
627
+ @property
628
+ def source_table(self) -> "ITable":
629
+ return self.table
630
+
631
+ @property
632
+ def schema(self) -> Schema:
633
+ # TODO add cte to schema
634
+ return self.table.schema
635
+
636
+
637
+ def _named_exprs_as_aliases(named_exprs):
638
+ return [Alias(expr, name) for name, expr in named_exprs.items()]
639
+
640
+
641
+ def resolve_names(source_table, exprs):
642
+ i = 0
643
+ for expr in exprs:
644
+ # Iterate recursively and update _ResolveColumn instances with the right expression
645
+ if isinstance(expr, ExprNode):
646
+ for v in expr._dfs_values():
647
+ if isinstance(v, _ResolveColumn):
648
+ v.resolve(source_table._get_column(v.resolve_name))
649
+ i += 1
650
+
651
+
652
+ @attrs.define(frozen=False, eq=False)
653
+ class _ResolveColumn(LazyOps, ExprNode):
654
+ resolve_name: str
655
+ resolved: Optional[Expr] = None
656
+
657
+ def resolve(self, expr: Expr):
658
+ if self.resolved is not None:
659
+ raise QueryBuilderError("Already resolved!")
660
+ self.resolved = expr
661
+
662
+ def _get_resolved(self) -> Expr:
663
+ if self.resolved is None:
664
+ raise QueryBuilderError(f"Column not resolved: {self.resolve_name}")
665
+ return self.resolved
666
+
667
+ @property
668
+ def type(self):
669
+ return self._get_resolved().type
670
+
671
+ @property
672
+ def name(self):
673
+ return self._get_resolved().name
674
+
675
+
676
+ @attrs.define(frozen=True)
677
+ class This:
678
+ """Builder object for accessing table attributes.
679
+
680
+ Automatically evaluates to the the 'top-most' table during compilation.
681
+ """
682
+
683
+ def __getattr__(self, name):
684
+ return _ResolveColumn(name)
685
+
686
+ def __getitem__(self, name):
687
+ if isinstance(name, (list, tuple)):
688
+ return [_ResolveColumn(n) for n in name]
689
+ return _ResolveColumn(name)
690
+
691
+
692
+ @attrs.define(frozen=True, eq=False)
693
+ class In(ExprNode):
694
+ expr: Expr
695
+ list: Sequence[Expr]
696
+
697
+ @property
698
+ def type(self) -> Optional[type]:
699
+ return bool
700
+
701
+
702
+ @attrs.define(frozen=True, eq=False)
703
+ class Cast(ExprNode):
704
+ expr: Expr
705
+ target_type: Expr
706
+
707
+
708
+ @attrs.define(frozen=True, eq=False)
709
+ class Random(LazyOps, ExprNode):
710
+ @property
711
+ def type(self) -> Optional[type]:
712
+ return float
713
+
714
+
715
+ @attrs.define(frozen=True, eq=False)
716
+ class ConstantTable(ExprNode):
717
+ rows: Sequence[Sequence]
718
+
719
+
720
+ @attrs.define(frozen=True, eq=False)
721
+ class Explain(ExprNode, Root):
722
+ select: Select
723
+
724
+ @property
725
+ def type(self) -> Optional[type]:
726
+ return str
727
+
728
+
729
+ @attrs.define(frozen=True)
730
+ class CurrentTimestamp(ExprNode):
731
+ @property
732
+ def type(self) -> Optional[type]:
733
+ return datetime
734
+
735
+
736
+ # DDL
737
+
738
+
739
+ @attrs.define(frozen=True)
740
+ class Statement(Compilable, Root):
741
+ @property
742
+ def type(self) -> Optional[type]:
743
+ return None
744
+
745
+
746
+ @attrs.define(frozen=True, eq=False)
747
+ class CreateTable(Statement):
748
+ path: TablePath
749
+ source_table: Optional[Expr] = None
750
+ if_not_exists: bool = False
751
+ primary_keys: Optional[List[str]] = None
752
+
753
+
754
+ @attrs.define(frozen=True, eq=False)
755
+ class DropTable(Statement):
756
+ path: TablePath
757
+ if_exists: bool = False
758
+
759
+
760
+ @attrs.define(frozen=True, eq=False)
761
+ class TruncateTable(Statement):
762
+ path: TablePath
763
+
764
+
765
+ @attrs.define(frozen=True, eq=False)
766
+ class InsertToTable(Statement):
767
+ path: TablePath
768
+ expr: Expr
769
+ columns: Optional[List[str]] = None
770
+ returning_exprs: Optional[List[str]] = None
771
+
772
+ def returning(self, *exprs) -> Self:
773
+ """Add a 'RETURNING' clause to the insert expression.
774
+
775
+ Note: Not all databases support this feature!
776
+ """
777
+ if self.returning_exprs:
778
+ raise ValueError("A returning clause is already specified")
779
+
780
+ exprs = args_as_tuple(exprs)
781
+ exprs = _drop_skips(exprs)
782
+ if not exprs:
783
+ return self
784
+
785
+ resolve_names(self.path, exprs)
786
+ return attrs.evolve(self, returning_exprs=exprs)
787
+
788
+
789
+ @attrs.define(frozen=True, eq=False)
790
+ class Commit(Statement):
791
+ """Generate a COMMIT statement, if we're in the middle of a transaction, or in auto-commit. Otherwise SKIP."""
792
+
793
+
794
+ @attrs.define(frozen=True, eq=False)
795
+ class Param(ExprNode, ITable): # TODO: Unused?
796
+ """A value placeholder, to be specified at compilation time using the `cv_params` context variable."""
797
+
798
+ name: str