sqlglotc 30.10.0__tar.gz → 30.11.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.10.0/sqlglotc.egg-info → sqlglotc-30.11.0}/PKG-INFO +3 -1
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/pyproject.toml +1 -1
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/setup.py +33 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/expressions/builders.py +6 -1
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/expressions/core.py +8 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generator.py +2 -2
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/bigquery.py +3 -3
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/databricks.py +10 -12
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/tsql.py +9 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/lineage.py +124 -20
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/optimizer/annotate_types.py +4 -4
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/optimizer/normalize_identifiers.py +4 -3
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/optimizer/qualify_columns.py +23 -11
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/optimizer/resolver.py +6 -4
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/optimizer/scope.py +32 -8
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/optimizer/simplify.py +19 -12
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/bigquery.py +4 -4
- {sqlglotc-30.10.0 → sqlglotc-30.11.0/sqlglotc.egg-info}/PKG-INFO +3 -1
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglotc.egg-info/requires.txt +1 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/MANIFEST.in +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/setup.cfg +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/errors.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/executor/table.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/expressions/aggregate.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/expressions/array.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/expressions/constraints.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/expressions/datatypes.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/expressions/ddl.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/expressions/dml.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/expressions/functions.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/expressions/json.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/expressions/math.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/expressions/properties.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/expressions/query.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/expressions/string.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/expressions/temporal.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/athena.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/clickhouse.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/doris.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/dremio.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/drill.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/druid.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/duckdb.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/dune.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/exasol.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/fabric.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/hive.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/materialize.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/mysql.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/oracle.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/postgres.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/presto.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/prql.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/python.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/redshift.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/risingwave.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/singlestore.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/snowflake.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/solr.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/spark.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/spark2.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/sqlite.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/starrocks.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/tableau.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/teradata.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/generators/trino.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/helper.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/optimizer/canonicalize_internal_names.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/optimizer/isolate_table_selects.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/optimizer/qualify.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/optimizer/qualify_tables.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parser.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/athena.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/base.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/clickhouse.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/databricks.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/doris.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/dremio.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/drill.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/druid.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/duckdb.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/dune.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/exasol.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/fabric.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/hive.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/materialize.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/mysql.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/oracle.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/postgres.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/presto.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/prql.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/redshift.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/risingwave.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/singlestore.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/snowflake.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/solr.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/spark.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/spark2.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/sqlite.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/starrocks.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/tableau.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/teradata.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/trino.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/parsers/tsql.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/schema.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/serde.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/time.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/tokenizer_core.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglot/trie.py +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglotc.egg-info/SOURCES.txt +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglotc.egg-info/dependency_links.txt +0 -0
- {sqlglotc-30.10.0 → sqlglotc-30.11.0}/sqlglotc.egg-info/top_level.txt +0 -0
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlglotc
|
|
3
|
-
Version: 30.
|
|
3
|
+
Version: 30.11.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.11.0
|
|
10
11
|
Provides-Extra: dev
|
|
11
12
|
Requires-Dist: setuptools>=61.0; extra == "dev"
|
|
12
13
|
Requires-Dist: setuptools_scm; extra == "dev"
|
|
13
14
|
Requires-Dist: sqlglot-mypy>=2.1.0.post2; extra == "dev"
|
|
15
|
+
Dynamic: requires-dist
|
|
@@ -128,9 +128,42 @@ class sdist(_sdist):
|
|
|
128
128
|
shutil.rmtree(local_sqlglot, ignore_errors=True)
|
|
129
129
|
|
|
130
130
|
|
|
131
|
+
def _sqlglot_requirement():
|
|
132
|
+
"""Pin sqlglot to the matching version so resolvers keep the two packages in lockstep."""
|
|
133
|
+
try:
|
|
134
|
+
pkg_info = os.path.join(os.path.dirname(os.path.abspath(__file__)), "PKG-INFO")
|
|
135
|
+
if os.path.isfile(pkg_info):
|
|
136
|
+
# Building from an sdist: no git metadata; the released version is frozen in PKG-INFO.
|
|
137
|
+
with open(pkg_info, encoding="utf-8") as fd:
|
|
138
|
+
version = next(
|
|
139
|
+
(line.split(":", 1)[1].strip() for line in fd if line.startswith("Version:")),
|
|
140
|
+
"",
|
|
141
|
+
)
|
|
142
|
+
else:
|
|
143
|
+
from setuptools_scm import get_version
|
|
144
|
+
|
|
145
|
+
version = get_version(
|
|
146
|
+
root="..",
|
|
147
|
+
relative_to=__file__,
|
|
148
|
+
local_scheme="no-local-version",
|
|
149
|
+
fallback_version="0.0.0",
|
|
150
|
+
)
|
|
151
|
+
except Exception as e:
|
|
152
|
+
print(f"sqlglotc: failed to determine the sqlglot version to pin: {e}")
|
|
153
|
+
version = ""
|
|
154
|
+
|
|
155
|
+
# Dev and fallback (no git metadata) builds have no matching sqlglot release on PyPI;
|
|
156
|
+
# depend on sqlglot unpinned since the extensions can't be imported without it.
|
|
157
|
+
if version in ("", "0.0.0") or ".dev" in version:
|
|
158
|
+
return ["sqlglot"]
|
|
159
|
+
|
|
160
|
+
return [f"sqlglot=={version}"]
|
|
161
|
+
|
|
162
|
+
|
|
131
163
|
setup(
|
|
132
164
|
name="sqlglotc",
|
|
133
165
|
packages=[],
|
|
166
|
+
install_requires=_sqlglot_requirement(),
|
|
134
167
|
ext_modules=mypycify(
|
|
135
168
|
_source_paths(), opt_level=os.environ.get("MYPYC_OPT", "2"), separate=True, verbose=True
|
|
136
169
|
),
|
|
@@ -319,6 +319,11 @@ def parse_identifier(name: str | Identifier, dialect: DialectType = None) -> Ide
|
|
|
319
319
|
Returns:
|
|
320
320
|
The identifier ast node.
|
|
321
321
|
"""
|
|
322
|
+
if isinstance(name, str) and SAFE_IDENTIFIER_RE.match(name):
|
|
323
|
+
# Simple names parse to a single unquoted identifier in all dialects, so we can
|
|
324
|
+
# avoid the tokenizer/parser round-trip for them.
|
|
325
|
+
return Identifier(this=name, quoted=False)
|
|
326
|
+
|
|
322
327
|
try:
|
|
323
328
|
expression = maybe_parse(name, dialect=dialect, into=Identifier)
|
|
324
329
|
except (ParseError, TokenError):
|
|
@@ -822,7 +827,7 @@ def replace_tables(
|
|
|
822
827
|
mapping = {normalize_table_name(k, dialect=dialect): v for k, v in mapping.items()}
|
|
823
828
|
|
|
824
829
|
def _replace_tables(node: Expr) -> Expr:
|
|
825
|
-
if isinstance(node, Table) and node.
|
|
830
|
+
if isinstance(node, Table) and node.meta_get("replace") is not False:
|
|
826
831
|
original = normalize_table_name(node, dialect=dialect)
|
|
827
832
|
new_name = mapping.get(original)
|
|
828
833
|
|
|
@@ -240,6 +240,9 @@ class Expr:
|
|
|
240
240
|
def meta(self) -> dict[str, t.Any]:
|
|
241
241
|
raise NotImplementedError
|
|
242
242
|
|
|
243
|
+
def meta_get(self, key: str, default: t.Any = None) -> t.Any:
|
|
244
|
+
raise NotImplementedError
|
|
245
|
+
|
|
243
246
|
def __deepcopy__(self, memo: t.Any) -> Expr:
|
|
244
247
|
raise NotImplementedError
|
|
245
248
|
|
|
@@ -990,6 +993,11 @@ class Expression(Expr):
|
|
|
990
993
|
self._meta = {}
|
|
991
994
|
return self._meta
|
|
992
995
|
|
|
996
|
+
def meta_get(self, key: str, default: t.Any = None) -> t.Any:
|
|
997
|
+
"""Reads a meta value without allocating the meta dict (unlike the `meta` property)."""
|
|
998
|
+
meta = self._meta
|
|
999
|
+
return meta.get(key, default) if meta is not None else default
|
|
1000
|
+
|
|
993
1001
|
def __deepcopy__(self, memo: t.Any) -> Expr:
|
|
994
1002
|
root = self.__class__()
|
|
995
1003
|
stack: list[tuple[Expr, Expr]] = [(self, root)]
|
|
@@ -4554,7 +4554,7 @@ class Generator:
|
|
|
4554
4554
|
args.append(arg_value)
|
|
4555
4555
|
|
|
4556
4556
|
if self.dialect.PRESERVE_ORIGINAL_NAMES:
|
|
4557
|
-
name =
|
|
4557
|
+
name = expression.meta_get("name") or expression.sql_name()
|
|
4558
4558
|
else:
|
|
4559
4559
|
name = expression.sql_name()
|
|
4560
4560
|
|
|
@@ -5258,7 +5258,7 @@ class Generator:
|
|
|
5258
5258
|
)
|
|
5259
5259
|
return self.sql(this)
|
|
5260
5260
|
|
|
5261
|
-
if self.IGNORE_NULLS_IN_FUNC and not expression.
|
|
5261
|
+
if self.IGNORE_NULLS_IN_FUNC and not expression.meta_get("inline"):
|
|
5262
5262
|
if self.IGNORE_NULLS_BEFORE_ORDER:
|
|
5263
5263
|
# The first modifier here will be the one closest to the AggFunc's arg
|
|
5264
5264
|
mods = sorted(
|
|
@@ -201,7 +201,7 @@ def _levenshtein_sql(self: BigQueryGenerator, expression: exp.Levenshtein) -> st
|
|
|
201
201
|
|
|
202
202
|
|
|
203
203
|
def _json_extract_sql(self: BigQueryGenerator, expression: JSON_EXTRACT_TYPE) -> str:
|
|
204
|
-
name =
|
|
204
|
+
name = expression.meta_get("name") or expression.sql_name()
|
|
205
205
|
upper = name.upper()
|
|
206
206
|
|
|
207
207
|
dquote_escaping = upper in DQUOTES_ESCAPING_JSON_FUNCTIONS
|
|
@@ -565,7 +565,7 @@ class BigQueryGenerator(generator.Generator):
|
|
|
565
565
|
)
|
|
566
566
|
|
|
567
567
|
def column_parts(self, expression: exp.Column) -> str:
|
|
568
|
-
if expression.
|
|
568
|
+
if expression.meta_get("quoted_column"):
|
|
569
569
|
# If a column reference is of the form `dataset.table`.name, we need
|
|
570
570
|
# to preserve the quoted table path, otherwise the reference breaks
|
|
571
571
|
table_parts = ".".join(p.name for p in expression.parts[:-1])
|
|
@@ -583,7 +583,7 @@ class BigQueryGenerator(generator.Generator):
|
|
|
583
583
|
#
|
|
584
584
|
# - WITH x AS (SELECT [1, 2] AS y) SELECT * FROM x, `x.y` -> cross join
|
|
585
585
|
# - WITH x AS (SELECT [1, 2] AS y) SELECT * FROM x, `x`.`y` -> implicit unnest
|
|
586
|
-
if expression.
|
|
586
|
+
if expression.meta_get("quoted_table"):
|
|
587
587
|
table_parts = ".".join(p.name for p in expression.parts)
|
|
588
588
|
return self.sql(exp.Identifier(this=table_parts, quoted=True))
|
|
589
589
|
|
|
@@ -10,14 +10,6 @@ from sqlglot.dialects.dialect import (
|
|
|
10
10
|
from sqlglot.generators.spark import SparkGenerator
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def _jsonextract_sql(
|
|
14
|
-
self: DatabricksGenerator, expression: exp.JSONExtract | exp.JSONExtractScalar
|
|
15
|
-
) -> str:
|
|
16
|
-
this = self.sql(expression, "this")
|
|
17
|
-
expr = self.sql(expression, "expression")
|
|
18
|
-
return f"{this}:{expr}"
|
|
19
|
-
|
|
20
|
-
|
|
21
13
|
class DatabricksGenerator(SparkGenerator):
|
|
22
14
|
TABLESAMPLE_SEED_KEYWORD = "REPEATABLE"
|
|
23
15
|
COPY_PARAMS_ARE_WRAPPED = False
|
|
@@ -52,9 +44,10 @@ class DatabricksGenerator(SparkGenerator):
|
|
|
52
44
|
transforms.any_to_exists,
|
|
53
45
|
]
|
|
54
46
|
),
|
|
55
|
-
exp.JSONExtract:
|
|
56
|
-
exp.
|
|
57
|
-
|
|
47
|
+
exp.JSONExtract: lambda self, e: f"{self.sql(e, 'this')}:{self.sql(e, 'expression')}",
|
|
48
|
+
exp.JSONPathRoot: lambda self, e: (
|
|
49
|
+
"$" if isinstance(e.parent and e.parent.parent, exp.JSONExtractScalar) else ""
|
|
50
|
+
),
|
|
58
51
|
exp.ToChar: lambda self, e: (
|
|
59
52
|
self.cast_sql(exp.Cast(this=e.this, to=exp.DataType(this="STRING")))
|
|
60
53
|
if e.args.get("is_numeric")
|
|
@@ -98,7 +91,12 @@ class DatabricksGenerator(SparkGenerator):
|
|
|
98
91
|
|
|
99
92
|
def jsonpath_sql(self, expression: exp.JSONPath) -> str:
|
|
100
93
|
expression.set("escape", None)
|
|
101
|
-
|
|
94
|
+
path = super().jsonpath_sql(expression)
|
|
95
|
+
|
|
96
|
+
if isinstance(expression.parent, exp.JSONExtractScalar):
|
|
97
|
+
return f"{self.dialect.QUOTE_START}{path}{self.dialect.QUOTE_END}"
|
|
98
|
+
|
|
99
|
+
return path
|
|
102
100
|
|
|
103
101
|
def uniform_sql(self, expression: exp.Uniform) -> str:
|
|
104
102
|
gen = expression.args.get("gen")
|
|
@@ -417,6 +417,15 @@ class TSQLGenerator(generator.Generator):
|
|
|
417
417
|
# but CREATE VIEW actually requires the WITH clause to come after it so we need
|
|
418
418
|
# to amend the AST by moving the CTEs to the CREATE VIEW statement's query.
|
|
419
419
|
ctas_expression.set("with_", with_.pop())
|
|
420
|
+
elif (
|
|
421
|
+
kind == "FUNCTION"
|
|
422
|
+
and isinstance(ctas_expression, exp.Return)
|
|
423
|
+
and isinstance(body := ctas_expression.this.unnest(), exp.Query)
|
|
424
|
+
and (with_ := expression.args.get("with_"))
|
|
425
|
+
):
|
|
426
|
+
# Similar to the VIEW branch, the table-valued functions require the WITH clause
|
|
427
|
+
# to stay inside the RETURN body, so we move back any CTEs that were bubbled up.
|
|
428
|
+
body.set("with_", with_.pop())
|
|
420
429
|
|
|
421
430
|
table = expression.find(exp.Table)
|
|
422
431
|
|
|
@@ -9,6 +9,7 @@ from sqlglot import Schema, exp, maybe_parse
|
|
|
9
9
|
from sqlglot.errors import SqlglotError
|
|
10
10
|
from sqlglot.optimizer import Scope, build_scope, find_all_in_scope, normalize_identifiers, qualify
|
|
11
11
|
from sqlglot.optimizer.scope import ScopeType
|
|
12
|
+
from sqlglot.schema import ensure_schema
|
|
12
13
|
|
|
13
14
|
if t.TYPE_CHECKING:
|
|
14
15
|
from sqlglot.dialects.dialect import DialectType
|
|
@@ -133,6 +134,8 @@ def lineage(
|
|
|
133
134
|
copy=copy,
|
|
134
135
|
)
|
|
135
136
|
|
|
137
|
+
schema = ensure_schema(schema, dialect=dialect)
|
|
138
|
+
|
|
136
139
|
if not scope:
|
|
137
140
|
expression = qualify.qualify(
|
|
138
141
|
expression,
|
|
@@ -162,6 +165,7 @@ def lineage(
|
|
|
162
165
|
scope,
|
|
163
166
|
dialect,
|
|
164
167
|
trim_selects=trim_selects,
|
|
168
|
+
schema=schema,
|
|
165
169
|
_cache=cache,
|
|
166
170
|
_scope_meta=scope_meta,
|
|
167
171
|
on_node=on_node,
|
|
@@ -180,6 +184,7 @@ def lineage(
|
|
|
180
184
|
scope,
|
|
181
185
|
dialect,
|
|
182
186
|
trim_selects=trim_selects,
|
|
187
|
+
schema=schema,
|
|
183
188
|
_cache=cache,
|
|
184
189
|
_scope_meta=scope_meta,
|
|
185
190
|
on_node=on_node,
|
|
@@ -197,6 +202,7 @@ def to_node(
|
|
|
197
202
|
source_name: str | None = None,
|
|
198
203
|
reference_node_name: str | None = None,
|
|
199
204
|
trim_selects: bool = True,
|
|
205
|
+
schema: Schema | None = None,
|
|
200
206
|
_cache: dict[tuple, Node] | None = None,
|
|
201
207
|
_scope_meta: dict[int, tuple[bool, dict[str, exp.Expr]]] | None = None,
|
|
202
208
|
on_node: t.Callable[[Node], None] | None = None,
|
|
@@ -248,6 +254,7 @@ def to_node(
|
|
|
248
254
|
source_name=source_name,
|
|
249
255
|
reference_node_name=reference_node_name,
|
|
250
256
|
trim_selects=trim_selects,
|
|
257
|
+
schema=schema,
|
|
251
258
|
_cache=_cache,
|
|
252
259
|
_scope_meta=_scope_meta,
|
|
253
260
|
on_node=on_node,
|
|
@@ -288,6 +295,7 @@ def to_node(
|
|
|
288
295
|
source_name=source_name,
|
|
289
296
|
reference_node_name=reference_node_name,
|
|
290
297
|
trim_selects=trim_selects,
|
|
298
|
+
schema=schema,
|
|
291
299
|
_cache=_cache,
|
|
292
300
|
_scope_meta=_scope_meta,
|
|
293
301
|
on_node=on_node,
|
|
@@ -337,6 +345,7 @@ def to_node(
|
|
|
337
345
|
dialect=dialect,
|
|
338
346
|
upstream=node,
|
|
339
347
|
trim_selects=trim_selects,
|
|
348
|
+
schema=schema,
|
|
340
349
|
_cache=_cache,
|
|
341
350
|
_scope_meta=_scope_meta,
|
|
342
351
|
on_node=on_node,
|
|
@@ -372,25 +381,19 @@ def to_node(
|
|
|
372
381
|
}
|
|
373
382
|
|
|
374
383
|
pivots = scope.pivots
|
|
375
|
-
pivot = pivots[0] if len(pivots) == 1
|
|
384
|
+
pivot = pivots[0] if len(pivots) == 1 else None
|
|
385
|
+
pivot_renames: dict[str, str] = {}
|
|
386
|
+
pivot_column_mapping: dict[str, list[exp.Column]] = {}
|
|
387
|
+
|
|
376
388
|
if pivot:
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
# Example: PIVOT (SUM(value) AS value_sum, MAX(price)) FOR category IN ('a' AS cat_a, 'b')
|
|
386
|
-
pivot_columns = pivot.args["columns"]
|
|
387
|
-
pivot_aggs_count = len(pivot.expressions)
|
|
388
|
-
|
|
389
|
-
pivot_column_mapping = {}
|
|
390
|
-
for i, agg in enumerate(pivot.expressions):
|
|
391
|
-
agg_cols = list(agg.find_all(exp.Column))
|
|
392
|
-
for col_index in range(i, len(pivot_columns), pivot_aggs_count):
|
|
393
|
-
pivot_column_mapping[pivot_columns[col_index].name] = agg_cols
|
|
389
|
+
pivot_renames = _pivot_output_renames(pivot, scope, schema)
|
|
390
|
+
pivot_column_mapping = _pivot_column_mapping(pivot)
|
|
391
|
+
if pivot_renames:
|
|
392
|
+
pivot_column_mapping = {
|
|
393
|
+
post: pivot_column_mapping[pre]
|
|
394
|
+
for post, pre in pivot_renames.items()
|
|
395
|
+
if pre in pivot_column_mapping
|
|
396
|
+
}
|
|
394
397
|
|
|
395
398
|
for c in source_columns:
|
|
396
399
|
table = c.table
|
|
@@ -414,6 +417,7 @@ def to_node(
|
|
|
414
417
|
source_name=source_names.get(table) or source_name,
|
|
415
418
|
reference_node_name=reference_node_name,
|
|
416
419
|
trim_selects=trim_selects,
|
|
420
|
+
schema=schema,
|
|
417
421
|
_cache=_cache,
|
|
418
422
|
_scope_meta=_scope_meta,
|
|
419
423
|
on_node=on_node,
|
|
@@ -422,19 +426,35 @@ def to_node(
|
|
|
422
426
|
downstream_columns = []
|
|
423
427
|
|
|
424
428
|
column_name = c.name
|
|
425
|
-
if
|
|
429
|
+
if column_name in pivot_column_mapping:
|
|
426
430
|
downstream_columns.extend(pivot_column_mapping[column_name])
|
|
427
431
|
else:
|
|
428
432
|
# The column is not in the pivot, so it must be an implicit column of the
|
|
429
433
|
# pivoted source -- adapt column to be from the implicit pivoted source.
|
|
430
434
|
pivot_parent = pivot.parent
|
|
431
435
|
downstream_columns.append(
|
|
432
|
-
exp.column(
|
|
436
|
+
exp.column(
|
|
437
|
+
pivot_renames.get(c.name, c.this),
|
|
438
|
+
table=pivot_parent.alias_or_name if pivot_parent else None,
|
|
439
|
+
)
|
|
433
440
|
)
|
|
434
441
|
|
|
435
442
|
for downstream_column in downstream_columns:
|
|
443
|
+
if not downstream_column.table:
|
|
444
|
+
# Some dialects (e.g. bigquery) don't qualify the IN-list columns,
|
|
445
|
+
# but they can only come from the pivoted source
|
|
446
|
+
pivot_parent = pivot.parent
|
|
447
|
+
downstream_column = exp.column(
|
|
448
|
+
downstream_column.this,
|
|
449
|
+
table=pivot_parent.alias_or_name if pivot_parent else None,
|
|
450
|
+
)
|
|
451
|
+
|
|
436
452
|
table = downstream_column.table
|
|
437
453
|
col_source = scope.sources.get(table)
|
|
454
|
+
if isinstance(col_source, exp.Table) and not col_source.db:
|
|
455
|
+
# A pivoted CTE reference maps to the raw table in `scope.sources`,
|
|
456
|
+
# so recover the CTE's scope to keep tracing through it
|
|
457
|
+
col_source = scope.cte_sources.get(col_source.name, col_source)
|
|
438
458
|
if isinstance(col_source, Scope):
|
|
439
459
|
to_node(
|
|
440
460
|
downstream_column.name,
|
|
@@ -445,6 +465,7 @@ def to_node(
|
|
|
445
465
|
source_name=source_names.get(table) or source_name,
|
|
446
466
|
reference_node_name=reference_node_name,
|
|
447
467
|
trim_selects=trim_selects,
|
|
468
|
+
schema=schema,
|
|
448
469
|
_cache=_cache,
|
|
449
470
|
_scope_meta=_scope_meta,
|
|
450
471
|
on_node=on_node,
|
|
@@ -479,6 +500,89 @@ def to_node(
|
|
|
479
500
|
return node
|
|
480
501
|
|
|
481
502
|
|
|
503
|
+
def _pivot_output_renames(
|
|
504
|
+
pivot: exp.Pivot, scope: Scope, schema: Schema | None = None
|
|
505
|
+
) -> dict[str, str]:
|
|
506
|
+
"""
|
|
507
|
+
Map each (UN)PIVOT output column name to its pre-rename name, when an alias column
|
|
508
|
+
list (`... AS t(c1, c2, ...)`) renames the outputs. The renames are positional over
|
|
509
|
+
the operator's full output, so they can only be aligned when the pre-pivot columns
|
|
510
|
+
are known: from the projections of a derived table or CTE source, or from the
|
|
511
|
+
schema for a physical table.
|
|
512
|
+
"""
|
|
513
|
+
if not pivot.alias_column_names:
|
|
514
|
+
return {}
|
|
515
|
+
|
|
516
|
+
parent = pivot.parent
|
|
517
|
+
pre_pivot_columns: list[str] = []
|
|
518
|
+
if isinstance(parent, exp.DerivedTable) and isinstance(parent.this, exp.Query):
|
|
519
|
+
pre_pivot_columns = parent.this.named_selects
|
|
520
|
+
elif isinstance(parent, exp.Table):
|
|
521
|
+
cte_source = scope.cte_sources.get(parent.name) if not parent.db else None
|
|
522
|
+
if isinstance(cte_source, Scope) and isinstance(cte_source.expression, exp.Query):
|
|
523
|
+
pre_pivot_columns = cte_source.expression.named_selects
|
|
524
|
+
elif schema is not None:
|
|
525
|
+
pre_pivot_columns = list(schema.column_names(parent, only_visible=True))
|
|
526
|
+
|
|
527
|
+
# The alignment is also unknowable when the source's projections aren't fully
|
|
528
|
+
# expanded (e.g. an unresolved star), since the renames would silently shift
|
|
529
|
+
if not pre_pivot_columns or "*" in pre_pivot_columns:
|
|
530
|
+
return {}
|
|
531
|
+
|
|
532
|
+
return pivot.output_columns(pre_pivot_columns)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _pivot_column_mapping(pivot: exp.Pivot) -> dict[str, list[exp.Column]]:
|
|
536
|
+
"""Map each (UN)PIVOT output column name to the source columns it's derived from."""
|
|
537
|
+
mapping: dict[str, list[exp.Column]] = {}
|
|
538
|
+
|
|
539
|
+
if pivot.unpivot:
|
|
540
|
+
# UNPIVOT((v1, v2) FOR name IN ((a1, a2), (b1, b2))): each value column is derived
|
|
541
|
+
# positionally from the IN-list entries, and the name column from all of them
|
|
542
|
+
value_columns = [
|
|
543
|
+
identifier for e in pivot.expressions for identifier in e.find_all(exp.Identifier)
|
|
544
|
+
]
|
|
545
|
+
for value_column in value_columns:
|
|
546
|
+
mapping[value_column.name] = []
|
|
547
|
+
|
|
548
|
+
for field in pivot.fields:
|
|
549
|
+
if not isinstance(field, exp.In):
|
|
550
|
+
continue
|
|
551
|
+
|
|
552
|
+
name_columns = mapping.setdefault(field.this.name, [])
|
|
553
|
+
for entry in field.expressions:
|
|
554
|
+
entry_columns = list(entry.find_all(exp.Column))
|
|
555
|
+
name_columns.extend(entry_columns)
|
|
556
|
+
|
|
557
|
+
if len(entry_columns) == len(value_columns):
|
|
558
|
+
for value_column, column in zip(value_columns, entry_columns):
|
|
559
|
+
mapping[value_column.name].append(column)
|
|
560
|
+
else:
|
|
561
|
+
for value_column in value_columns:
|
|
562
|
+
mapping[value_column.name].extend(entry_columns)
|
|
563
|
+
|
|
564
|
+
return mapping
|
|
565
|
+
|
|
566
|
+
# For each aggregation function, the pivot creates a new column for each field in category
|
|
567
|
+
# combined with the aggfunc. So the columns parsed have this order: cat_a_value_sum, cat_a,
|
|
568
|
+
# b_value_sum, b. Because of this step wise manner the aggfunc 'sum(value) as value_sum'
|
|
569
|
+
# belongs to the column indices 0, 2, and the aggfunc 'max(price)' without an alias belongs
|
|
570
|
+
# to the column indices 1, 3. Here, only the columns used in the aggregations are of interest
|
|
571
|
+
# in the lineage, so lookup the pivot column name by index and map that with the columns used
|
|
572
|
+
# in the aggregation.
|
|
573
|
+
#
|
|
574
|
+
# Example: PIVOT (SUM(value) AS value_sum, MAX(price)) FOR category IN ('a' AS cat_a, 'b')
|
|
575
|
+
pivot_columns = pivot.args["columns"]
|
|
576
|
+
pivot_aggs_count = len(pivot.expressions)
|
|
577
|
+
|
|
578
|
+
mapping = {}
|
|
579
|
+
for i, agg in enumerate(pivot.expressions):
|
|
580
|
+
agg_cols = list(agg.find_all(exp.Column))
|
|
581
|
+
for col_index in range(i, len(pivot_columns), pivot_aggs_count):
|
|
582
|
+
mapping[pivot_columns[col_index].name] = agg_cols
|
|
583
|
+
return mapping
|
|
584
|
+
|
|
585
|
+
|
|
482
586
|
class GraphHTML:
|
|
483
587
|
"""Node to HTML generator using vis.js.
|
|
484
588
|
|
|
@@ -400,7 +400,7 @@ class TypeAnnotator:
|
|
|
400
400
|
isinstance(source, Scope)
|
|
401
401
|
and isinstance(source.expression, exp.Query)
|
|
402
402
|
and (
|
|
403
|
-
source.expression.
|
|
403
|
+
source.expression.meta_get("query_type") or exp.DType.UNKNOWN.into_expr()
|
|
404
404
|
).is_type(exp.DType.STRUCT)
|
|
405
405
|
):
|
|
406
406
|
self._set_type(table_column, source.expression.meta["query_type"])
|
|
@@ -497,7 +497,7 @@ class TypeAnnotator:
|
|
|
497
497
|
else:
|
|
498
498
|
self._set_type(expr, exp.DType.UNKNOWN)
|
|
499
499
|
|
|
500
|
-
if expr.is_type(exp.DType.JSON) and (dot_parts := expr.
|
|
500
|
+
if expr.is_type(exp.DType.JSON) and (dot_parts := expr.meta_get("dot_parts")):
|
|
501
501
|
# JSON dot access is case sensitive across all dialects, so we need to undo the normalization.
|
|
502
502
|
i = iter(dot_parts)
|
|
503
503
|
parent = expr.parent
|
|
@@ -740,7 +740,7 @@ class TypeAnnotator:
|
|
|
740
740
|
self._annotate_by_args(expression, left, right)
|
|
741
741
|
|
|
742
742
|
if isinstance(expression, exp.Is) or (
|
|
743
|
-
left.
|
|
743
|
+
left.meta_get("nonnull") is True and right.meta_get("nonnull") is True
|
|
744
744
|
):
|
|
745
745
|
expression.meta["nonnull"] = True
|
|
746
746
|
|
|
@@ -752,7 +752,7 @@ class TypeAnnotator:
|
|
|
752
752
|
else:
|
|
753
753
|
self._set_type(expression, expression.this.type)
|
|
754
754
|
|
|
755
|
-
if expression.this.
|
|
755
|
+
if expression.this.meta_get("nonnull") is True:
|
|
756
756
|
expression.meta["nonnull"] = True
|
|
757
757
|
|
|
758
758
|
return expression
|
|
@@ -63,8 +63,8 @@ def normalize_identifiers(expression, dialect=None, store_original_column_identi
|
|
|
63
63
|
if isinstance(expression, str):
|
|
64
64
|
expression = exp.parse_identifier(expression, dialect=dialect)
|
|
65
65
|
|
|
66
|
-
for node in expression.walk(prune=lambda n: bool(n.
|
|
67
|
-
if not node.
|
|
66
|
+
for node in expression.walk(prune=lambda n: bool(n.meta_get("case_sensitive"))):
|
|
67
|
+
if not node.meta_get("case_sensitive"):
|
|
68
68
|
if store_original_column_identifiers and isinstance(node, exp.Column):
|
|
69
69
|
# TODO: This does not handle non-column cases, e.g PARSE_JSON(...).key
|
|
70
70
|
parent = node
|
|
@@ -73,6 +73,7 @@ def normalize_identifiers(expression, dialect=None, store_original_column_identi
|
|
|
73
73
|
|
|
74
74
|
node.meta["dot_parts"] = [p.name for p in parent.parts]
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
if isinstance(node, exp.Identifier):
|
|
77
|
+
dialect.normalize_identifier(node)
|
|
77
78
|
|
|
78
79
|
return expression
|
|
@@ -124,10 +124,10 @@ def validate_qualify_columns(expression: E, sql: str | None = None) -> E:
|
|
|
124
124
|
if scope.external_columns and not scope.is_correlated_subquery and not scope.pivots:
|
|
125
125
|
column = scope.external_columns[0]
|
|
126
126
|
for_table = f" for table: '{column.table}'" if column.table else ""
|
|
127
|
-
line = column.this.
|
|
128
|
-
col = column.this.
|
|
129
|
-
start = column.this.
|
|
130
|
-
end = column.this.
|
|
127
|
+
line = column.this.meta_get("line")
|
|
128
|
+
col = column.this.meta_get("col")
|
|
129
|
+
start = column.this.meta_get("start")
|
|
130
|
+
end = column.this.meta_get("end")
|
|
131
131
|
|
|
132
132
|
error_msg = f"Column '{column.name}' could not be resolved{for_table}."
|
|
133
133
|
if line and col:
|
|
@@ -142,10 +142,10 @@ def validate_qualify_columns(expression: E, sql: str | None = None) -> E:
|
|
|
142
142
|
|
|
143
143
|
if all_unqualified_columns:
|
|
144
144
|
first_column = all_unqualified_columns[0]
|
|
145
|
-
line = first_column.this.
|
|
146
|
-
col = first_column.this.
|
|
147
|
-
start = first_column.this.
|
|
148
|
-
end = first_column.this.
|
|
145
|
+
line = first_column.this.meta_get("line")
|
|
146
|
+
col = first_column.this.meta_get("col")
|
|
147
|
+
start = first_column.this.meta_get("start")
|
|
148
|
+
end = first_column.this.meta_get("end")
|
|
149
149
|
|
|
150
150
|
error_msg = f"Ambiguous column '{first_column.name}'"
|
|
151
151
|
if line and col:
|
|
@@ -204,6 +204,9 @@ def _expand_using(scope: Scope, resolver: Resolver) -> dict[str, t.Any]:
|
|
|
204
204
|
columns[column_name] = source_name
|
|
205
205
|
|
|
206
206
|
joins = list(scope.find_all(exp.Join))
|
|
207
|
+
if not joins:
|
|
208
|
+
return {}
|
|
209
|
+
|
|
207
210
|
names = {join.alias_or_name for join in joins}
|
|
208
211
|
ordered = [key for key in scope.selected_sources if key not in names]
|
|
209
212
|
|
|
@@ -213,6 +216,9 @@ def _expand_using(scope: Scope, resolver: Resolver) -> dict[str, t.Any]:
|
|
|
213
216
|
# Mapping of automatically joined column names to an ordered set of source names (dict).
|
|
214
217
|
column_tables: dict[str, dict[str, t.Any]] = {}
|
|
215
218
|
|
|
219
|
+
if not any(join.args.get("using") for join in joins):
|
|
220
|
+
return column_tables
|
|
221
|
+
|
|
216
222
|
for source_name in ordered:
|
|
217
223
|
_update_source_columns(source_name)
|
|
218
224
|
|
|
@@ -954,9 +960,15 @@ def qualify_outputs(scope_or_expression: Scope | exp.Expr) -> None:
|
|
|
954
960
|
|
|
955
961
|
def quote_identifiers(expression: E, dialect: DialectType = None, identify: bool = True) -> E:
|
|
956
962
|
"""Makes sure all identifiers that need to be quoted are quoted."""
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
963
|
+
dialect = Dialect.get_or_raise(dialect)
|
|
964
|
+
|
|
965
|
+
# `quote_identifier` only mutates identifiers in place, so we avoid `transform` here
|
|
966
|
+
# because its node replacement machinery is wasteful for this case.
|
|
967
|
+
for node in expression.walk():
|
|
968
|
+
if isinstance(node, exp.Identifier):
|
|
969
|
+
dialect.quote_identifier(node, identify=identify)
|
|
970
|
+
|
|
971
|
+
return expression
|
|
960
972
|
|
|
961
973
|
|
|
962
974
|
def pushdown_cte_alias_columns(scope: Scope) -> None:
|
|
@@ -157,9 +157,8 @@ class Resolver:
|
|
|
157
157
|
):
|
|
158
158
|
columns = source_expr.named_selects
|
|
159
159
|
|
|
160
|
-
# in bigquery, unnest structs are automatically scoped as tables, so you can
|
|
161
|
-
#
|
|
162
|
-
# this handles the case where the unnest is statically defined.
|
|
160
|
+
# in bigquery, unnest structs are automatically scoped as tables, so you can directly select
|
|
161
|
+
# a struct field in a query. This handles the case where the unnest is statically defined.
|
|
163
162
|
if self.dialect.UNNEST_COLUMN_ONLY and isinstance(source_expr, exp.Unnest):
|
|
164
163
|
if not source_expr.type or source_expr.type.is_type(exp.DType.UNKNOWN):
|
|
165
164
|
unnest_expr = seq_get(source_expr.expressions, 0)
|
|
@@ -178,7 +177,10 @@ class Resolver:
|
|
|
178
177
|
):
|
|
179
178
|
explode_col = source_expr.this.this
|
|
180
179
|
|
|
181
|
-
|
|
180
|
+
# If the column is unqualified at this point, it couldn't be resolved when
|
|
181
|
+
# this scope's children were qualified; disambiguating it here would require
|
|
182
|
+
# enumerating this very source's columns, i.e recurse without bound
|
|
183
|
+
if isinstance(explode_col, exp.Column) and explode_col.table and source.parent:
|
|
182
184
|
col_type = self._get_unnest_column_type(explode_col, source.parent)
|
|
183
185
|
columns.extend(self._struct_field_names(col_type))
|
|
184
186
|
elif isinstance(source, Scope) and isinstance(source.expression, exp.SetOperation):
|
|
@@ -21,6 +21,20 @@ TRAVERSABLES = (exp.Query, exp.DDL, exp.DML)
|
|
|
21
21
|
|
|
22
22
|
ROW_LEVEL_AGG_FUNCS = (exp.Count,)
|
|
23
23
|
|
|
24
|
+
# The node types `Scope._collect` classifies, roughly ordered by frequency. `exp.Query`
|
|
25
|
+
# covers subqueries, and `exp.UDTF` covers laterals.
|
|
26
|
+
COLLECTIBLE_TYPES = (
|
|
27
|
+
exp.Column,
|
|
28
|
+
exp.Dot,
|
|
29
|
+
exp.Table,
|
|
30
|
+
exp.Query,
|
|
31
|
+
exp.UDTF,
|
|
32
|
+
exp.CTE,
|
|
33
|
+
exp.Star,
|
|
34
|
+
exp.TableColumn,
|
|
35
|
+
exp.JoinHint,
|
|
36
|
+
)
|
|
37
|
+
|
|
24
38
|
|
|
25
39
|
class ScopeType(Enum):
|
|
26
40
|
ROOT = auto()
|
|
@@ -170,7 +184,9 @@ class Scope:
|
|
|
170
184
|
self._column_index = set()
|
|
171
185
|
|
|
172
186
|
for node in self.walk():
|
|
173
|
-
|
|
187
|
+
# Most nodes (identifiers, literals, operators etc.) aren't collectible, so a
|
|
188
|
+
# single isinstance gate lets them skip the classification chain below.
|
|
189
|
+
if node is self.expression or not isinstance(node, COLLECTIBLE_TYPES):
|
|
174
190
|
continue
|
|
175
191
|
|
|
176
192
|
if isinstance(node, exp.Dot) and node.is_star:
|
|
@@ -460,8 +476,10 @@ class Scope:
|
|
|
460
476
|
list[exp.Column]: Column instances that reference sources in the current scope.
|
|
461
477
|
"""
|
|
462
478
|
if self._local_columns is None:
|
|
463
|
-
|
|
464
|
-
|
|
479
|
+
# Compare nodes by identity: structural equality would conflate distinct
|
|
480
|
+
# column nodes that happen to look the same, and is much more expensive.
|
|
481
|
+
external_column_ids = {id(c) for c in self.external_columns}
|
|
482
|
+
self._local_columns = [c for c in self.columns if id(c) not in external_column_ids]
|
|
465
483
|
|
|
466
484
|
return self._local_columns
|
|
467
485
|
|
|
@@ -950,11 +968,17 @@ def walk_in_scope(
|
|
|
950
968
|
|
|
951
969
|
yield node
|
|
952
970
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
971
|
+
# Only CTEs and Queries can start child scopes; checking that first lets all
|
|
972
|
+
# other nodes (the vast majority) skip the rest of the boundary checks.
|
|
973
|
+
if (
|
|
974
|
+
node is not expression
|
|
975
|
+
and isinstance(node, (exp.CTE, exp.Query))
|
|
976
|
+
and (
|
|
977
|
+
isinstance(node, exp.CTE)
|
|
978
|
+
or (isinstance(node.parent, (exp.From, exp.Join)) and _is_derived_table(node))
|
|
979
|
+
or isinstance(node.parent, exp.UDTF)
|
|
980
|
+
or isinstance(node, exp.UNWRAPPED_QUERIES)
|
|
981
|
+
)
|
|
958
982
|
):
|
|
959
983
|
if isinstance(node, (exp.Subquery, exp.UDTF)):
|
|
960
984
|
for key in ("joins", "laterals", "pivots"):
|
|
@@ -596,9 +596,9 @@ class Simplifier:
|
|
|
596
596
|
joins: list[exp.Join] = []
|
|
597
597
|
|
|
598
598
|
for node in expression.walk(
|
|
599
|
-
prune=lambda n: bool(isinstance(n, exp.Condition) or n.
|
|
599
|
+
prune=lambda n: bool(isinstance(n, exp.Condition) or n.meta_get(FINAL))
|
|
600
600
|
):
|
|
601
|
-
if node.
|
|
601
|
+
if node.meta_get(FINAL):
|
|
602
602
|
continue
|
|
603
603
|
|
|
604
604
|
# group by expressions cannot be simplified, for example
|
|
@@ -687,10 +687,10 @@ class Simplifier:
|
|
|
687
687
|
original.replace(node)
|
|
688
688
|
|
|
689
689
|
for n in node.iter_expressions(reverse=True):
|
|
690
|
-
if n.
|
|
690
|
+
if n.meta_get(FINAL):
|
|
691
691
|
raise
|
|
692
692
|
pre_transformation_stack.extend(
|
|
693
|
-
n for n in node.iter_expressions(reverse=True) if not n.
|
|
693
|
+
n for n in node.iter_expressions(reverse=True) if not n.meta_get(FINAL)
|
|
694
694
|
)
|
|
695
695
|
post_transformation_stack.append((node, parent))
|
|
696
696
|
|
|
@@ -699,9 +699,16 @@ class Simplifier:
|
|
|
699
699
|
root = original is expression
|
|
700
700
|
|
|
701
701
|
# Resets parent, arg_key, index pointers– this is needed because some of the
|
|
702
|
-
# previous transformations mutate the AST, leading to an inconsistent state
|
|
702
|
+
# previous transformations mutate the AST, leading to an inconsistent state.
|
|
703
|
+
# We only fix pointers instead of calling `set` because the values are unchanged:
|
|
704
|
+
# actual mutations go through `set`/`replace`, which already invalidate cached
|
|
705
|
+
# hashes, so clearing them again here would force `while_changing` to rehash
|
|
706
|
+
# entire subtrees after every (mostly no-op) pass.
|
|
703
707
|
for k, v in tuple(original.args.items()):
|
|
704
|
-
|
|
708
|
+
if v is None:
|
|
709
|
+
original.args.pop(k)
|
|
710
|
+
else:
|
|
711
|
+
original._set_parent(k, v)
|
|
705
712
|
|
|
706
713
|
# Post-order transformations
|
|
707
714
|
node = self.simplify_not(original)
|
|
@@ -937,7 +944,7 @@ class Simplifier:
|
|
|
937
944
|
ops = set(expression.flatten())
|
|
938
945
|
for op in ops:
|
|
939
946
|
if isinstance(op, exp.Not) and op.this in ops:
|
|
940
|
-
if expression.
|
|
947
|
+
if expression.meta_get("nonnull") is True:
|
|
941
948
|
return exp.false() if isinstance(expression, exp.And) else exp.true()
|
|
942
949
|
|
|
943
950
|
return expression
|
|
@@ -1289,15 +1296,15 @@ class Simplifier:
|
|
|
1289
1296
|
sep_expr, *expressions = expression.expressions
|
|
1290
1297
|
sep = sep_expr.name
|
|
1291
1298
|
concat_type = exp.ConcatWs
|
|
1292
|
-
args = {}
|
|
1293
1299
|
else:
|
|
1294
1300
|
expressions = expression.expressions
|
|
1295
1301
|
sep = ""
|
|
1296
1302
|
concat_type = exp.Concat
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1303
|
+
|
|
1304
|
+
args = {
|
|
1305
|
+
"safe": expression.args.get("safe"),
|
|
1306
|
+
"coalesce": expression.args.get("coalesce"),
|
|
1307
|
+
}
|
|
1301
1308
|
|
|
1302
1309
|
new_args = []
|
|
1303
1310
|
for is_string_group, group in itertools.groupby(
|
|
@@ -480,10 +480,10 @@ class BigQueryParser(parser.Parser):
|
|
|
480
480
|
|
|
481
481
|
info_schema_view = f"{table_parts[-2].name}.{table_parts[-1].name}"
|
|
482
482
|
new_this = exp.Identifier(this=info_schema_view, quoted=True).update_positions(
|
|
483
|
-
line=table_parts[-2].
|
|
484
|
-
col=table_parts[-1].
|
|
485
|
-
start=table_parts[-2].
|
|
486
|
-
end=table_parts[-1].
|
|
483
|
+
line=table_parts[-2].meta_get("line"),
|
|
484
|
+
col=table_parts[-1].meta_get("col"),
|
|
485
|
+
start=table_parts[-2].meta_get("start"),
|
|
486
|
+
end=table_parts[-1].meta_get("end"),
|
|
487
487
|
)
|
|
488
488
|
table.set("this", new_this)
|
|
489
489
|
table.set("db", seq_get(table_parts, -3))
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlglotc
|
|
3
|
-
Version: 30.
|
|
3
|
+
Version: 30.11.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.11.0
|
|
10
11
|
Provides-Extra: dev
|
|
11
12
|
Requires-Dist: setuptools>=61.0; extra == "dev"
|
|
12
13
|
Requires-Dist: setuptools_scm; extra == "dev"
|
|
13
14
|
Requires-Dist: sqlglot-mypy>=2.1.0.post2; extra == "dev"
|
|
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
|