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.
Files changed (119) hide show
  1. {sqlglotc-30.16.0/sqlglotc.egg-info → sqlglotc-30.17.0}/PKG-INFO +3 -3
  2. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/pyproject.toml +2 -2
  3. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/core.py +10 -1
  4. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/query.py +22 -1
  5. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generator.py +20 -0
  6. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/python.py +14 -1
  7. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/sqlite.py +16 -1
  8. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/trino.py +26 -0
  9. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/optimizer/annotate_types.py +21 -6
  10. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/optimizer/canonicalize_internal_names.py +6 -4
  11. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/optimizer/qualify_columns.py +27 -15
  12. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/optimizer/qualify_tables.py +14 -1
  13. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/optimizer/scope.py +58 -5
  14. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/optimizer/simplify.py +17 -4
  15. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parser.py +26 -6
  16. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/clickhouse.py +2 -2
  17. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/prql.py +1 -1
  18. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/spark2.py +5 -0
  19. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/sqlite.py +27 -3
  20. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/trino.py +45 -0
  21. {sqlglotc-30.16.0 → sqlglotc-30.17.0/sqlglotc.egg-info}/PKG-INFO +3 -3
  22. sqlglotc-30.17.0/sqlglotc.egg-info/requires.txt +6 -0
  23. sqlglotc-30.17.0/sqlglotc.egg-info/scm_version.json +8 -0
  24. sqlglotc-30.16.0/sqlglotc.egg-info/requires.txt +0 -6
  25. sqlglotc-30.16.0/sqlglotc.egg-info/scm_version.json +0 -8
  26. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/MANIFEST.in +0 -0
  27. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/setup.cfg +0 -0
  28. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/setup.py +0 -0
  29. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/anonymize.py +0 -0
  30. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/errors.py +0 -0
  31. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/executor/table.py +0 -0
  32. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/aggregate.py +0 -0
  33. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/array.py +0 -0
  34. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/builders.py +0 -0
  35. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/constraints.py +0 -0
  36. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/datatypes.py +0 -0
  37. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/ddl.py +0 -0
  38. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/dml.py +0 -0
  39. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/functions.py +0 -0
  40. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/json.py +0 -0
  41. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/math.py +0 -0
  42. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/properties.py +0 -0
  43. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/string.py +0 -0
  44. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/expressions/temporal.py +0 -0
  45. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/athena.py +0 -0
  46. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/bigquery.py +0 -0
  47. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/clickhouse.py +0 -0
  48. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/databricks.py +0 -0
  49. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/dax.py +0 -0
  50. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/doris.py +0 -0
  51. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/dremio.py +0 -0
  52. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/drill.py +0 -0
  53. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/druid.py +0 -0
  54. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/duckdb.py +0 -0
  55. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/dune.py +0 -0
  56. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/exasol.py +0 -0
  57. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/fabric.py +0 -0
  58. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/hive.py +0 -0
  59. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/materialize.py +0 -0
  60. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/mysql.py +0 -0
  61. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/oracle.py +0 -0
  62. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/postgres.py +0 -0
  63. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/presto.py +0 -0
  64. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/prql.py +0 -0
  65. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/redshift.py +0 -0
  66. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/risingwave.py +0 -0
  67. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/singlestore.py +0 -0
  68. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/snowflake.py +0 -0
  69. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/solr.py +0 -0
  70. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/spark.py +0 -0
  71. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/spark2.py +0 -0
  72. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/starrocks.py +0 -0
  73. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/tableau.py +0 -0
  74. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/teradata.py +0 -0
  75. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/generators/tsql.py +0 -0
  76. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/helper.py +0 -0
  77. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/lineage.py +0 -0
  78. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/optimizer/isolate_table_selects.py +0 -0
  79. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/optimizer/normalize_identifiers.py +0 -0
  80. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/optimizer/qualify.py +0 -0
  81. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/optimizer/resolver.py +0 -0
  82. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/athena.py +0 -0
  83. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/base.py +0 -0
  84. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/bigquery.py +0 -0
  85. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/databricks.py +0 -0
  86. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/dax.py +0 -0
  87. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/doris.py +0 -0
  88. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/dremio.py +0 -0
  89. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/drill.py +0 -0
  90. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/druid.py +0 -0
  91. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/duckdb.py +0 -0
  92. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/dune.py +0 -0
  93. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/exasol.py +0 -0
  94. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/fabric.py +0 -0
  95. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/hive.py +0 -0
  96. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/materialize.py +0 -0
  97. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/mysql.py +0 -0
  98. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/oracle.py +0 -0
  99. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/postgres.py +0 -0
  100. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/presto.py +0 -0
  101. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/redshift.py +0 -0
  102. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/risingwave.py +0 -0
  103. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/singlestore.py +0 -0
  104. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/snowflake.py +0 -0
  105. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/solr.py +0 -0
  106. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/spark.py +0 -0
  107. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/starrocks.py +0 -0
  108. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/tableau.py +0 -0
  109. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/teradata.py +0 -0
  110. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/parsers/tsql.py +0 -0
  111. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/schema.py +0 -0
  112. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/serde.py +0 -0
  113. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/time.py +0 -0
  114. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/tokenizer_core.py +0 -0
  115. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglot/trie.py +0 -0
  116. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglotc.egg-info/SOURCES.txt +0 -0
  117. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglotc.egg-info/dependency_links.txt +0 -0
  118. {sqlglotc-30.16.0 → sqlglotc-30.17.0}/sqlglotc.egg-info/scm_file_list.json +0 -0
  119. {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.16.0
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.16.0
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.1.0.post9; extra == "dev"
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.1.0.post9"]
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.1.0.post9",
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
- arg_types = {"this": True, "table": False, "db": False, "catalog": False, "join_mark": False}
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
- if generated:
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 len(expression.this.selects) == len(
389
- expression.expression.selects
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.replace(exp.to_identifier(next(i), quoted=True))
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 len(setop.this.selects) == len(setop.expression.selects)
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.get(s.alias_or_name) or exp.DType.UNKNOWN,
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
- # Regular UNION takes names from the left branch; UNION BY NAME takes names
36
- # from the union of all branches, so both sides of a by_name SetOperation
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 Scope, build_scope, find_in_scope, traverse_scope, walk_in_scope
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
- for query in scope.subqueries:
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 isinstance(node, exp.UNWRAPPED_QUERIES) and not _is_from_or_join(node):
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 not isinstance(query.parent, (exp.CTE, exp.Subquery)):
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
- expressions.extend(join.this for join in node.args.get("joins") or [])
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
- expressions.extend(join.this for join in node.args.get("joins") or [])
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
- name = f'"{name}"' if this.quoted else name.upper()
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
- self.stack.append(f'"{e.this}"' if e.quoted else e.this)
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
- self.stack.append(f"'{e.this}'" if e.is_string else e.this)
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
- kvs.append([f":{k}", v])
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
- unit = self._parse_function() if parse_function_unit else None
6205
- if not unit and (
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(self._index - 1)
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=self._parse_disjunction(),
7505
- persisted=persisted or self._match_text_seq("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_(*self._parse_function_args(alias=False)),
386
- "OR": lambda self: exp.or_(*self._parse_function_args(alias=False)),
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.16.0
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.16.0
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.1.0.post9; extra == "dev"
14
+ Requires-Dist: sqlglot-mypy>=2.3.0.post1; extra == "dev"
15
15
  Dynamic: requires-dist
@@ -0,0 +1,6 @@
1
+ sqlglot==30.17.0
2
+
3
+ [dev]
4
+ setuptools>=61.0
5
+ setuptools_scm
6
+ sqlglot-mypy>=2.3.0.post1
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "30.17.0",
3
+ "distance": 0,
4
+ "node": "g9a8129b6f2667673f24713f4b49162ebae1f699d",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-08-12"
8
+ }
@@ -1,6 +0,0 @@
1
- sqlglot==30.16.0
2
-
3
- [dev]
4
- setuptools>=61.0
5
- setuptools_scm
6
- sqlglot-mypy>=2.1.0.post9
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "30.16.0",
3
- "distance": 0,
4
- "node": "gbbfb497f16002da3cda10c927683a404a2fb694e",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-08-10"
8
- }
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