sqlleaf 0.0.1__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.
- sqlleaf/__init__.py +4 -0
- sqlleaf/dialects/__init__.py +0 -0
- sqlleaf/dialects/plpgsql/__init__.py +0 -0
- sqlleaf/dialects/plpgsql/p_classes.py +275 -0
- sqlleaf/dialects/plpgsql/p_dialect.py +148 -0
- sqlleaf/dialects/plpgsql/p_generator.py +538 -0
- sqlleaf/dialects/plpgsql/p_parser.py +1244 -0
- sqlleaf/dialects/plpgsql/p_tokenizer.py +41 -0
- sqlleaf/exception.py +67 -0
- sqlleaf/lineage.py +326 -0
- sqlleaf/mappings.py +376 -0
- sqlleaf/models/__init__.py +0 -0
- sqlleaf/models/context.py +153 -0
- sqlleaf/models/hook.py +9 -0
- sqlleaf/models/node/__init__.py +27 -0
- sqlleaf/models/node/base.py +148 -0
- sqlleaf/models/node/column.py +345 -0
- sqlleaf/models/node/dynamodb.py +39 -0
- sqlleaf/models/node/function.py +33 -0
- sqlleaf/models/node/interval.py +21 -0
- sqlleaf/models/node/json.py +57 -0
- sqlleaf/models/node/literal.py +17 -0
- sqlleaf/models/node/null.py +14 -0
- sqlleaf/models/node/pivot.py +34 -0
- sqlleaf/models/node/program.py +27 -0
- sqlleaf/models/node/sequence.py +18 -0
- sqlleaf/models/node/star.py +11 -0
- sqlleaf/models/node/stream.py +14 -0
- sqlleaf/models/node/user_defined_function.py +33 -0
- sqlleaf/models/node/var.py +7 -0
- sqlleaf/models/node/variable.py +8 -0
- sqlleaf/models/node/window.py +22 -0
- sqlleaf/models/query/__init__.py +33 -0
- sqlleaf/models/query/base.py +248 -0
- sqlleaf/models/query/call.py +111 -0
- sqlleaf/models/query/copy.py +144 -0
- sqlleaf/models/query/ctas.py +61 -0
- sqlleaf/models/query/database.py +48 -0
- sqlleaf/models/query/delete.py +26 -0
- sqlleaf/models/query/execute.py +66 -0
- sqlleaf/models/query/holder.py +49 -0
- sqlleaf/models/query/insert.py +69 -0
- sqlleaf/models/query/merge.py +29 -0
- sqlleaf/models/query/multitable_insert.py +42 -0
- sqlleaf/models/query/prepare.py +61 -0
- sqlleaf/models/query/procedure.py +72 -0
- sqlleaf/models/query/put.py +38 -0
- sqlleaf/models/query/replace.py +32 -0
- sqlleaf/models/query/schema.py +47 -0
- sqlleaf/models/query/select.py +27 -0
- sqlleaf/models/query/sequence.py +33 -0
- sqlleaf/models/query/set.py +74 -0
- sqlleaf/models/query/stage.py +78 -0
- sqlleaf/models/query/table.py +220 -0
- sqlleaf/models/query/trigger.py +42 -0
- sqlleaf/models/query/type.py +42 -0
- sqlleaf/models/query/unload.py +64 -0
- sqlleaf/models/query/update.py +45 -0
- sqlleaf/models/query/user_defined_function.py +297 -0
- sqlleaf/models/query/values.py +20 -0
- sqlleaf/models/query/view.py +41 -0
- sqlleaf/path.py +194 -0
- sqlleaf/processors/__init__.py +0 -0
- sqlleaf/processors/collector/__init__.py +0 -0
- sqlleaf/processors/collector/collector.py +827 -0
- sqlleaf/processors/collector/substitute.py +396 -0
- sqlleaf/processors/generator/__init__.py +0 -0
- sqlleaf/processors/generator/dialects/__init__.py +5 -0
- sqlleaf/processors/generator/dialects/athena.py +45 -0
- sqlleaf/processors/generator/dialects/base.py +572 -0
- sqlleaf/processors/generator/dialects/mysql.py +19 -0
- sqlleaf/processors/generator/dialects/postgres.py +182 -0
- sqlleaf/processors/generator/dialects/redshift.py +192 -0
- sqlleaf/processors/generator/dialects/snowflake.py +90 -0
- sqlleaf/processors/generator/generator.py +495 -0
- sqlleaf/processors/transformer/__init__.py +31 -0
- sqlleaf/processors/transformer/base.py +899 -0
- sqlleaf/processors/transformer/call.py +9 -0
- sqlleaf/processors/transformer/copy.py +63 -0
- sqlleaf/processors/transformer/ctas.py +39 -0
- sqlleaf/processors/transformer/delete.py +14 -0
- sqlleaf/processors/transformer/execute.py +9 -0
- sqlleaf/processors/transformer/expressions/__init__.py +11 -0
- sqlleaf/processors/transformer/expressions/functional_notation.py +22 -0
- sqlleaf/processors/transformer/expressions/row.py +451 -0
- sqlleaf/processors/transformer/expressions/values.py +318 -0
- sqlleaf/processors/transformer/insert.py +125 -0
- sqlleaf/processors/transformer/merge.py +14 -0
- sqlleaf/processors/transformer/multitable_insert.py +14 -0
- sqlleaf/processors/transformer/replace.py +23 -0
- sqlleaf/processors/transformer/transformer.py +141 -0
- sqlleaf/processors/transformer/udf.py +235 -0
- sqlleaf/processors/transformer/unload.py +30 -0
- sqlleaf/processors/transformer/update.py +174 -0
- sqlleaf/processors/transformer/values.py +10 -0
- sqlleaf/settings/__init__.py +19 -0
- sqlleaf/settings/athena.py +23 -0
- sqlleaf/settings/base.py +37 -0
- sqlleaf/settings/postgres.py +52 -0
- sqlleaf/system_tables/load.py +0 -0
- sqlleaf/typing.py +93 -0
- sqlleaf/util/__init__.py +34 -0
- sqlleaf/util/expression.py +282 -0
- sqlleaf/util/graph.py +116 -0
- sqlleaf/util/helpers.py +42 -0
- sqlleaf/util/iterators.py +37 -0
- sqlleaf-0.0.1.dist-info/METADATA +46 -0
- sqlleaf-0.0.1.dist-info/RECORD +111 -0
- sqlleaf-0.0.1.dist-info/WHEEL +5 -0
- sqlleaf-0.0.1.dist-info/licenses/LICENSE +21 -0
- sqlleaf-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from sqlglot import exp
|
|
6
|
+
|
|
7
|
+
from sqlleaf import mappings, util
|
|
8
|
+
from sqlleaf.models.query.base import Query
|
|
9
|
+
from sqlleaf.typing import SourceInfo, TargetInfo
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class PutQueryProperties:
|
|
14
|
+
file_format: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PutQuery(Query):
|
|
18
|
+
KIND = "put"
|
|
19
|
+
|
|
20
|
+
def __init__(self, expr: exp.Put, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int):
|
|
21
|
+
source = expr.this
|
|
22
|
+
target = expr.args["target"]
|
|
23
|
+
|
|
24
|
+
util.rename_if_stage(source, target)
|
|
25
|
+
|
|
26
|
+
source_type = self._determine_expression_type(source, dialect)
|
|
27
|
+
target_type = self._determine_expression_type(target, dialect)
|
|
28
|
+
|
|
29
|
+
super().__init__(
|
|
30
|
+
dialect=dialect,
|
|
31
|
+
statement=expr,
|
|
32
|
+
statement_index=statement_index,
|
|
33
|
+
object_mapping=object_mapping,
|
|
34
|
+
source_info=SourceInfo(expression=source, type=source_type),
|
|
35
|
+
target_info=TargetInfo(expression=target, type=target_type),
|
|
36
|
+
skip_type_annotation=True,
|
|
37
|
+
)
|
|
38
|
+
self.parameters = PutQueryProperties(file_format="TEXT")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sqlglot
|
|
4
|
+
from sqlglot import exp
|
|
5
|
+
|
|
6
|
+
from sqlleaf import mappings
|
|
7
|
+
from sqlleaf.models.query.insert import InsertQuery
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ReplaceQuery(InsertQuery):
|
|
11
|
+
KIND = "replace"
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
expr: exp.Command,
|
|
16
|
+
dialect: str,
|
|
17
|
+
object_mapping: mappings.ObjectMapping,
|
|
18
|
+
statement_index: int,
|
|
19
|
+
):
|
|
20
|
+
# Transform to Insert temporarily to initialize source/target info
|
|
21
|
+
expression = expr.args.get("expression")
|
|
22
|
+
new_sql = f"INSERT {expression.this}" if expression else "INSERT"
|
|
23
|
+
insert_expr = sqlglot.parse_one(new_sql, dialect=dialect)
|
|
24
|
+
|
|
25
|
+
super().__init__(
|
|
26
|
+
expr=insert_expr,
|
|
27
|
+
dialect=dialect,
|
|
28
|
+
object_mapping=object_mapping,
|
|
29
|
+
statement_index=statement_index,
|
|
30
|
+
)
|
|
31
|
+
# Restore the original statement
|
|
32
|
+
self.statement = expr
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from sqlglot import exp
|
|
6
|
+
|
|
7
|
+
from sqlleaf import mappings, util
|
|
8
|
+
from sqlleaf.models.query.base import Query
|
|
9
|
+
from sqlleaf.typing import SqlObjectType, TargetInfo
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class SchemaQueryProperties:
|
|
14
|
+
location: str | None
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def from_expression(cls, expr: exp.Create, dialect: str):
|
|
18
|
+
location = util.get_location_property(expr, dialect)
|
|
19
|
+
return cls(location=location)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SchemaQuery(Query):
|
|
23
|
+
KIND = "schema"
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
expr: exp.Create,
|
|
28
|
+
dialect: str,
|
|
29
|
+
object_mapping: mappings.ObjectMapping,
|
|
30
|
+
statement_index: int,
|
|
31
|
+
):
|
|
32
|
+
source = None
|
|
33
|
+
target = expr.this
|
|
34
|
+
|
|
35
|
+
super().__init__(
|
|
36
|
+
dialect=dialect,
|
|
37
|
+
statement=expr,
|
|
38
|
+
statement_index=statement_index,
|
|
39
|
+
object_mapping=object_mapping,
|
|
40
|
+
source_info=source,
|
|
41
|
+
target_info=TargetInfo(expression=target, type=SqlObjectType.SCHEMA),
|
|
42
|
+
)
|
|
43
|
+
self.properties = SchemaQueryProperties.from_expression(expr, dialect)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def location(self) -> str | None:
|
|
47
|
+
return self.properties.location
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from sqlglot import exp
|
|
4
|
+
|
|
5
|
+
from sqlleaf import mappings
|
|
6
|
+
from sqlleaf.models.query.base import Query
|
|
7
|
+
from sqlleaf.typing import SourceInfo, TargetInfo, SqlObjectType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SelectQuery(Query):
|
|
11
|
+
KIND = "select"
|
|
12
|
+
|
|
13
|
+
def __init__(self, expr: exp.Select, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int):
|
|
14
|
+
source = expr
|
|
15
|
+
source_type = self._determine_expression_type(source, dialect)
|
|
16
|
+
|
|
17
|
+
super().__init__(
|
|
18
|
+
dialect=dialect,
|
|
19
|
+
statement=expr,
|
|
20
|
+
statement_index=statement_index,
|
|
21
|
+
object_mapping=object_mapping,
|
|
22
|
+
source_info=SourceInfo(expression=source, type=source_type),
|
|
23
|
+
target_info=TargetInfo(expression=None, type=SqlObjectType.NONE)
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def get_ctes(self):
|
|
27
|
+
return getattr(self.statement, "ctes", [])
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from sqlglot import exp
|
|
4
|
+
|
|
5
|
+
from sqlleaf import mappings, util
|
|
6
|
+
from sqlleaf.models.query.base import Query
|
|
7
|
+
from sqlleaf.typing import TargetInfo
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SequenceQuery(Query):
|
|
11
|
+
KIND = "sequence"
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
expr: exp.Create,
|
|
16
|
+
dialect: str,
|
|
17
|
+
object_mapping: mappings.ObjectMapping,
|
|
18
|
+
statement_index: int,
|
|
19
|
+
):
|
|
20
|
+
source = None
|
|
21
|
+
target = expr.this
|
|
22
|
+
|
|
23
|
+
target_type = self._determine_expression_type(target, dialect)
|
|
24
|
+
|
|
25
|
+
super().__init__(
|
|
26
|
+
dialect=dialect,
|
|
27
|
+
statement=expr,
|
|
28
|
+
statement_index=statement_index,
|
|
29
|
+
object_mapping=object_mapping,
|
|
30
|
+
source_info=source,
|
|
31
|
+
target_info=TargetInfo(expression=target, type=target_type),
|
|
32
|
+
)
|
|
33
|
+
self.property = util.find_property(expr, self.target_info.expression, dialect)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from sqlglot import exp
|
|
4
|
+
|
|
5
|
+
from sqlleaf import mappings
|
|
6
|
+
from sqlleaf.models.query.base import Query
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SetQuery(Query):
|
|
10
|
+
"""
|
|
11
|
+
Represents a SQL SET statement, used to define session variables.
|
|
12
|
+
|
|
13
|
+
In Snowflake, this can take several forms:
|
|
14
|
+
- SET a = 10;
|
|
15
|
+
- SET (a, b) = (10, 20);
|
|
16
|
+
- SET (a, b) = (SELECT x, y FROM table);
|
|
17
|
+
|
|
18
|
+
This class extracts these variables and stores them in the ObjectMapping
|
|
19
|
+
so they can be substituted into subsequent queries in the same session.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
KIND = "set"
|
|
23
|
+
|
|
24
|
+
def __init__(self, expr: exp.Set, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int):
|
|
25
|
+
super().__init__(
|
|
26
|
+
dialect=dialect,
|
|
27
|
+
statement=expr,
|
|
28
|
+
statement_index=statement_index,
|
|
29
|
+
object_mapping=object_mapping,
|
|
30
|
+
source_info=None,
|
|
31
|
+
target_info=None,
|
|
32
|
+
skip_type_annotation=True,
|
|
33
|
+
)
|
|
34
|
+
self._collect()
|
|
35
|
+
|
|
36
|
+
def _collect(self):
|
|
37
|
+
"""
|
|
38
|
+
Extracts session variables from the SET statement.
|
|
39
|
+
"""
|
|
40
|
+
for expression in self.statement.expressions:
|
|
41
|
+
eq_expr = expression.this if isinstance(expression, exp.SetItem) else expression
|
|
42
|
+
if not isinstance(eq_expr, exp.EQ):
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
left, right = eq_expr.left, eq_expr.right
|
|
46
|
+
if isinstance(left, exp.Tuple):
|
|
47
|
+
# SET (a, b) = (10, 20) or SET (a, b) = (SELECT ...)
|
|
48
|
+
names = [col.name.upper() for col in left.expressions]
|
|
49
|
+
if isinstance(right, exp.Tuple):
|
|
50
|
+
for name, value in zip(names, right.expressions):
|
|
51
|
+
self.object_mapping.session_variables[name] = value
|
|
52
|
+
|
|
53
|
+
else: # Subquery - clone it once per variable, keeping only the i-th column
|
|
54
|
+
# Example: SET (a, b) = (SELECT col1, col2 FROM t)
|
|
55
|
+
# This will store:
|
|
56
|
+
# a -> (SELECT col1 FROM t)
|
|
57
|
+
# b -> (SELECT col2 FROM t)
|
|
58
|
+
if isinstance(right, exp.Subquery) and isinstance(right.this, exp.Select):
|
|
59
|
+
select_cols = right.this.expressions
|
|
60
|
+
|
|
61
|
+
for i, name in enumerate(names):
|
|
62
|
+
cloned = right.copy()
|
|
63
|
+
if i < len(select_cols):
|
|
64
|
+
cloned.this.set("expressions", [select_cols[i].copy()])
|
|
65
|
+
|
|
66
|
+
# Map the variable name to its corresponding single-column subquery
|
|
67
|
+
self.object_mapping.session_variables[name] = cloned
|
|
68
|
+
else:
|
|
69
|
+
for name in names:
|
|
70
|
+
self.object_mapping.session_variables[name] = right
|
|
71
|
+
else:
|
|
72
|
+
# SET a = <expr>
|
|
73
|
+
name = left.name.upper()
|
|
74
|
+
self.object_mapping.session_variables[name] = right
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from sqlglot import exp
|
|
7
|
+
|
|
8
|
+
from sqlleaf import mappings, util
|
|
9
|
+
from sqlleaf.models.query.base import Query
|
|
10
|
+
from sqlleaf.typing import SourceInfo, TargetInfo
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class StageQueryProperties:
|
|
15
|
+
path: str
|
|
16
|
+
is_temporary: bool
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def from_expression(
|
|
20
|
+
cls, expr: exp.Create, source_info: SourceInfo, target_info: TargetInfo, dialect: str
|
|
21
|
+
) -> StageQueryProperties:
|
|
22
|
+
is_temporary = expr.find(exp.TemporaryProperty) is not None
|
|
23
|
+
|
|
24
|
+
path = ""
|
|
25
|
+
# Get the URL
|
|
26
|
+
if props := expr.args.get("properties"):
|
|
27
|
+
for prop in props.expressions:
|
|
28
|
+
if isinstance(prop, exp.Property) and prop.name.upper() == "URL":
|
|
29
|
+
path = prop.args["value"].this
|
|
30
|
+
break
|
|
31
|
+
return cls(is_temporary=is_temporary, path=path)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class StageQuery(Query):
|
|
35
|
+
KIND = "stage"
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
expr: exp.Create,
|
|
40
|
+
dialect: str,
|
|
41
|
+
object_mapping: mappings.ObjectMapping,
|
|
42
|
+
statement_index: int,
|
|
43
|
+
):
|
|
44
|
+
source = None
|
|
45
|
+
target = util.get_table(expr)
|
|
46
|
+
|
|
47
|
+
target_type = self._determine_expression_type(target, dialect)
|
|
48
|
+
|
|
49
|
+
super().__init__(
|
|
50
|
+
dialect=dialect,
|
|
51
|
+
statement=expr,
|
|
52
|
+
statement_index=statement_index,
|
|
53
|
+
object_mapping=object_mapping,
|
|
54
|
+
source_info=source,
|
|
55
|
+
target_info=TargetInfo(expression=target, type=target_type),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
self.column_defs: t.List[exp.ColumnDef] = []
|
|
59
|
+
# Needed due to a bug in sqlglot. Never access the table name via print()!
|
|
60
|
+
# as it prints double-double quotes
|
|
61
|
+
target.this.set("this", "@" + str(target.this))
|
|
62
|
+
target.this.set("quoted", False)
|
|
63
|
+
|
|
64
|
+
self.properties = StageQueryProperties.from_expression(
|
|
65
|
+
self.statement, self.source_info, self.target_info, dialect
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def path(self) -> str:
|
|
70
|
+
return self.properties.path
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def name(self) -> str:
|
|
74
|
+
return self.target_info.expression.name
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def is_temporary(self) -> bool:
|
|
78
|
+
return self.properties.is_temporary
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from sqlglot import exp
|
|
7
|
+
|
|
8
|
+
from sqlleaf import mappings, util, exception, settings
|
|
9
|
+
from sqlleaf.models.query.base import Query
|
|
10
|
+
from sqlleaf.typing import SourceInfo, TargetInfo
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class TableQueryProperties:
|
|
15
|
+
location: exp.LocationProperty | None
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def from_expression(cls, expr: exp.Create, dialect: str):
|
|
19
|
+
location = None
|
|
20
|
+
if properties := expr.args.get("properties"):
|
|
21
|
+
location = properties.find(exp.LocationProperty)
|
|
22
|
+
return cls(location=location)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TableQuery(Query):
|
|
26
|
+
KIND = "table"
|
|
27
|
+
|
|
28
|
+
def __init__(self, expr: exp.Create, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int):
|
|
29
|
+
self.properties = TableQueryProperties.from_expression(expr, dialect)
|
|
30
|
+
|
|
31
|
+
if self.properties.location:
|
|
32
|
+
location_literal = self.properties.location.this
|
|
33
|
+
source_type = self._determine_expression_type(location_literal, dialect)
|
|
34
|
+
source = SourceInfo(expression=location_literal, type=source_type)
|
|
35
|
+
else:
|
|
36
|
+
source = None
|
|
37
|
+
|
|
38
|
+
target = util.get_table(expr.this)
|
|
39
|
+
target_type = self._determine_expression_type(target, dialect)
|
|
40
|
+
|
|
41
|
+
super().__init__(
|
|
42
|
+
dialect=dialect,
|
|
43
|
+
statement=expr,
|
|
44
|
+
statement_index=statement_index,
|
|
45
|
+
object_mapping=object_mapping,
|
|
46
|
+
source_info=source,
|
|
47
|
+
target_info=TargetInfo(expression=target, type=target_type),
|
|
48
|
+
skip_type_annotation=True,
|
|
49
|
+
)
|
|
50
|
+
self.column_defs: t.List[exp.ColumnDef] = []
|
|
51
|
+
self.system_column_defs: t.List[exp.ColumnDef] = []
|
|
52
|
+
self.inherits: t.List[TableQuery] = []
|
|
53
|
+
self.inherited_by: t.List[TableQuery] = []
|
|
54
|
+
|
|
55
|
+
self.property: str = util.find_property(expr, self.target_info.expression, dialect)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def location(self) -> exp.LocationProperty | None:
|
|
59
|
+
return self.properties.location
|
|
60
|
+
|
|
61
|
+
def is_external(self) -> bool:
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
def get_column_defs(self, include_system: bool = False) -> t.List[exp.ColumnDef]:
|
|
65
|
+
return self.column_defs + self.system_column_defs if include_system else self.column_defs
|
|
66
|
+
|
|
67
|
+
def get_column_names_with_types(self, include_system: bool = False) -> t.Dict[str, str]:
|
|
68
|
+
"""
|
|
69
|
+
Used by sqlglot's MappingSchema
|
|
70
|
+
"""
|
|
71
|
+
columns = {col.name: str(col.kind) for col in self.get_column_defs(include_system=include_system)}
|
|
72
|
+
return columns
|
|
73
|
+
|
|
74
|
+
def set_column_defs(self) -> None:
|
|
75
|
+
"""
|
|
76
|
+
Collect all the column definitions for this table.
|
|
77
|
+
"""
|
|
78
|
+
all_columns = []
|
|
79
|
+
|
|
80
|
+
for expression in self.statement.this.expressions:
|
|
81
|
+
if isinstance(expression, exp.ColumnDef):
|
|
82
|
+
all_columns.append(expression)
|
|
83
|
+
elif isinstance(expression, exp.LikeProperty):
|
|
84
|
+
like_columns = self._collect_like_columns(expression, self.object_mapping, self.get_target_as_table())
|
|
85
|
+
all_columns.extend(like_columns)
|
|
86
|
+
elif isinstance(expression, exp.Identifier):
|
|
87
|
+
# CREATE TABLE (a INT, b);
|
|
88
|
+
raise exception.InvalidQueryError(message=f"Column '{expression.name}' must define a data type.")
|
|
89
|
+
else:
|
|
90
|
+
raise exception.InvalidQueryError(message=f"Unsupported column expression: {type(expression)}")
|
|
91
|
+
|
|
92
|
+
if inherited_props := list[exp.InheritsProperty](self.statement.find_all(exp.InheritsProperty)):
|
|
93
|
+
inherited_columns = self._collect_inherited_columns(inherited_props)
|
|
94
|
+
all_columns = inherited_columns + all_columns
|
|
95
|
+
|
|
96
|
+
# Set the column's 'default' type to the column's own type (it is sometimes missing)
|
|
97
|
+
for col_def in all_columns:
|
|
98
|
+
if default := col_def.find(exp.DefaultColumnConstraint):
|
|
99
|
+
default.this.type = col_def.kind
|
|
100
|
+
|
|
101
|
+
self.column_defs = all_columns
|
|
102
|
+
self.system_column_defs = settings.system_columns(self.dialect)
|
|
103
|
+
|
|
104
|
+
def _collect_inherited_columns(
|
|
105
|
+
self, inherits_properties: t.List[exp.InheritsProperty]
|
|
106
|
+
) -> t.List[exp.ColumnDef]:
|
|
107
|
+
"""
|
|
108
|
+
Search for tables referenced as 'CREATE TABLE b INHERITS (a)' and collect all their columns.
|
|
109
|
+
A table can have multiple tables in an INHERITS clause.
|
|
110
|
+
"""
|
|
111
|
+
column_defs = []
|
|
112
|
+
|
|
113
|
+
for inh_prop in inherits_properties:
|
|
114
|
+
for inh_table in inh_prop.expressions:
|
|
115
|
+
parent_table_query = t.cast(TableQuery, self.object_mapping.lookup_table_query(table=inh_table))
|
|
116
|
+
parent_table_query.inherited_by.append(self)
|
|
117
|
+
self.inherits.append(parent_table_query)
|
|
118
|
+
|
|
119
|
+
# Re-assign the columns to a copy of the correct table
|
|
120
|
+
expr = self.target_info.expression
|
|
121
|
+
if expr.parent:
|
|
122
|
+
schema = util.copy_expression(expr.parent)
|
|
123
|
+
for parent_col_def in parent_table_query.column_defs:
|
|
124
|
+
col_def = parent_col_def.copy()
|
|
125
|
+
schema.append("expressions", col_def)
|
|
126
|
+
column_defs.append(col_def)
|
|
127
|
+
|
|
128
|
+
return column_defs
|
|
129
|
+
|
|
130
|
+
def _collect_like_columns(
|
|
131
|
+
self, like_property: exp.LikeProperty, object_mapping: mappings.ObjectMapping, child_object: exp.Table
|
|
132
|
+
) -> t.List[exp.ColumnDef]:
|
|
133
|
+
"""
|
|
134
|
+
Search for tables referenced as 'CREATE TABLE b (LIKE a)'.
|
|
135
|
+
A table can have multiple LIKE clauses.
|
|
136
|
+
"""
|
|
137
|
+
columns = []
|
|
138
|
+
property_names = []
|
|
139
|
+
|
|
140
|
+
for like_prop in like_property.expressions:
|
|
141
|
+
# sqlglot concats properties with '='
|
|
142
|
+
property_names.append(str(like_prop).replace("=", " "))
|
|
143
|
+
|
|
144
|
+
properties = self._get_properties_to_include(property_names)
|
|
145
|
+
|
|
146
|
+
# Look up the like-table's columns and determine which properties to transfer
|
|
147
|
+
parent_table_query = t.cast(TableQuery, object_mapping.lookup_table_query(table=like_property.this))
|
|
148
|
+
parent_columns = parent_table_query.get_column_defs()
|
|
149
|
+
|
|
150
|
+
for parent_col_def in parent_columns:
|
|
151
|
+
new_col = parent_col_def.copy()
|
|
152
|
+
for prop_name, prop_attrs in properties.items():
|
|
153
|
+
prop_expr = new_col.find(prop_attrs["expr"])
|
|
154
|
+
|
|
155
|
+
if properties[prop_name]["include"]:
|
|
156
|
+
# Set the expression's parent to be the new table (it's missing)
|
|
157
|
+
if prop_expr:
|
|
158
|
+
for inner_col in prop_expr.find_all(exp.Column):
|
|
159
|
+
# A GENERATED column expression might refer to other columns
|
|
160
|
+
try:
|
|
161
|
+
referenced_parent_col_def = [c for c in parent_columns if c.name == inner_col.name][0]
|
|
162
|
+
except IndexError:
|
|
163
|
+
message = f"Column '{inner_col.name}' does not exist in table '{child_object}'."
|
|
164
|
+
raise exception.MappingError(message=message)
|
|
165
|
+
|
|
166
|
+
inner_col.set("catalog", exp.to_identifier(child_object.catalog))
|
|
167
|
+
inner_col.set("db", exp.to_identifier(child_object.db))
|
|
168
|
+
inner_col.set("table", exp.to_identifier(child_object.this))
|
|
169
|
+
inner_col.type = referenced_parent_col_def.kind
|
|
170
|
+
else:
|
|
171
|
+
# Discard the column's expression
|
|
172
|
+
if prop_expr:
|
|
173
|
+
prop_expr.parent.pop()
|
|
174
|
+
|
|
175
|
+
columns.append(new_col)
|
|
176
|
+
|
|
177
|
+
return columns
|
|
178
|
+
|
|
179
|
+
def _get_properties_to_include(self, options: t.List[str]) -> t.Dict:
|
|
180
|
+
"""
|
|
181
|
+
Determine which column properties to keep within a LIKE according to the rules below.
|
|
182
|
+
|
|
183
|
+
From the Postgres docs:
|
|
184
|
+
Specifying INCLUDING copies the property, specifying EXCLUDING omits the property.
|
|
185
|
+
EXCLUDING is the default. If multiple specifications are made for the same kind
|
|
186
|
+
of object, the last one is used. It could be useful to write individual EXCLUDING
|
|
187
|
+
clauses after INCLUDING ALL to select all but some specific options.
|
|
188
|
+
"""
|
|
189
|
+
# All supported properties
|
|
190
|
+
properties = {
|
|
191
|
+
"DEFAULTS": {"include": False, "expr": exp.DefaultColumnConstraint},
|
|
192
|
+
"GENERATED": {"include": False, "expr": exp.ComputedColumnConstraint},
|
|
193
|
+
"IDENTITY": {"include": False, "expr": exp.GeneratedAsIdentityColumnConstraint},
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for opt in options:
|
|
197
|
+
opt = opt.strip().upper()
|
|
198
|
+
|
|
199
|
+
if opt == "INCLUDING ALL":
|
|
200
|
+
for prop in properties:
|
|
201
|
+
properties[prop]["include"] = True
|
|
202
|
+
continue
|
|
203
|
+
|
|
204
|
+
if opt == "EXCLUDING ALL":
|
|
205
|
+
for prop in properties:
|
|
206
|
+
properties[prop]["include"] = False
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
parts = opt.split()
|
|
210
|
+
action, prop = parts
|
|
211
|
+
|
|
212
|
+
if prop not in properties:
|
|
213
|
+
continue # Ignore unknown properties
|
|
214
|
+
|
|
215
|
+
if action == "INCLUDING":
|
|
216
|
+
properties[prop]["include"] = True
|
|
217
|
+
elif action == "EXCLUDING":
|
|
218
|
+
properties[prop]["include"] = False
|
|
219
|
+
|
|
220
|
+
return properties
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from sqlglot import exp
|
|
4
|
+
|
|
5
|
+
from sqlleaf import mappings
|
|
6
|
+
from sqlleaf.models.query.base import Query
|
|
7
|
+
from sqlleaf.typing import SqlObjectType, TargetInfo
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TriggerQuery(Query):
|
|
11
|
+
KIND = "trigger"
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
expr: exp.Create,
|
|
16
|
+
dialect: str,
|
|
17
|
+
object_mapping: mappings.ObjectMapping,
|
|
18
|
+
statement_index: int,
|
|
19
|
+
):
|
|
20
|
+
"""
|
|
21
|
+
Example:
|
|
22
|
+
CREATE TRIGGER before_fruit_insert
|
|
23
|
+
BEFORE INSERT ON fruit.processed
|
|
24
|
+
FOR EACH ROW
|
|
25
|
+
EXECUTE FUNCTION check_fruit('apple');
|
|
26
|
+
"""
|
|
27
|
+
properties = expr.args["properties"].expressions[0]
|
|
28
|
+
target = expr.this
|
|
29
|
+
super().__init__(
|
|
30
|
+
dialect=dialect,
|
|
31
|
+
statement=expr,
|
|
32
|
+
statement_index=statement_index,
|
|
33
|
+
object_mapping=object_mapping,
|
|
34
|
+
source_info=None,
|
|
35
|
+
target_info=TargetInfo(expression=target, type=SqlObjectType.TABLE),
|
|
36
|
+
)
|
|
37
|
+
self.name = expr.name # before_fruit_insert
|
|
38
|
+
self.table = properties.args["table"] # Table(fruit.processed)
|
|
39
|
+
self.timing = properties.args["timing"] # BEFORE
|
|
40
|
+
self.events = properties.args["events"] # [TriggerEvent(INSERT)]
|
|
41
|
+
self.execute = properties.args["execute"].this # Anonymous(check_fruit())
|
|
42
|
+
self.execute_args = self.execute.expressions # [Literal(apple)]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
from sqlglot import exp
|
|
6
|
+
|
|
7
|
+
from sqlleaf import mappings, util
|
|
8
|
+
from sqlleaf.models.query.base import Query
|
|
9
|
+
from sqlleaf.typing import TargetInfo
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TypeQuery(Query):
|
|
13
|
+
KIND = "type"
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self, statement: exp.Create, dialect: str, object_mapping: mappings.ObjectMapping, statement_index: int
|
|
17
|
+
):
|
|
18
|
+
source = None
|
|
19
|
+
target = util.get_table(statement)
|
|
20
|
+
|
|
21
|
+
target_type = self._determine_expression_type(target, dialect)
|
|
22
|
+
|
|
23
|
+
super().__init__(
|
|
24
|
+
dialect=dialect,
|
|
25
|
+
statement=statement,
|
|
26
|
+
statement_index=statement_index,
|
|
27
|
+
object_mapping=object_mapping,
|
|
28
|
+
source_info=source,
|
|
29
|
+
target_info=TargetInfo(expression=target, type=target_type),
|
|
30
|
+
)
|
|
31
|
+
self.column_defs: t.List[exp.ColumnDef] = []
|
|
32
|
+
self._collect()
|
|
33
|
+
|
|
34
|
+
def _collect(self) -> None:
|
|
35
|
+
"""
|
|
36
|
+
Collect the type's column definitions.
|
|
37
|
+
"""
|
|
38
|
+
expression = self.statement.args.get("expression")
|
|
39
|
+
if isinstance(expression, exp.Schema):
|
|
40
|
+
for col_def in expression.expressions:
|
|
41
|
+
if isinstance(col_def, exp.ColumnDef):
|
|
42
|
+
self.column_defs.append(col_def)
|