sqlglotc 30.16.0__tar.gz → 30.17.0__tar.gz
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.
- {sqlglotc-30.16.0/sqlglotc.egg-info → sqlglotc-30.17.0}/PKG-INFO +3 -3
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/pyproject.toml +2 -2
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/core.py +10 -1
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/query.py +22 -1
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generator.py +20 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/python.py +14 -1
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/sqlite.py +16 -1
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/trino.py +26 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/optimizer/annotate_types.py +21 -6
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/optimizer/canonicalize_internal_names.py +6 -4
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/optimizer/qualify_columns.py +27 -15
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/optimizer/qualify_tables.py +14 -1
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/optimizer/scope.py +58 -5
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/optimizer/simplify.py +17 -4
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parser.py +26 -6
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/clickhouse.py +2 -2
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/prql.py +1 -1
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/spark2.py +5 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/sqlite.py +27 -3
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/trino.py +45 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0/sqlglotc.egg-info}/PKG-INFO +3 -3
- sqlglotc-30.17.0/sqlglotc.egg-info/requires.txt +6 -0
- sqlglotc-30.17.0/sqlglotc.egg-info/scm_version.json +8 -0
- sqlglotc-30.16.0/sqlglotc.egg-info/requires.txt +0 -6
- sqlglotc-30.16.0/sqlglotc.egg-info/scm_version.json +0 -8
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/MANIFEST.in +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/setup.cfg +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/setup.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/anonymize.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/errors.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/executor/table.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/aggregate.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/array.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/builders.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/constraints.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/datatypes.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/ddl.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/dml.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/functions.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/json.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/math.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/properties.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/string.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/temporal.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/athena.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/bigquery.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/clickhouse.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/databricks.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/dax.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/doris.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/dremio.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/drill.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/druid.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/duckdb.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/dune.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/exasol.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/fabric.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/hive.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/materialize.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/mysql.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/oracle.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/postgres.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/presto.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/prql.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/redshift.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/risingwave.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/singlestore.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/snowflake.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/solr.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/spark.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/spark2.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/starrocks.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/tableau.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/teradata.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/tsql.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/helper.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/lineage.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/optimizer/isolate_table_selects.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/optimizer/normalize_identifiers.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/optimizer/qualify.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/optimizer/resolver.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/athena.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/base.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/bigquery.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/databricks.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/dax.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/doris.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/dremio.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/drill.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/druid.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/duckdb.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/dune.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/exasol.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/fabric.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/hive.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/materialize.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/mysql.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/oracle.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/postgres.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/presto.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/redshift.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/risingwave.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/singlestore.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/snowflake.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/solr.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/spark.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/starrocks.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/tableau.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/teradata.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/tsql.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/schema.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/serde.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/time.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/tokenizer_core.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/trie.py +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglotc.egg-info/SOURCES.txt +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglotc.egg-info/dependency_links.txt +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglotc.egg-info/scm_file_list.json +0 -0
- {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglotc.egg-info/top_level.txt +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlglotc
|
|
3
|
-
Version: 30.
|
|
3
|
+
Version: 30.17.0
|
|
4
4
|
Summary: mypyc-compiled extensions for sqlglot
|
|
5
5
|
Author-email: Toby Mao <toby.mao@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://sqlglot.com/
|
|
8
8
|
Project-URL: Repository, https://github.com/tobymao/sqlglot
|
|
9
9
|
Requires-Python: >=3.10
|
|
10
|
-
Requires-Dist: sqlglot==30.
|
|
10
|
+
Requires-Dist: sqlglot==30.17.0
|
|
11
11
|
Provides-Extra: dev
|
|
12
12
|
Requires-Dist: setuptools>=61.0; extra == "dev"
|
|
13
13
|
Requires-Dist: setuptools_scm; extra == "dev"
|
|
14
|
-
Requires-Dist: sqlglot-mypy>=2.
|
|
14
|
+
Requires-Dist: sqlglot-mypy>=2.3.0.post1; extra == "dev"
|
|
15
15
|
Dynamic: requires-dist
|
|
@@ -7,7 +7,7 @@ license = "MIT"
|
|
|
7
7
|
requires-python = ">= 3.10"
|
|
8
8
|
|
|
9
9
|
[project.optional-dependencies]
|
|
10
|
-
dev = ["setuptools >= 61.0", "setuptools_scm", "sqlglot-mypy >= 2.
|
|
10
|
+
dev = ["setuptools >= 61.0", "setuptools_scm", "sqlglot-mypy >= 2.3.0.post1"]
|
|
11
11
|
|
|
12
12
|
[project.urls]
|
|
13
13
|
Homepage = "https://sqlglot.com/"
|
|
@@ -17,7 +17,7 @@ Repository = "https://github.com/tobymao/sqlglot"
|
|
|
17
17
|
requires = [
|
|
18
18
|
"setuptools >= 61.0",
|
|
19
19
|
"setuptools_scm",
|
|
20
|
-
"sqlglot-mypy >= 2.
|
|
20
|
+
"sqlglot-mypy >= 2.3.0.post1",
|
|
21
21
|
"types-python-dateutil",
|
|
22
22
|
"sqlglot",
|
|
23
23
|
]
|
|
@@ -1699,7 +1699,16 @@ class AggFunc(Func):
|
|
|
1699
1699
|
|
|
1700
1700
|
|
|
1701
1701
|
class Column(Expression, Condition):
|
|
1702
|
-
|
|
1702
|
+
# "shadow" marks a column whose qualifier is shadowed by a projection alias, so it must be
|
|
1703
|
+
# rendered unqualified in dialects where PROJECTION_ALIASES_SHADOW_SOURCE_NAMES is set
|
|
1704
|
+
arg_types = {
|
|
1705
|
+
"this": True,
|
|
1706
|
+
"table": False,
|
|
1707
|
+
"db": False,
|
|
1708
|
+
"catalog": False,
|
|
1709
|
+
"join_mark": False,
|
|
1710
|
+
"shadow": False,
|
|
1711
|
+
}
|
|
1703
1712
|
|
|
1704
1713
|
@property
|
|
1705
1714
|
def table(self) -> str:
|
|
@@ -1050,6 +1050,11 @@ class SetOperation(Expression, Query):
|
|
|
1050
1050
|
def named_selects(self) -> list[str]:
|
|
1051
1051
|
expr: Expr = self
|
|
1052
1052
|
while isinstance(expr, SetOperation):
|
|
1053
|
+
if expr.args.get("by_name"):
|
|
1054
|
+
left = t.cast(Selectable, expr.this.unnest()).named_selects
|
|
1055
|
+
right = t.cast(Selectable, expr.expression.unnest()).named_selects
|
|
1056
|
+
return list(dict.fromkeys(left + right))
|
|
1057
|
+
|
|
1053
1058
|
expr = expr.this.unnest()
|
|
1054
1059
|
return _named_selects(expr)
|
|
1055
1060
|
|
|
@@ -2124,7 +2129,23 @@ class CaseStatement(Expression):
|
|
|
2124
2129
|
|
|
2125
2130
|
|
|
2126
2131
|
class WhileBlock(Expression):
|
|
2127
|
-
arg_types = {"this": True, "body": True}
|
|
2132
|
+
arg_types = {"this": True, "body": True, "label": False}
|
|
2133
|
+
|
|
2134
|
+
|
|
2135
|
+
class LoopBlock(Expression):
|
|
2136
|
+
arg_types = {"body": True, "label": False}
|
|
2137
|
+
|
|
2138
|
+
|
|
2139
|
+
class RepeatBlock(Expression):
|
|
2140
|
+
arg_types = {"body": True, "until": True, "label": False}
|
|
2141
|
+
|
|
2142
|
+
|
|
2143
|
+
class Leave(Expression):
|
|
2144
|
+
pass
|
|
2145
|
+
|
|
2146
|
+
|
|
2147
|
+
class Iterate(Expression):
|
|
2148
|
+
pass
|
|
2128
2149
|
|
|
2129
2150
|
|
|
2130
2151
|
class EndStatement(Expression):
|
|
@@ -1151,6 +1151,10 @@ class Generator:
|
|
|
1151
1151
|
return f"{default}CHARACTER SET={self.sql(expression, 'this')}"
|
|
1152
1152
|
|
|
1153
1153
|
def column_parts(self, expression: exp.Column) -> str:
|
|
1154
|
+
if expression.args.get("shadow") and self.dialect.PROJECTION_ALIASES_SHADOW_SOURCE_NAMES:
|
|
1155
|
+
# The qualifier would be captured by a colliding projection alias (see qualify_columns)
|
|
1156
|
+
return self.sql(expression, "this")
|
|
1157
|
+
|
|
1154
1158
|
return ".".join(
|
|
1155
1159
|
self.sql(part)
|
|
1156
1160
|
for part in (
|
|
@@ -6319,6 +6323,22 @@ class Generator:
|
|
|
6319
6323
|
self.unsupported("Unsupported While block syntax")
|
|
6320
6324
|
return ""
|
|
6321
6325
|
|
|
6326
|
+
def loopblock_sql(self, expression: exp.LoopBlock) -> str:
|
|
6327
|
+
self.unsupported("Unsupported Loop block syntax")
|
|
6328
|
+
return ""
|
|
6329
|
+
|
|
6330
|
+
def repeatblock_sql(self, expression: exp.RepeatBlock) -> str:
|
|
6331
|
+
self.unsupported("Unsupported Repeat block syntax")
|
|
6332
|
+
return ""
|
|
6333
|
+
|
|
6334
|
+
def leave_sql(self, expression: exp.Leave) -> str:
|
|
6335
|
+
self.unsupported("Unsupported Leave syntax")
|
|
6336
|
+
return ""
|
|
6337
|
+
|
|
6338
|
+
def iterate_sql(self, expression: exp.Iterate) -> str:
|
|
6339
|
+
self.unsupported("Unsupported Iterate syntax")
|
|
6340
|
+
return ""
|
|
6341
|
+
|
|
6322
6342
|
def execute_sql(self, expression: exp.Execute) -> str:
|
|
6323
6343
|
self.unsupported("Unsupported Execute syntax")
|
|
6324
6344
|
return ""
|
|
@@ -58,6 +58,15 @@ def _lambda_sql(self, e: exp.Lambda) -> str:
|
|
|
58
58
|
return f"lambda {self.expressions(e, flat=True)}: {self.sql(e, 'this')}"
|
|
59
59
|
|
|
60
60
|
|
|
61
|
+
def _like_sql(self: generator.Generator, e: exp.Like | exp.ILike) -> str:
|
|
62
|
+
sql = self.func(e.key, e.this, e.expression)
|
|
63
|
+
|
|
64
|
+
if e.args.get("negate"):
|
|
65
|
+
sql = f"NOT({sql})"
|
|
66
|
+
|
|
67
|
+
return sql
|
|
68
|
+
|
|
69
|
+
|
|
61
70
|
def _div_sql(self: generator.Generator, e: exp.Div) -> str:
|
|
62
71
|
denominator = self.sql(e, "expression")
|
|
63
72
|
|
|
@@ -66,7 +75,9 @@ def _div_sql(self: generator.Generator, e: exp.Div) -> str:
|
|
|
66
75
|
|
|
67
76
|
sql = f"DIV({self.sql(e, 'this')}, {denominator})"
|
|
68
77
|
|
|
69
|
-
if e.args.get("typed")
|
|
78
|
+
if e.args.get("typed") and not (
|
|
79
|
+
e.this.is_type(*exp.DataType.REAL_TYPES) or e.expression.is_type(*exp.DataType.REAL_TYPES)
|
|
80
|
+
):
|
|
70
81
|
sql = f"int({sql})"
|
|
71
82
|
|
|
72
83
|
return sql
|
|
@@ -90,6 +101,7 @@ class PythonGenerator(generator.Generator):
|
|
|
90
101
|
exp.Distinct: lambda self, e: f"set({self.sql(e, 'this')})",
|
|
91
102
|
exp.Div: _div_sql,
|
|
92
103
|
exp.Extract: lambda self, e: f"EXTRACT('{e.name.lower()}', {self.sql(e, 'expression')})",
|
|
104
|
+
exp.ILike: _like_sql,
|
|
93
105
|
exp.In: lambda self, e: self.func("IN", e.this, *e.expressions),
|
|
94
106
|
exp.Interval: lambda self, e: f"INTERVAL({self.sql(e.this)}, '{self.sql(e.unit)}')",
|
|
95
107
|
exp.Is: lambda self, e: (
|
|
@@ -102,6 +114,7 @@ class PythonGenerator(generator.Generator):
|
|
|
102
114
|
exp.JSONPathKey: lambda self, e: f"'{self.sql(e.this)}'",
|
|
103
115
|
exp.JSONPathSubscript: lambda self, e: f"'{e.this}'",
|
|
104
116
|
exp.Lambda: _lambda_sql,
|
|
117
|
+
exp.Like: _like_sql,
|
|
105
118
|
exp.Not: lambda self, e: self.func("NOT", e.this),
|
|
106
119
|
exp.Null: lambda *_: "None",
|
|
107
120
|
exp.Or: lambda self, e: f"OR(lambda: {self.sql(e.left)}, lambda: {self.sql(e.right)})",
|
|
@@ -69,7 +69,9 @@ def _generated_to_auto_increment(expression: exp.Expr) -> exp.Expr:
|
|
|
69
69
|
|
|
70
70
|
generated = expression.find(exp.GeneratedAsIdentityColumnConstraint)
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
# Only rewrite true identity columns. Expression-bearing forms are computed
|
|
73
|
+
# columns (GENERATED ALWAYS AS (expr)) and must keep their expression.
|
|
74
|
+
if generated and generated.expression is None:
|
|
73
75
|
t.cast(exp.ColumnConstraint, generated.parent).pop()
|
|
74
76
|
|
|
75
77
|
not_null = expression.find(exp.NotNullColumnConstraint)
|
|
@@ -253,6 +255,19 @@ class SQLiteGenerator(generator.Generator):
|
|
|
253
255
|
|
|
254
256
|
return super().cast_sql(expression)
|
|
255
257
|
|
|
258
|
+
# https://www.sqlite.org/gencol.html
|
|
259
|
+
# Inline unsupported check: mypyc cannot compile @unsupported_args on an
|
|
260
|
+
# override of an undecorated base-class method.
|
|
261
|
+
def computedcolumnconstraint_sql(self, expression: exp.ComputedColumnConstraint) -> str:
|
|
262
|
+
if expression.args.get("data_type"):
|
|
263
|
+
self.unsupported("SQLite generated columns do not support a data type")
|
|
264
|
+
|
|
265
|
+
this = expression.this
|
|
266
|
+
this_sql = self.sql(this) if isinstance(this, exp.Paren) else f"({self.sql(this)})"
|
|
267
|
+
storage = " STORED" if expression.args.get("persisted") else ""
|
|
268
|
+
not_null = " NOT NULL" if expression.args.get("not_null") else ""
|
|
269
|
+
return f"AS {this_sql}{storage}{not_null}"
|
|
270
|
+
|
|
256
271
|
# Note: SQLite's TRUNC always returns REAL (e.g., trunc(10.99) -> 10.0), not INTEGER.
|
|
257
272
|
# This creates a transpilation gap affecting division semantics, similar to Presto.
|
|
258
273
|
# Unlike Presto where this only affects decimals=0, SQLite has no decimals parameter
|
|
@@ -98,6 +98,32 @@ class TrinoGenerator(PrestoGenerator):
|
|
|
98
98
|
branches.append("END CASE")
|
|
99
99
|
return " ".join(branches)
|
|
100
100
|
|
|
101
|
+
def whileblock_sql(self, expression: exp.WhileBlock) -> str:
|
|
102
|
+
label = expression.args.get("label")
|
|
103
|
+
label_sql = f"{self.sql(label)}: " if label else ""
|
|
104
|
+
condition = self.sql(expression, "this")
|
|
105
|
+
body = self.sql(expression, "body")
|
|
106
|
+
return f"{label_sql}WHILE {condition} DO {body}; END WHILE"
|
|
107
|
+
|
|
108
|
+
def loopblock_sql(self, expression: exp.LoopBlock) -> str:
|
|
109
|
+
label = expression.args.get("label")
|
|
110
|
+
label_sql = f"{self.sql(label)}: " if label else ""
|
|
111
|
+
body = self.sql(expression, "body")
|
|
112
|
+
return f"{label_sql}LOOP {body}; END LOOP"
|
|
113
|
+
|
|
114
|
+
def repeatblock_sql(self, expression: exp.RepeatBlock) -> str:
|
|
115
|
+
label = expression.args.get("label")
|
|
116
|
+
label_sql = f"{self.sql(label)}: " if label else ""
|
|
117
|
+
body = self.sql(expression, "body")
|
|
118
|
+
until = self.sql(expression, "until")
|
|
119
|
+
return f"{label_sql}REPEAT {body}; UNTIL {until} END REPEAT"
|
|
120
|
+
|
|
121
|
+
def leave_sql(self, expression: exp.Leave) -> str:
|
|
122
|
+
return f"LEAVE {self.sql(expression, 'this')}"
|
|
123
|
+
|
|
124
|
+
def iterate_sql(self, expression: exp.Iterate) -> str:
|
|
125
|
+
return f"ITERATE {self.sql(expression, 'this')}"
|
|
126
|
+
|
|
101
127
|
def jsonextract_sql(self, expression: exp.JSONExtract) -> str:
|
|
102
128
|
if not expression.args.get("json_query"):
|
|
103
129
|
return super().jsonextract_sql(expression)
|
|
@@ -385,8 +385,9 @@ class TypeAnnotator:
|
|
|
385
385
|
|
|
386
386
|
return {alias: column.type for alias, column in zip(alias_column_names, values)}
|
|
387
387
|
|
|
388
|
-
if isinstance(expression, exp.SetOperation) and
|
|
389
|
-
expression.
|
|
388
|
+
if isinstance(expression, exp.SetOperation) and (
|
|
389
|
+
expression.args.get("by_name")
|
|
390
|
+
or len(expression.this.selects) == len(expression.expression.selects)
|
|
390
391
|
):
|
|
391
392
|
return self._get_setop_column_types(expression)
|
|
392
393
|
|
|
@@ -522,7 +523,13 @@ class TypeAnnotator:
|
|
|
522
523
|
i = iter(dot_parts)
|
|
523
524
|
parent = expr.parent
|
|
524
525
|
while isinstance(parent, exp.Dot):
|
|
525
|
-
parent.expression
|
|
526
|
+
identifier = parent.expression
|
|
527
|
+
if isinstance(identifier, exp.Identifier):
|
|
528
|
+
# Rename in place to preserve the identifier's meta, e.g. token positions
|
|
529
|
+
identifier.set("this", next(i))
|
|
530
|
+
identifier.set("quoted", True)
|
|
531
|
+
else:
|
|
532
|
+
identifier.replace(exp.to_identifier(next(i), quoted=True))
|
|
526
533
|
parent = parent.parent
|
|
527
534
|
|
|
528
535
|
expr.meta.pop("dot_parts", None)
|
|
@@ -633,12 +640,16 @@ class TypeAnnotator:
|
|
|
633
640
|
|
|
634
641
|
col_types: dict[str, exp.DataType | exp.DType] = {}
|
|
635
642
|
|
|
636
|
-
# Validate that left and right have same number of projections
|
|
643
|
+
# Validate that left and right have same number of projections (BY NAME
|
|
644
|
+
# operations match columns by name, so their counts are allowed to differ)
|
|
637
645
|
if not (
|
|
638
646
|
isinstance(setop, exp.SetOperation)
|
|
639
647
|
and setop.this.selects
|
|
640
648
|
and setop.expression.selects
|
|
641
|
-
and
|
|
649
|
+
and (
|
|
650
|
+
setop.args.get("by_name")
|
|
651
|
+
or len(setop.this.selects) == len(setop.expression.selects)
|
|
652
|
+
)
|
|
642
653
|
):
|
|
643
654
|
return col_types
|
|
644
655
|
|
|
@@ -650,14 +661,18 @@ class TypeAnnotator:
|
|
|
650
661
|
continue
|
|
651
662
|
|
|
652
663
|
if set_op.args.get("by_name"):
|
|
664
|
+
# Columns missing from one side are filled with NULLs, so the other
|
|
665
|
+
# side's type is preserved (NULL is the identity for _maybe_coerce)
|
|
653
666
|
r_type_by_select = {s.alias_or_name: s.type for s in set_op.expression.selects}
|
|
654
667
|
setop_cols = {
|
|
655
668
|
s.alias_or_name: self._maybe_coerce(
|
|
656
669
|
t.cast(exp.DataType, s.type),
|
|
657
|
-
r_type_by_select.
|
|
670
|
+
r_type_by_select.pop(s.alias_or_name, exp.DType.NULL) or exp.DType.UNKNOWN,
|
|
658
671
|
)
|
|
659
672
|
for s in set_op.this.selects
|
|
660
673
|
}
|
|
674
|
+
for name, r_type in r_type_by_select.items():
|
|
675
|
+
setop_cols[name] = r_type or exp.DType.UNKNOWN
|
|
661
676
|
else:
|
|
662
677
|
setop_cols = {
|
|
663
678
|
ls.alias_or_name: self._maybe_coerce(
|
|
@@ -30,11 +30,13 @@ def canonicalize_internal_names(expression: E) -> E:
|
|
|
30
30
|
>>> canonicalize_internal_names(qualify(sqlglot.parse_one("WITH t AS (SELECT c1, c2 FROM c.db.src) SELECT * FROM t"), schema=schema)).sql()
|
|
31
31
|
'WITH "_t1" AS (SELECT "_t0"."c1" AS "_c0", "_t0"."c2" AS "_c1" FROM "c"."db"."src" AS "_t0") SELECT "_t1"."_c0" AS "c1", "_t1"."_c1" AS "c2" FROM "_t1" AS "_t1"'
|
|
32
32
|
"""
|
|
33
|
+
# Skip non-queries for now (e.g., UPDATE ... SET x = s.x FROM (SELECT ...) AS s)
|
|
34
|
+
if not isinstance(expression, exp.Query):
|
|
35
|
+
return expression
|
|
33
36
|
|
|
34
|
-
# Top-level output scopes: their aliases are the query's data contract.
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
# contribute.
|
|
37
|
+
# Top-level output scopes: their aliases are the query's data contract. Regular UNION takes names
|
|
38
|
+
# from the left branch; UNION BY NAME takes names from the union of all branches, so both sides of
|
|
39
|
+
# a by_name SetOperation contribute.
|
|
38
40
|
output_scope_exprs: set[int] = set()
|
|
39
41
|
stack: list[exp.Expr] = [expression]
|
|
40
42
|
while stack:
|
|
@@ -9,7 +9,14 @@ from sqlglot.dialects.dialect import Dialect, DialectType
|
|
|
9
9
|
from sqlglot.errors import OptimizeError, highlight_sql
|
|
10
10
|
from sqlglot.optimizer.annotate_types import TypeAnnotator
|
|
11
11
|
from sqlglot.optimizer.resolver import Resolver
|
|
12
|
-
from sqlglot.optimizer.scope import
|
|
12
|
+
from sqlglot.optimizer.scope import (
|
|
13
|
+
Scope,
|
|
14
|
+
build_scope,
|
|
15
|
+
find_all_in_scope,
|
|
16
|
+
find_in_scope,
|
|
17
|
+
traverse_scope,
|
|
18
|
+
walk_in_scope,
|
|
19
|
+
)
|
|
13
20
|
from sqlglot.optimizer.simplify import simplify_parens
|
|
14
21
|
from sqlglot.schema import Schema, ensure_schema
|
|
15
22
|
|
|
@@ -382,20 +389,6 @@ def _expand_alias_refs(
|
|
|
382
389
|
node.parts[0].name in projections
|
|
383
390
|
for node in alias_expr.find_all(exp.Column)
|
|
384
391
|
)
|
|
385
|
-
elif dialect.PROJECTION_ALIASES_SHADOW_SOURCE_NAMES and (
|
|
386
|
-
is_group_by or is_having or is_qualify
|
|
387
|
-
):
|
|
388
|
-
column_table = table.name if table else column.table
|
|
389
|
-
if column_table in projections:
|
|
390
|
-
# BigQuery's GROUP BY and HAVING clauses get confused if the column name
|
|
391
|
-
# matches a source name and a projection. For instance:
|
|
392
|
-
# SELECT id, ARRAY_AGG(col) AS custom_fields FROM custom_fields GROUP BY id HAVING id >= 1
|
|
393
|
-
# We should not qualify "id" with "custom_fields" in either clause, since the aggregation shadows the actual table
|
|
394
|
-
# and we'd get the error: "Column custom_fields contains an aggregation function, which is not allowed in GROUP BY clause"
|
|
395
|
-
column.replace(exp.to_identifier(column.name))
|
|
396
|
-
replaced = True
|
|
397
|
-
return
|
|
398
|
-
|
|
399
392
|
if table and (not alias_expr or skip_replace):
|
|
400
393
|
column.set("table", table)
|
|
401
394
|
elif not column.table and alias_expr and not skip_replace:
|
|
@@ -460,6 +453,25 @@ def _expand_alias_refs(
|
|
|
460
453
|
for join in expression.args.get("joins") or []:
|
|
461
454
|
replace_columns(join)
|
|
462
455
|
|
|
456
|
+
if dialect.PROJECTION_ALIASES_SHADOW_SOURCE_NAMES:
|
|
457
|
+
# In BigQuery's GROUP BY, HAVING and QUALIFY clauses, a qualifier that collides with a
|
|
458
|
+
# projection alias resolves to the projection instead of the source. For instance:
|
|
459
|
+
# SELECT id, ARRAY_AGG(col) AS custom_fields FROM custom_fields GROUP BY custom_fields.id
|
|
460
|
+
# fails with "Column custom_fields contains an aggregation function, which is not
|
|
461
|
+
# allowed in GROUP BY", so such references must be rendered as bare names. We keep the
|
|
462
|
+
# columns qualified and mark them, deferring to Generator.column_parts
|
|
463
|
+
for clause in (
|
|
464
|
+
expression.args.get("group"),
|
|
465
|
+
expression.args.get("having"),
|
|
466
|
+
expression.args.get("qualify"),
|
|
467
|
+
):
|
|
468
|
+
if not clause:
|
|
469
|
+
continue
|
|
470
|
+
|
|
471
|
+
for column in find_all_in_scope(clause, exp.Column):
|
|
472
|
+
if column.table and not column.db:
|
|
473
|
+
column.set("shadow", column.table in projections or None)
|
|
474
|
+
|
|
463
475
|
if replaced:
|
|
464
476
|
scope.clear_cache()
|
|
465
477
|
|
|
@@ -112,10 +112,23 @@ def qualify_tables(
|
|
|
112
112
|
local_columns = scope.local_columns
|
|
113
113
|
canonical_aliases: dict[str, str] = {}
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
queries: list[exp.Expr] = list(scope.subqueries)
|
|
116
|
+
|
|
117
|
+
# Subquery wrappers around a DML / DDL query fragment, e.g., a CREATE FUNCTION body or
|
|
118
|
+
# an UPDATE's SET subquery, don't belong to any scope, so they aren't collected above
|
|
119
|
+
if scope.is_root and isinstance(scope.expression, exp.Subquery):
|
|
120
|
+
queries.append(scope.expression.unnest())
|
|
121
|
+
elif scope.is_subquery:
|
|
122
|
+
queries.append(scope.expression)
|
|
123
|
+
|
|
124
|
+
for query in queries:
|
|
116
125
|
subquery = query.parent
|
|
117
126
|
if isinstance(subquery, exp.Subquery):
|
|
118
127
|
unwrapped = subquery.unwrap()
|
|
128
|
+
if isinstance(unwrapped.parent, (exp.From, exp.Join)):
|
|
129
|
+
# We can reach this from a wrapped derived table, which must keep its alias
|
|
130
|
+
continue
|
|
131
|
+
|
|
119
132
|
if isinstance(unwrapped.parent, exp.Create) and unwrapped is not subquery:
|
|
120
133
|
# Function bodies may require wrapping parentheses, e.g. in BigQuery
|
|
121
134
|
# `... AS ((SELECT 1))` the outer parens delimit the body itself
|
|
@@ -189,6 +189,12 @@ class Scope:
|
|
|
189
189
|
self._semi_anti_join_tables = set()
|
|
190
190
|
self._column_index = set()
|
|
191
191
|
|
|
192
|
+
# The inner query of a Subquery-rooted scope is scoped as a derived table by
|
|
193
|
+
# `_traverse_tables`, so it must not also be collected as a subquery
|
|
194
|
+
inner_query = (
|
|
195
|
+
self.expression.unnest() if isinstance(self.expression, exp.Subquery) else None
|
|
196
|
+
)
|
|
197
|
+
|
|
192
198
|
for node in self.walk():
|
|
193
199
|
# Most nodes (identifiers, literals, operators etc.) aren't collectible, so a
|
|
194
200
|
# single isinstance gate lets them skip the classification chain below.
|
|
@@ -220,7 +226,11 @@ class Scope:
|
|
|
220
226
|
self._ctes.append(node)
|
|
221
227
|
elif _is_derived_table(node) and _is_from_or_join(node):
|
|
222
228
|
self._derived_tables.append(t.cast(exp.Subquery, node))
|
|
223
|
-
elif
|
|
229
|
+
elif (
|
|
230
|
+
isinstance(node, exp.UNWRAPPED_QUERIES)
|
|
231
|
+
and not _is_from_or_join(node)
|
|
232
|
+
and node is not inner_query
|
|
233
|
+
):
|
|
224
234
|
self._subqueries.append(node)
|
|
225
235
|
elif isinstance(node, exp.TableColumn):
|
|
226
236
|
self._table_columns.append(node)
|
|
@@ -705,10 +715,47 @@ def _traverse_scope(scope: Scope) -> Iterator[Scope]:
|
|
|
705
715
|
return
|
|
706
716
|
elif isinstance(expression, exp.DML):
|
|
707
717
|
yield from _traverse_ctes(scope)
|
|
718
|
+
|
|
719
|
+
# Bare tables in relation position (e.g. UPDATE ... FROM t, DELETE / MERGE ... USING t)
|
|
720
|
+
# aren't part of any query, so they're scoped as standalone tables; `_traverse_tables`
|
|
721
|
+
# also picks up any joins hanging off of them
|
|
722
|
+
relations: list[exp.Expr] = []
|
|
723
|
+
from_ = expression.args.get("from_")
|
|
724
|
+
|
|
725
|
+
if isinstance(from_, exp.From):
|
|
726
|
+
relations.append(from_.this)
|
|
727
|
+
|
|
728
|
+
using = expression.args.get("using")
|
|
729
|
+
if isinstance(using, list):
|
|
730
|
+
relations.extend(using)
|
|
731
|
+
elif isinstance(using, exp.Expr):
|
|
732
|
+
relations.append(using)
|
|
733
|
+
|
|
734
|
+
for relation in relations:
|
|
735
|
+
if isinstance(relation, exp.Table):
|
|
736
|
+
yield from _traverse_scope(Scope(relation, cte_sources=scope.cte_sources))
|
|
737
|
+
|
|
708
738
|
for query in find_all_in_scope(expression, exp.Query):
|
|
709
739
|
# This check ensures we don't yield the CTE/nested queries twice
|
|
710
|
-
if
|
|
740
|
+
if isinstance(query.parent, (exp.CTE, exp.Subquery)):
|
|
741
|
+
continue
|
|
742
|
+
|
|
743
|
+
if _is_from_or_join(query):
|
|
744
|
+
parent = query.parent
|
|
745
|
+
if isinstance(parent, exp.Join) and isinstance(
|
|
746
|
+
parent.parent, (exp.Subquery, exp.Table)
|
|
747
|
+
):
|
|
748
|
+
# Scoped by the FROM-position relation (wrapper or table) it's joined to
|
|
749
|
+
continue
|
|
750
|
+
|
|
751
|
+
# A query in FROM/JOIN position (e.g. UPDATE ... FROM (SELECT ...) AS s) acts
|
|
752
|
+
# like a derived table, so its scope stays rooted at the Subquery wrapper to
|
|
753
|
+
# pick up the wrapper's alias, column list and joins
|
|
711
754
|
yield from _traverse_scope(Scope(query, cte_sources=scope.cte_sources))
|
|
755
|
+
else:
|
|
756
|
+
# Queries in value position (SET, WHERE, USING, ...) are scoped as subqueries,
|
|
757
|
+
# e.g. so their columns can be correlated to the DML's target table
|
|
758
|
+
yield from _traverse_scope(scope.branch(query, scope_type=ScopeType.SUBQUERY))
|
|
712
759
|
return
|
|
713
760
|
else:
|
|
714
761
|
logger.warning("Cannot traverse scope %s with type '%s'", expression, type(expression))
|
|
@@ -835,7 +882,10 @@ def _traverse_tables(scope: Scope) -> Iterator[Scope]:
|
|
|
835
882
|
for join in scope.expression.args.get("joins") or []:
|
|
836
883
|
expressions.append(join.this)
|
|
837
884
|
|
|
838
|
-
if isinstance(scope.expression, exp.Table):
|
|
885
|
+
if isinstance(scope.expression, (exp.Table, exp.Subquery)):
|
|
886
|
+
# A Subquery-rooted scope, e.g., the FROM clause of a DML statement, a DDL source or
|
|
887
|
+
# a parenthesized query like (SELECT ...) LIMIT 1, scopes its own inner query as a
|
|
888
|
+
# derived table
|
|
839
889
|
expressions.append(scope.expression)
|
|
840
890
|
|
|
841
891
|
expressions.extend(scope.expression.args.get("laterals") or [])
|
|
@@ -880,11 +930,14 @@ def _traverse_tables(scope: Scope) -> Iterator[Scope]:
|
|
|
880
930
|
lateral_sources = None
|
|
881
931
|
scope_type = ScopeType.DERIVED_TABLE
|
|
882
932
|
scopes = scope.derived_table_scopes
|
|
883
|
-
|
|
933
|
+
if node is not scope.expression:
|
|
934
|
+
# The scope expression's own joins were already added above
|
|
935
|
+
expressions.extend(join.this for join in node.args.get("joins") or [])
|
|
884
936
|
else:
|
|
885
937
|
# Makes sure we check for possible sources in nested table constructs
|
|
886
938
|
expressions.append(node.this)
|
|
887
|
-
|
|
939
|
+
if node is not scope.expression:
|
|
940
|
+
expressions.extend(join.this for join in node.args.get("joins") or [])
|
|
888
941
|
continue
|
|
889
942
|
|
|
890
943
|
child_scope: Scope | None = None
|
|
@@ -1662,7 +1662,11 @@ class Gen:
|
|
|
1662
1662
|
name = this.upper()
|
|
1663
1663
|
elif isinstance(this, exp.Identifier):
|
|
1664
1664
|
name = this.this
|
|
1665
|
-
|
|
1665
|
+
if this.quoted:
|
|
1666
|
+
escaped = name.replace('"', '""')
|
|
1667
|
+
name = f'"{escaped}"'
|
|
1668
|
+
else:
|
|
1669
|
+
name = name.upper()
|
|
1666
1670
|
else:
|
|
1667
1671
|
raise ValueError(
|
|
1668
1672
|
f"Anonymous.this expects a str or an Identifier, got '{this.__class__.__name__}'."
|
|
@@ -1729,7 +1733,11 @@ class Gen:
|
|
|
1729
1733
|
self._binary(e, " >= ")
|
|
1730
1734
|
|
|
1731
1735
|
def identifier_sql(self, e: exp.Identifier) -> None:
|
|
1732
|
-
|
|
1736
|
+
if e.quoted:
|
|
1737
|
+
escaped = e.this.replace('"', '""')
|
|
1738
|
+
self.stack.append(f'"{escaped}"')
|
|
1739
|
+
else:
|
|
1740
|
+
self.stack.append(e.this)
|
|
1733
1741
|
|
|
1734
1742
|
def ilike_sql(self, e: exp.ILike) -> None:
|
|
1735
1743
|
self._binary(e, " NOT ILIKE " if e.args.get("negate") else " ILIKE ")
|
|
@@ -1755,7 +1763,11 @@ class Gen:
|
|
|
1755
1763
|
self._binary(e, " NOT Like " if e.args.get("negate") else " Like ")
|
|
1756
1764
|
|
|
1757
1765
|
def literal_sql(self, e: exp.Literal) -> None:
|
|
1758
|
-
|
|
1766
|
+
if e.is_string:
|
|
1767
|
+
escaped = e.this.replace("'", "''")
|
|
1768
|
+
self.stack.append(f"'{escaped}'")
|
|
1769
|
+
else:
|
|
1770
|
+
self.stack.append(e.this)
|
|
1759
1771
|
|
|
1760
1772
|
def lt_sql(self, e: exp.LT) -> None:
|
|
1761
1773
|
self._binary(e, " < ")
|
|
@@ -1847,7 +1859,8 @@ class Gen:
|
|
|
1847
1859
|
v = node.args.get(k)
|
|
1848
1860
|
|
|
1849
1861
|
if v is not None:
|
|
1850
|
-
|
|
1862
|
+
# repr() plain strings so their content can't mimic gen's structural text
|
|
1863
|
+
kvs.append([f":{k}", repr(v) if isinstance(v, str) else v])
|
|
1851
1864
|
if kvs:
|
|
1852
1865
|
self.stack.append(kvs)
|
|
1853
1866
|
return True
|
|
@@ -6198,14 +6198,18 @@ class Parser:
|
|
|
6198
6198
|
|
|
6199
6199
|
self._retreat(index)
|
|
6200
6200
|
|
|
6201
|
+
unit_index = self._index
|
|
6201
6202
|
if interval_span_units_omitted:
|
|
6202
6203
|
unit = None
|
|
6203
6204
|
else:
|
|
6204
|
-
|
|
6205
|
-
|
|
6205
|
+
# Only attempt to parse a unit if the current token can actually be one, so that a
|
|
6206
|
+
# trailing operator isn't swallowed, e.g. INTERVAL '1 day' AND (x)
|
|
6207
|
+
is_unit = self._curr is not None and (
|
|
6206
6208
|
self._curr.token_type == TokenType.VAR
|
|
6207
6209
|
or self._curr.text.upper() in self.dialect.VALID_INTERVAL_UNITS
|
|
6208
|
-
)
|
|
6210
|
+
)
|
|
6211
|
+
unit = self._parse_function() if parse_function_unit and is_unit else None
|
|
6212
|
+
if not unit and is_unit:
|
|
6209
6213
|
unit = self._parse_var(any_token=True, upper=True)
|
|
6210
6214
|
|
|
6211
6215
|
# Most dialects support, e.g., the form INTERVAL '5' day, thus we try to parse
|
|
@@ -6220,7 +6224,7 @@ class Parser:
|
|
|
6220
6224
|
if parts and unit:
|
|
6221
6225
|
# Unconsume the eagerly-parsed unit, since the real unit was part of the string
|
|
6222
6226
|
unit = None
|
|
6223
|
-
self._retreat(
|
|
6227
|
+
self._retreat(unit_index)
|
|
6224
6228
|
|
|
6225
6229
|
if len(parts) == 1:
|
|
6226
6230
|
this = exp.Literal.string(parts[0][0])
|
|
@@ -7173,6 +7177,14 @@ class Parser:
|
|
|
7173
7177
|
def _parse_function_args(self, alias: bool = False) -> list[exp.Expr]:
|
|
7174
7178
|
return self._parse_csv(lambda: self._parse_lambda(alias=alias))
|
|
7175
7179
|
|
|
7180
|
+
def _parse_connector_function(self, connector: t.Callable[..., exp.Condition]) -> exp.Paren:
|
|
7181
|
+
args = self._parse_function_args(alias=False)
|
|
7182
|
+
if not args:
|
|
7183
|
+
self.raise_error("Expected at least one argument")
|
|
7184
|
+
|
|
7185
|
+
# Wrapped so the connector keeps its precedence in the parent context
|
|
7186
|
+
return exp.Paren(this=connector(*args, copy=False))
|
|
7187
|
+
|
|
7176
7188
|
def _parse_function_call(
|
|
7177
7189
|
self,
|
|
7178
7190
|
functions: dict[str, t.Callable] | None = None,
|
|
@@ -7499,10 +7511,18 @@ class Parser:
|
|
|
7499
7511
|
if (not kind and self._match(TokenType.ALIAS)) or self._match_texts(
|
|
7500
7512
|
("ALIAS", "MATERIALIZED")
|
|
7501
7513
|
):
|
|
7514
|
+
# Match storage before _parse_types so STORED is not treated as a data type
|
|
7515
|
+
# (needed for typeless columns, e.g. SQLite `b AS (a * 2) STORED`).
|
|
7502
7516
|
persisted = self._prev.text.upper() == "MATERIALIZED"
|
|
7517
|
+
expression = self._parse_disjunction()
|
|
7518
|
+
if not persisted:
|
|
7519
|
+
if self._match_text_seq("PERSISTED"):
|
|
7520
|
+
persisted = True
|
|
7521
|
+
elif self._match_texts(("STORED", "VIRTUAL")):
|
|
7522
|
+
persisted = self._prev.text.upper() == "STORED"
|
|
7503
7523
|
constraint_kind = exp.ComputedColumnConstraint(
|
|
7504
|
-
this=
|
|
7505
|
-
persisted=persisted
|
|
7524
|
+
this=expression,
|
|
7525
|
+
persisted=persisted,
|
|
7506
7526
|
data_type=exp.Var(this="AUTO")
|
|
7507
7527
|
if self._match_text_seq("AUTO")
|
|
7508
7528
|
else self._parse_types(),
|
|
@@ -382,8 +382,8 @@ class ClickHouseParser(parser.Parser):
|
|
|
382
382
|
"MEDIAN": lambda self: self._parse_quantile(),
|
|
383
383
|
"COLUMNS": lambda self: self._parse_columns(),
|
|
384
384
|
"TUPLE": lambda self: exp.Struct.from_arg_list(self._parse_function_args(alias=True)),
|
|
385
|
-
"AND": lambda self: exp.and_
|
|
386
|
-
"OR": lambda self: exp.or_
|
|
385
|
+
"AND": lambda self: self._parse_connector_function(exp.and_),
|
|
386
|
+
"OR": lambda self: self._parse_connector_function(exp.or_),
|
|
387
387
|
"XOR": lambda self: exp.xor(*self._parse_function_args(alias=False)),
|
|
388
388
|
}
|
|
389
389
|
|
|
@@ -8,7 +8,7 @@ from sqlglot.tokens import TokenType
|
|
|
8
8
|
from collections.abc import Collection
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def _select_all(table: exp.Expr) -> exp.Select | None:
|
|
11
|
+
def _select_all(table: exp.Expr | None) -> exp.Select | None:
|
|
12
12
|
return exp.select("*").from_(table, copy=False) if table else None
|
|
13
13
|
|
|
14
14
|
|
|
@@ -11,6 +11,7 @@ from sqlglot.dialects.dialect import (
|
|
|
11
11
|
from sqlglot.helper import ensure_list, seq_get
|
|
12
12
|
from sqlglot.parsers.hive import HiveParser
|
|
13
13
|
from sqlglot.parser import build_trim
|
|
14
|
+
from sqlglot.tokens import TokenType
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
def build_as_cast(to_type: str) -> t.Callable[[list], exp.Expr]:
|
|
@@ -22,6 +23,8 @@ class Spark2Parser(HiveParser):
|
|
|
22
23
|
CHANGE_COLUMN_ALTER_SYNTAX = True
|
|
23
24
|
PIVOT_COLUMN_NAMING = "agg_name_if_multiple"
|
|
24
25
|
|
|
26
|
+
FUNC_TOKENS = HiveParser.FUNC_TOKENS | {TokenType.AND, TokenType.OR}
|
|
27
|
+
|
|
25
28
|
FUNCTIONS = {
|
|
26
29
|
**HiveParser.FUNCTIONS,
|
|
27
30
|
"AGGREGATE": exp.Reduce.from_arg_list,
|
|
@@ -80,11 +83,13 @@ class Spark2Parser(HiveParser):
|
|
|
80
83
|
|
|
81
84
|
FUNCTION_PARSERS = {
|
|
82
85
|
**HiveParser.FUNCTION_PARSERS,
|
|
86
|
+
"AND": lambda self: self._parse_connector_function(exp.and_),
|
|
83
87
|
"APPROX_PERCENTILE": lambda self: self._parse_distinct_arg_function(exp.ApproxQuantile),
|
|
84
88
|
"BROADCAST": lambda self: self._parse_join_hint("BROADCAST"),
|
|
85
89
|
"BROADCASTJOIN": lambda self: self._parse_join_hint("BROADCASTJOIN"),
|
|
86
90
|
"MAPJOIN": lambda self: self._parse_join_hint("MAPJOIN"),
|
|
87
91
|
"MERGE": lambda self: self._parse_join_hint("MERGE"),
|
|
92
|
+
"OR": lambda self: self._parse_connector_function(exp.or_),
|
|
88
93
|
"SHUFFLEMERGE": lambda self: self._parse_join_hint("SHUFFLEMERGE"),
|
|
89
94
|
"MERGEJOIN": lambda self: self._parse_join_hint("MERGEJOIN"),
|
|
90
95
|
"SHUFFLE_HASH": lambda self: self._parse_join_hint("SHUFFLE_HASH"),
|
|
@@ -98,9 +98,7 @@ class SQLiteParser(parser.Parser):
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
def _parse_factor_operand(self) -> exp.Expr | None:
|
|
101
|
-
in_arithmetic_operand =
|
|
102
|
-
self._prev is not None and self._prev.token_type in self.ARITHMETIC_TOKENS
|
|
103
|
-
)
|
|
101
|
+
in_arithmetic_operand = self._prev.token_type in self.ARITHMETIC_TOKENS
|
|
104
102
|
this = self._parse_concat_operand()
|
|
105
103
|
parsed_op = False
|
|
106
104
|
|
|
@@ -178,3 +176,29 @@ class SQLiteParser(parser.Parser):
|
|
|
178
176
|
if is_attach
|
|
179
177
|
else self.expression(exp.Detach(this=this))
|
|
180
178
|
)
|
|
179
|
+
|
|
180
|
+
# https://www.sqlite.org/gencol.html
|
|
181
|
+
def _parse_generated_as_identity(
|
|
182
|
+
self,
|
|
183
|
+
) -> (
|
|
184
|
+
exp.GeneratedAsIdentityColumnConstraint
|
|
185
|
+
| exp.ComputedColumnConstraint
|
|
186
|
+
| exp.GeneratedAsRowColumnConstraint
|
|
187
|
+
):
|
|
188
|
+
this = super()._parse_generated_as_identity()
|
|
189
|
+
|
|
190
|
+
# Only expression-bearing GENERATED ALWAYS AS (expr) forms are computed
|
|
191
|
+
# columns. Match STORED/VIRTUAL inside this branch so true identity
|
|
192
|
+
# (AS IDENTITY) does not silently consume a trailing storage keyword.
|
|
193
|
+
if (
|
|
194
|
+
isinstance(this, exp.GeneratedAsIdentityColumnConstraint)
|
|
195
|
+
and this.expression is not None
|
|
196
|
+
):
|
|
197
|
+
persisted = (
|
|
198
|
+
self._match_texts(("STORED", "VIRTUAL")) and self._prev.text.upper() == "STORED"
|
|
199
|
+
)
|
|
200
|
+
return self.expression(
|
|
201
|
+
exp.ComputedColumnConstraint(this=this.expression, persisted=persisted)
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
return this
|
|
@@ -199,7 +199,37 @@ class TrinoParser(PrestoParser):
|
|
|
199
199
|
self._match_text_seq("CASE")
|
|
200
200
|
return self.expression(exp.CaseStatement(this=this, ifs=ifs, default=default))
|
|
201
201
|
|
|
202
|
+
def _parse_routine_while(self, label: exp.Expr | None = None) -> exp.WhileBlock:
|
|
203
|
+
# https://trino.io/docs/current/udf/sql/while.html
|
|
204
|
+
condition = self._parse_disjunction()
|
|
205
|
+
self._match_text_seq("DO")
|
|
206
|
+
body = self.expression(exp.Block(expressions=self._parse_routine_statements("END")))
|
|
207
|
+
self._match_text_seq("WHILE")
|
|
208
|
+
return self.expression(exp.WhileBlock(this=condition, body=body, label=label))
|
|
209
|
+
|
|
210
|
+
def _parse_routine_loop(self, label: exp.Expr | None = None) -> exp.LoopBlock:
|
|
211
|
+
# https://trino.io/docs/current/udf/sql/loop.html
|
|
212
|
+
body = self.expression(exp.Block(expressions=self._parse_routine_statements("END")))
|
|
213
|
+
self._match_text_seq("LOOP")
|
|
214
|
+
return self.expression(exp.LoopBlock(body=body, label=label))
|
|
215
|
+
|
|
216
|
+
def _parse_routine_repeat(self, label: exp.Expr | None = None) -> exp.RepeatBlock:
|
|
217
|
+
# https://trino.io/docs/current/udf/sql/repeat.html - unlike WHILE/LOOP,
|
|
218
|
+
# the body is evaluated at least once, with UNTIL <condition> tested after
|
|
219
|
+
body = self.expression(exp.Block(expressions=self._parse_routine_statements("UNTIL")))
|
|
220
|
+
until = self._parse_disjunction()
|
|
221
|
+
self._match_text_seq("END", "REPEAT")
|
|
222
|
+
return self.expression(exp.RepeatBlock(body=body, until=until, label=label))
|
|
223
|
+
|
|
202
224
|
def _parse_routine_statement(self) -> exp.Expr | None:
|
|
225
|
+
# An optional `label :` can precede WHILE, LOOP, or REPEAT to name the block for ITERATE/LEAVE.
|
|
226
|
+
# Any non-reserved keyword (SET, IF, ITERATE etc) is a valid label name, so the colon lookahead
|
|
227
|
+
# has to run first, before the statement keywords are matched.
|
|
228
|
+
label = None
|
|
229
|
+
if self._next and self._next.token_type == TokenType.COLON:
|
|
230
|
+
label = self._parse_id_var()
|
|
231
|
+
self._match(TokenType.COLON)
|
|
232
|
+
|
|
203
233
|
if self._match(TokenType.BEGIN, advance=False):
|
|
204
234
|
return self._parse_routine_block()
|
|
205
235
|
|
|
@@ -218,5 +248,20 @@ class TrinoParser(PrestoParser):
|
|
|
218
248
|
if self._match(TokenType.SET):
|
|
219
249
|
return self._parse_set()
|
|
220
250
|
|
|
251
|
+
if self._match_text_seq("ITERATE"):
|
|
252
|
+
return self.expression(exp.Iterate(this=self._parse_id_var()))
|
|
253
|
+
|
|
254
|
+
if self._match_text_seq("LEAVE"):
|
|
255
|
+
return self.expression(exp.Leave(this=self._parse_id_var()))
|
|
256
|
+
|
|
257
|
+
if self._match_text_seq("WHILE"):
|
|
258
|
+
return self._parse_routine_while(label=label)
|
|
259
|
+
|
|
260
|
+
if self._match_text_seq("LOOP"):
|
|
261
|
+
return self._parse_routine_loop(label=label)
|
|
262
|
+
|
|
263
|
+
if self._match_text_seq("REPEAT"):
|
|
264
|
+
return self._parse_routine_repeat(label=label)
|
|
265
|
+
|
|
221
266
|
self.raise_error("Expected routine statement")
|
|
222
267
|
return None
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlglotc
|
|
3
|
-
Version: 30.
|
|
3
|
+
Version: 30.17.0
|
|
4
4
|
Summary: mypyc-compiled extensions for sqlglot
|
|
5
5
|
Author-email: Toby Mao <toby.mao@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://sqlglot.com/
|
|
8
8
|
Project-URL: Repository, https://github.com/tobymao/sqlglot
|
|
9
9
|
Requires-Python: >=3.10
|
|
10
|
-
Requires-Dist: sqlglot==30.
|
|
10
|
+
Requires-Dist: sqlglot==30.17.0
|
|
11
11
|
Provides-Extra: dev
|
|
12
12
|
Requires-Dist: setuptools>=61.0; extra == "dev"
|
|
13
13
|
Requires-Dist: setuptools_scm; extra == "dev"
|
|
14
|
-
Requires-Dist: sqlglot-mypy>=2.
|
|
14
|
+
Requires-Dist: sqlglot-mypy>=2.3.0.post1; extra == "dev"
|
|
15
15
|
Dynamic: requires-dist
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|