1
0
Fork 0

Merging upstream version 18.4.1.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-13 21:01:12 +01:00
parent b982664fe2
commit d90681de49
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
92 changed files with 43076 additions and 40554 deletions

View file

@ -1,6 +1,63 @@
Changelog Changelog
========= =========
## [v18.4.0] - 2023-09-12
### :sparkles: New Features
- [`5e2042a`](https://github.com/tobymao/sqlglot/commit/5e2042aaa0e4be08d02c369a660d3b37ce78b567) - add TINYTEXT and TINYBLOB types *(PR [#2182](https://github.com/tobymao/sqlglot/pull/2182) by [@Nitrino](https://github.com/Nitrino))*
- [`0c536bd`](https://github.com/tobymao/sqlglot/commit/0c536bd3ca0fc0bf0d9ba649281530faf53304dd) - **oracle**: add support for JSON_ARRAYAGG *(PR [#2189](https://github.com/tobymao/sqlglot/pull/2189) by [@GeorgeSittas](https://github.com/GeorgeSittas))*
- [`f4e3e09`](https://github.com/tobymao/sqlglot/commit/f4e3e095c5eebc347f5d95e41fd68252af9b13bc) - **oracle**: add support for JSON_TABLE *(PR [#2191](https://github.com/tobymao/sqlglot/pull/2191) by [@GeorgeSittas](https://github.com/GeorgeSittas))*
- :arrow_lower_right: *addresses issue [#2187](undefined) opened by [@sashindeitidata](https://github.com/sashindeitidata)*
- [`11d95ff`](https://github.com/tobymao/sqlglot/commit/11d95ff3ece4691aa4d766c60c6765cd8a68589a) - add redshift concat_ws support *(PR [#2194](https://github.com/tobymao/sqlglot/pull/2194) by [@eakmanrq](https://github.com/eakmanrq))*
### :bug: Bug Fixes
- [`c7433bf`](https://github.com/tobymao/sqlglot/commit/c7433bfe5086eb66895b43514eb4edfa56eb1228) - join using with star *(commit by [@tobymao](https://github.com/tobymao))*
- [`451439c`](https://github.com/tobymao/sqlglot/commit/451439c84a8feda05d51c47180c9f69cc92f22d6) - **clickhouse**: add missing type mappings for string types *(PR [#2183](https://github.com/tobymao/sqlglot/pull/2183) by [@GeorgeSittas](https://github.com/GeorgeSittas))*
- [`5ba5165`](https://github.com/tobymao/sqlglot/commit/5ba51657bc810139a28603b1bb542d44173bdc55) - **duckdb**: rename VariancePop -> var_pop in DuckDB *(PR [#2184](https://github.com/tobymao/sqlglot/pull/2184) by [@gforsyth](https://github.com/gforsyth))*
- [`d192515`](https://github.com/tobymao/sqlglot/commit/d19251566424ba07efe46b3be4ac6bbe327e7821) - **optimizer**: merge subqueries should use alias from outer scope *(PR [#2185](https://github.com/tobymao/sqlglot/pull/2185) by [@barakalon](https://github.com/barakalon))*
- [`12db377`](https://github.com/tobymao/sqlglot/commit/12db377ea8b07b1ff418dc988ef1ea4c20288206) - **mysql**: multi table update closes [#2193](https://github.com/tobymao/sqlglot/pull/2193) *(commit by [@tobymao](https://github.com/tobymao))*
- [`b9f5ede`](https://github.com/tobymao/sqlglot/commit/b9f5edee02aed346ebaea767274cc08e3960419b) - **oracle**: make parentheses in JSON_TABLE's COLUMNS clause optional *(commit by [@GeorgeSittas](https://github.com/GeorgeSittas))*
- [`8c51275`](https://github.com/tobymao/sqlglot/commit/8c512750044efa059adc3afee32517684dabfc12) - **mysql**: parse column prefix in index / pk defn. correctly *(PR [#2197](https://github.com/tobymao/sqlglot/pull/2197) by [@GeorgeSittas](https://github.com/GeorgeSittas))*
- :arrow_lower_right: *fixes issue [#2195](undefined) opened by [@Nitrino](https://github.com/Nitrino)*
### :recycle: Refactors
- [`a81dd14`](https://github.com/tobymao/sqlglot/commit/a81dd14a6de1a50438eae64c2dd20e4841c29572) - override Bracket.output_name only when there's one bracket expression *(commit by [@GeorgeSittas](https://github.com/GeorgeSittas))*
- [`7ae5a94`](https://github.com/tobymao/sqlglot/commit/7ae5a9463cd68371f6ed45b9e00582eb44cead3b) - fix mutation bug in Column.to_dot, simplify Dot.build *(PR [#2196](https://github.com/tobymao/sqlglot/pull/2196) by [@GeorgeSittas](https://github.com/GeorgeSittas))*
### :wrench: Chores
- [`981ad23`](https://github.com/tobymao/sqlglot/commit/981ad23cd1bf2b95e121bb9a7f3b677d4a053be4) - **duckdb**: fix var_pop tests *(commit by [@GeorgeSittas](https://github.com/GeorgeSittas))*
## [v18.3.0] - 2023-09-07
### :boom: BREAKING CHANGES
- due to [`3fc2eb5`](https://github.com/tobymao/sqlglot/commit/3fc2eb581528504db4523c3e0a537000e026a4cc) - improve support for interval spans like HOUR TO SECOND *(PR [#2167](https://github.com/tobymao/sqlglot/pull/2167) by [@GeorgeSittas](https://github.com/GeorgeSittas))*:
improve support for interval spans like HOUR TO SECOND (#2167)
- due to [`93b7ba2`](https://github.com/tobymao/sqlglot/commit/93b7ba20640a880ceeb63660b796ab94579bb73a) - MySQL Timestamp Data Types *(PR [#2173](https://github.com/tobymao/sqlglot/pull/2173) by [@eakmanrq](https://github.com/eakmanrq))*:
MySQL Timestamp Data Types (#2173)
### :sparkles: New Features
- [`5dd0fda`](https://github.com/tobymao/sqlglot/commit/5dd0fdaaf9ec8bc5f9f0a2cd01395222eacf28a0) - **spark**: add support for raw strings *(PR [#2165](https://github.com/tobymao/sqlglot/pull/2165) by [@GeorgeSittas](https://github.com/GeorgeSittas))*
- :arrow_lower_right: *addresses issue [#2162](undefined) opened by [@aersam](https://github.com/aersam)*
- [`d9f8910`](https://github.com/tobymao/sqlglot/commit/d9f89109e9795685392adb43bc2e87fbd346f263) - **teradata**: add support for the SAMPLE clause *(PR [#2169](https://github.com/tobymao/sqlglot/pull/2169) by [@GeorgeSittas](https://github.com/GeorgeSittas))*
- [`63ac621`](https://github.com/tobymao/sqlglot/commit/63ac621f7507d35ccdc32784ec0631437ddf0c1b) - **mysql**: improve support for unsigned int types *(PR [#2172](https://github.com/tobymao/sqlglot/pull/2172) by [@GeorgeSittas](https://github.com/GeorgeSittas))*
- :arrow_lower_right: *addresses issue [#2166](undefined) opened by [@Nitrino](https://github.com/Nitrino)*
- [`cd301cc`](https://github.com/tobymao/sqlglot/commit/cd301cc9aa7a910fc6f7f0b9cc2dbba9a7d9ea24) - **postgres**: add support for ALTER TABLE ONLY ... *(PR [#2179](https://github.com/tobymao/sqlglot/pull/2179) by [@GeorgeSittas](https://github.com/GeorgeSittas))*
- :arrow_lower_right: *addresses issue [#2178](undefined) opened by [@Nitrino](https://github.com/Nitrino)*
### :bug: Bug Fixes
- [`3fc2eb5`](https://github.com/tobymao/sqlglot/commit/3fc2eb581528504db4523c3e0a537000e026a4cc) - improve support for interval spans like HOUR TO SECOND *(PR [#2167](https://github.com/tobymao/sqlglot/pull/2167) by [@GeorgeSittas](https://github.com/GeorgeSittas))*
- :arrow_lower_right: *fixes issue [#2163](undefined) opened by [@aersam](https://github.com/aersam)*
- [`93b7ba2`](https://github.com/tobymao/sqlglot/commit/93b7ba20640a880ceeb63660b796ab94579bb73a) - MySQL Timestamp Data Types *(PR [#2173](https://github.com/tobymao/sqlglot/pull/2173) by [@eakmanrq](https://github.com/eakmanrq))*
- [`6d761f9`](https://github.com/tobymao/sqlglot/commit/6d761f9934fcf57a06fb4645e43ce91dca6adc96) - filter_sql use strip closes [#2180](https://github.com/tobymao/sqlglot/pull/2180) *(commit by [@tobymao](https://github.com/tobymao))*
### :wrench: Chores
- [`5fbe303`](https://github.com/tobymao/sqlglot/commit/5fbe303504f19a1c949d0acf777c2bf2d3ecc1b6) - add minimum python version required to setup.py *(PR [#2170](https://github.com/tobymao/sqlglot/pull/2170) by [@GeorgeSittas](https://github.com/GeorgeSittas))*
- :arrow_lower_right: *addresses issue [#2168](undefined) opened by [@jlardieri5](https://github.com/jlardieri5)*
## [v18.2.0] - 2023-09-05 ## [v18.2.0] - 2023-09-05
### :sparkles: New Features ### :sparkles: New Features
- [`5df9b5f`](https://github.com/tobymao/sqlglot/commit/5df9b5f658d24267e4f6b00bd89eb0b2f4dc5bfc) - **snowflake**: desc table type closes [#2145](https://github.com/tobymao/sqlglot/pull/2145) *(commit by [@tobymao](https://github.com/tobymao))* - [`5df9b5f`](https://github.com/tobymao/sqlglot/commit/5df9b5f658d24267e4f6b00bd89eb0b2f4dc5bfc) - **snowflake**: desc table type closes [#2145](https://github.com/tobymao/sqlglot/pull/2145) *(commit by [@tobymao](https://github.com/tobymao))*
@ -1312,3 +1369,5 @@ Changelog
[v18.0.1]: https://github.com/tobymao/sqlglot/compare/v18.0.0...v18.0.1 [v18.0.1]: https://github.com/tobymao/sqlglot/compare/v18.0.0...v18.0.1
[v18.1.0]: https://github.com/tobymao/sqlglot/compare/v18.0.1...v18.1.0 [v18.1.0]: https://github.com/tobymao/sqlglot/compare/v18.0.1...v18.1.0
[v18.2.0]: https://github.com/tobymao/sqlglot/compare/v18.1.0...v18.2.0 [v18.2.0]: https://github.com/tobymao/sqlglot/compare/v18.1.0...v18.2.0
[v18.3.0]: https://github.com/tobymao/sqlglot/compare/v18.2.0...v18.3.0
[v18.4.0]: https://github.com/tobymao/sqlglot/compare/v18.3.0...v18.4.0

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -313,6 +313,8 @@ class ClickHouse(Dialect):
exp.DataType.Type.LONGTEXT: "String", exp.DataType.Type.LONGTEXT: "String",
exp.DataType.Type.MEDIUMBLOB: "String", exp.DataType.Type.MEDIUMBLOB: "String",
exp.DataType.Type.MEDIUMTEXT: "String", exp.DataType.Type.MEDIUMTEXT: "String",
exp.DataType.Type.TINYBLOB: "String",
exp.DataType.Type.TINYTEXT: "String",
exp.DataType.Type.TEXT: "String", exp.DataType.Type.TEXT: "String",
exp.DataType.Type.VARBINARY: "String", exp.DataType.Type.VARBINARY: "String",
exp.DataType.Type.VARCHAR: "String", exp.DataType.Type.VARCHAR: "String",
@ -331,6 +333,7 @@ class ClickHouse(Dialect):
exp.DataType.Type.FIXEDSTRING: "FixedString", exp.DataType.Type.FIXEDSTRING: "FixedString",
exp.DataType.Type.FLOAT: "Float32", exp.DataType.Type.FLOAT: "Float32",
exp.DataType.Type.INT: "Int32", exp.DataType.Type.INT: "Int32",
exp.DataType.Type.MEDIUMINT: "Int32",
exp.DataType.Type.INT128: "Int128", exp.DataType.Type.INT128: "Int128",
exp.DataType.Type.INT256: "Int256", exp.DataType.Type.INT256: "Int256",
exp.DataType.Type.LOWCARDINALITY: "LowCardinality", exp.DataType.Type.LOWCARDINALITY: "LowCardinality",

View file

@ -2,6 +2,7 @@ from __future__ import annotations
import typing as t import typing as t
from enum import Enum from enum import Enum
from functools import reduce
from sqlglot import exp from sqlglot import exp
from sqlglot._typing import E from sqlglot._typing import E
@ -656,11 +657,18 @@ def ts_or_ds_to_date_sql(dialect: str) -> t.Callable:
def concat_to_dpipe_sql(self: Generator, expression: exp.Concat | exp.SafeConcat) -> str: def concat_to_dpipe_sql(self: Generator, expression: exp.Concat | exp.SafeConcat) -> str:
expression = expression.copy() expression = expression.copy()
this, *rest_args = expression.expressions return self.sql(reduce(lambda x, y: exp.DPipe(this=x, expression=y), expression.expressions))
for arg in rest_args:
this = exp.DPipe(this=this, expression=arg)
return self.sql(this)
def concat_ws_to_dpipe_sql(self: Generator, expression: exp.ConcatWs) -> str:
expression = expression.copy()
delim, *rest_args = expression.expressions
return self.sql(
reduce(
lambda x, y: exp.DPipe(this=x, expression=exp.DPipe(this=delim, expression=y)),
rest_args,
)
)
def regexp_extract_sql(self: Generator, expression: exp.RegexpExtract) -> str: def regexp_extract_sql(self: Generator, expression: exp.RegexpExtract) -> str:

View file

@ -291,6 +291,7 @@ class DuckDB(Dialect):
exp.UnixToStr: lambda self, e: f"STRFTIME(TO_TIMESTAMP({self.sql(e, 'this')}), {self.format_time(e)})", exp.UnixToStr: lambda self, e: f"STRFTIME(TO_TIMESTAMP({self.sql(e, 'this')}), {self.format_time(e)})",
exp.UnixToTime: rename_func("TO_TIMESTAMP"), exp.UnixToTime: rename_func("TO_TIMESTAMP"),
exp.UnixToTimeStr: lambda self, e: f"CAST(TO_TIMESTAMP({self.sql(e, 'this')}) AS TEXT)", exp.UnixToTimeStr: lambda self, e: f"CAST(TO_TIMESTAMP({self.sql(e, 'this')}) AS TEXT)",
exp.VariancePop: rename_func("VAR_POP"),
exp.WeekOfYear: rename_func("WEEKOFYEAR"), exp.WeekOfYear: rename_func("WEEKOFYEAR"),
} }

View file

@ -119,7 +119,7 @@ class MySQL(Dialect):
QUOTES = ["'", '"'] QUOTES = ["'", '"']
COMMENTS = ["--", "#", ("/*", "*/")] COMMENTS = ["--", "#", ("/*", "*/")]
IDENTIFIERS = ["`"] IDENTIFIERS = ["`"]
STRING_ESCAPES = ["'", "\\"] STRING_ESCAPES = ["'", '"', "\\"]
BIT_STRINGS = [("b'", "'"), ("B'", "'"), ("0b", "")] BIT_STRINGS = [("b'", "'"), ("B'", "'"), ("0b", "")]
HEX_STRINGS = [("x'", "'"), ("X'", "'"), ("0x", "")] HEX_STRINGS = [("x'", "'"), ("X'", "'"), ("0x", "")]
@ -132,6 +132,8 @@ class MySQL(Dialect):
"LONGBLOB": TokenType.LONGBLOB, "LONGBLOB": TokenType.LONGBLOB,
"LONGTEXT": TokenType.LONGTEXT, "LONGTEXT": TokenType.LONGTEXT,
"MEDIUMBLOB": TokenType.MEDIUMBLOB, "MEDIUMBLOB": TokenType.MEDIUMBLOB,
"TINYBLOB": TokenType.TINYBLOB,
"TINYTEXT": TokenType.TINYTEXT,
"MEDIUMTEXT": TokenType.MEDIUMTEXT, "MEDIUMTEXT": TokenType.MEDIUMTEXT,
"MEDIUMINT": TokenType.MEDIUMINT, "MEDIUMINT": TokenType.MEDIUMINT,
"MEMBER OF": TokenType.MEMBER_OF, "MEMBER OF": TokenType.MEMBER_OF,
@ -356,6 +358,15 @@ class MySQL(Dialect):
LOG_DEFAULTS_TO_LN = True LOG_DEFAULTS_TO_LN = True
def _parse_primary_key_part(self) -> t.Optional[exp.Expression]:
this = self._parse_id_var()
if not self._match(TokenType.L_PAREN):
return this
expression = self._parse_number()
self._match_r_paren()
return self.expression(exp.ColumnPrefix, this=this, expression=expression)
def _parse_index_constraint( def _parse_index_constraint(
self, kind: t.Optional[str] = None self, kind: t.Optional[str] = None
) -> exp.IndexColumnConstraint: ) -> exp.IndexColumnConstraint:
@ -577,8 +588,10 @@ class MySQL(Dialect):
TYPE_MAPPING.pop(exp.DataType.Type.MEDIUMTEXT) TYPE_MAPPING.pop(exp.DataType.Type.MEDIUMTEXT)
TYPE_MAPPING.pop(exp.DataType.Type.LONGTEXT) TYPE_MAPPING.pop(exp.DataType.Type.LONGTEXT)
TYPE_MAPPING.pop(exp.DataType.Type.TINYTEXT)
TYPE_MAPPING.pop(exp.DataType.Type.MEDIUMBLOB) TYPE_MAPPING.pop(exp.DataType.Type.MEDIUMBLOB)
TYPE_MAPPING.pop(exp.DataType.Type.LONGBLOB) TYPE_MAPPING.pop(exp.DataType.Type.LONGBLOB)
TYPE_MAPPING.pop(exp.DataType.Type.TINYBLOB)
PROPERTIES_LOCATION = { PROPERTIES_LOCATION = {
**generator.Generator.PROPERTIES_LOCATION, **generator.Generator.PROPERTIES_LOCATION,

View file

@ -7,6 +7,9 @@ from sqlglot.dialects.dialect import Dialect, no_ilike_sql, rename_func, trim_sq
from sqlglot.helper import seq_get from sqlglot.helper import seq_get
from sqlglot.tokens import TokenType from sqlglot.tokens import TokenType
if t.TYPE_CHECKING:
from sqlglot._typing import E
def _parse_xml_table(self: Oracle.Parser) -> exp.XMLTable: def _parse_xml_table(self: Oracle.Parser) -> exp.XMLTable:
this = self._parse_string() this = self._parse_string()
@ -69,6 +72,16 @@ class Oracle(Dialect):
FUNCTION_PARSERS: t.Dict[str, t.Callable] = { FUNCTION_PARSERS: t.Dict[str, t.Callable] = {
**parser.Parser.FUNCTION_PARSERS, **parser.Parser.FUNCTION_PARSERS,
"JSON_ARRAY": lambda self: self._parse_json_array(
exp.JSONArray,
expressions=self._parse_csv(lambda: self._parse_format_json(self._parse_bitwise())),
),
"JSON_ARRAYAGG": lambda self: self._parse_json_array(
exp.JSONArrayAgg,
this=self._parse_format_json(self._parse_bitwise()),
order=self._parse_order(),
),
"JSON_TABLE": lambda self: self._parse_json_table(),
"XMLTABLE": _parse_xml_table, "XMLTABLE": _parse_xml_table,
} }
@ -82,6 +95,38 @@ class Oracle(Dialect):
# Reference: https://stackoverflow.com/a/336455 # Reference: https://stackoverflow.com/a/336455
DISTINCT_TOKENS = {TokenType.DISTINCT, TokenType.UNIQUE} DISTINCT_TOKENS = {TokenType.DISTINCT, TokenType.UNIQUE}
# Note: this is currently incomplete; it only implements the "JSON_value_column" part
def _parse_json_column_def(self) -> exp.JSONColumnDef:
this = self._parse_id_var()
kind = self._parse_types(allow_identifiers=False)
path = self._match_text_seq("PATH") and self._parse_string()
return self.expression(exp.JSONColumnDef, this=this, kind=kind, path=path)
def _parse_json_table(self) -> exp.JSONTable:
this = self._parse_format_json(self._parse_bitwise())
path = self._match(TokenType.COMMA) and self._parse_string()
error_handling = self._parse_on_handling("ERROR", "ERROR", "NULL")
empty_handling = self._parse_on_handling("EMPTY", "ERROR", "NULL")
self._match(TokenType.COLUMN)
expressions = self._parse_wrapped_csv(self._parse_json_column_def, optional=True)
return exp.JSONTable(
this=this,
expressions=expressions,
path=path,
error_handling=error_handling,
empty_handling=empty_handling,
)
def _parse_json_array(self, expr_type: t.Type[E], **kwargs) -> E:
return self.expression(
expr_type,
null_handling=self._parse_on_handling("NULL", "NULL", "ABSENT"),
return_type=self._match_text_seq("RETURNING") and self._parse_type(),
strict=self._match_text_seq("STRICT"),
**kwargs,
)
def _parse_column(self) -> t.Optional[exp.Expression]: def _parse_column(self) -> t.Optional[exp.Expression]:
column = super()._parse_column() column = super()._parse_column()
if column: if column:

View file

@ -5,6 +5,7 @@ import typing as t
from sqlglot import exp, transforms from sqlglot import exp, transforms
from sqlglot.dialects.dialect import ( from sqlglot.dialects.dialect import (
concat_to_dpipe_sql, concat_to_dpipe_sql,
concat_ws_to_dpipe_sql,
rename_func, rename_func,
ts_or_ds_to_date_sql, ts_or_ds_to_date_sql,
) )
@ -123,6 +124,7 @@ class Redshift(Postgres):
TRANSFORMS = { TRANSFORMS = {
**Postgres.Generator.TRANSFORMS, **Postgres.Generator.TRANSFORMS,
exp.Concat: concat_to_dpipe_sql, exp.Concat: concat_to_dpipe_sql,
exp.ConcatWs: concat_ws_to_dpipe_sql,
exp.CurrentTimestamp: lambda self, e: "SYSDATE", exp.CurrentTimestamp: lambda self, e: "SYSDATE",
exp.DateAdd: lambda self, e: self.func( exp.DateAdd: lambda self, e: self.func(
"DATEADD", exp.var(e.text("unit") or "day"), e.expression, e.this "DATEADD", exp.var(e.text("unit") or "day"), e.expression, e.this

View file

@ -20,6 +20,7 @@ import typing as t
from collections import deque from collections import deque
from copy import deepcopy from copy import deepcopy
from enum import auto from enum import auto
from functools import reduce
from sqlglot._typing import E from sqlglot._typing import E
from sqlglot.errors import ParseError from sqlglot.errors import ParseError
@ -1170,7 +1171,7 @@ class Column(Condition):
parts.append(parent.expression) parts.append(parent.expression)
parent = parent.parent parent = parent.parent
return Dot.build(parts) return Dot.build(deepcopy(parts))
class ColumnPosition(Expression): class ColumnPosition(Expression):
@ -1537,6 +1538,10 @@ class ForeignKey(Expression):
} }
class ColumnPrefix(Expression):
arg_types = {"this": True, "expression": True}
class PrimaryKey(Expression): class PrimaryKey(Expression):
arg_types = {"expressions": True, "options": False} arg_types = {"expressions": True, "options": False}
@ -3529,6 +3534,8 @@ class DataType(Expression):
STRUCT = auto() STRUCT = auto()
SUPER = auto() SUPER = auto()
TEXT = auto() TEXT = auto()
TINYBLOB = auto()
TINYTEXT = auto()
TIME = auto() TIME = auto()
TIMETZ = auto() TIMETZ = auto()
TIMESTAMP = auto() TIMESTAMP = auto()
@ -3793,13 +3800,7 @@ class Dot(Binary):
if len(expressions) < 2: if len(expressions) < 2:
raise ValueError(f"Dot requires >= 2 expressions.") raise ValueError(f"Dot requires >= 2 expressions.")
a, b, *expressions = expressions return t.cast(Dot, reduce(lambda x, y: Dot(this=x, expression=y), expressions))
dot = Dot(this=a, expression=b)
for expression in expressions:
dot = Dot(this=dot, expression=expression)
return dot
class DPipe(Binary): class DPipe(Binary):
@ -3959,6 +3960,13 @@ class Between(Predicate):
class Bracket(Condition): class Bracket(Condition):
arg_types = {"this": True, "expressions": True} arg_types = {"this": True, "expressions": True}
@property
def output_name(self) -> str:
if len(self.expressions) == 1:
return self.expressions[0].output_name
return super().output_name
class SafeBracket(Bracket): class SafeBracket(Bracket):
"""Represents array lookup where OOB index yields NULL instead of causing a failure.""" """Represents array lookup where OOB index yields NULL instead of causing a failure."""
@ -4477,6 +4485,10 @@ class IsNan(Func):
_sql_names = ["IS_NAN", "ISNAN"] _sql_names = ["IS_NAN", "ISNAN"]
class FormatJson(Expression):
pass
class JSONKeyValue(Expression): class JSONKeyValue(Expression):
arg_types = {"this": True, "expression": True} arg_types = {"this": True, "expression": True}
@ -4487,11 +4499,48 @@ class JSONObject(Func):
"null_handling": False, "null_handling": False,
"unique_keys": False, "unique_keys": False,
"return_type": False, "return_type": False,
"format_json": False,
"encoding": False, "encoding": False,
} }
# https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/JSON_ARRAY.html
class JSONArray(Func):
arg_types = {
"expressions": True,
"null_handling": False,
"return_type": False,
"strict": False,
}
# https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/JSON_ARRAYAGG.html
class JSONArrayAgg(Func):
arg_types = {
"this": True,
"order": False,
"null_handling": False,
"return_type": False,
"strict": False,
}
# https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/JSON_TABLE.html
# Note: parsing of JSON column definitions is currently incomplete.
class JSONColumnDef(Expression):
arg_types = {"this": True, "kind": False, "path": False}
# # https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/JSON_TABLE.html
class JSONTable(Func):
arg_types = {
"this": True,
"expressions": True,
"path": False,
"error_handling": False,
"empty_handling": False,
}
class OpenJSONColumnDef(Expression): class OpenJSONColumnDef(Expression):
arg_types = {"this": True, "kind": True, "path": False, "as_json": False} arg_types = {"this": True, "kind": True, "path": False, "as_json": False}

View file

@ -193,8 +193,10 @@ class Generator:
exp.DataType.Type.NVARCHAR: "VARCHAR", exp.DataType.Type.NVARCHAR: "VARCHAR",
exp.DataType.Type.MEDIUMTEXT: "TEXT", exp.DataType.Type.MEDIUMTEXT: "TEXT",
exp.DataType.Type.LONGTEXT: "TEXT", exp.DataType.Type.LONGTEXT: "TEXT",
exp.DataType.Type.TINYTEXT: "TEXT",
exp.DataType.Type.MEDIUMBLOB: "BLOB", exp.DataType.Type.MEDIUMBLOB: "BLOB",
exp.DataType.Type.LONGBLOB: "BLOB", exp.DataType.Type.LONGBLOB: "BLOB",
exp.DataType.Type.TINYBLOB: "BLOB",
exp.DataType.Type.INET: "INET", exp.DataType.Type.INET: "INET",
} }
@ -2021,6 +2023,9 @@ class Generator:
def jsonkeyvalue_sql(self, expression: exp.JSONKeyValue) -> str: def jsonkeyvalue_sql(self, expression: exp.JSONKeyValue) -> str:
return f"{self.sql(expression, 'this')}: {self.sql(expression, 'expression')}" return f"{self.sql(expression, 'this')}: {self.sql(expression, 'expression')}"
def formatjson_sql(self, expression: exp.FormatJson) -> str:
return f"{self.sql(expression, 'this')} FORMAT JSON"
def jsonobject_sql(self, expression: exp.JSONObject) -> str: def jsonobject_sql(self, expression: exp.JSONObject) -> str:
null_handling = expression.args.get("null_handling") null_handling = expression.args.get("null_handling")
null_handling = f" {null_handling}" if null_handling else "" null_handling = f" {null_handling}" if null_handling else ""
@ -2031,13 +2036,57 @@ class Generator:
unique_keys = "" unique_keys = ""
return_type = self.sql(expression, "return_type") return_type = self.sql(expression, "return_type")
return_type = f" RETURNING {return_type}" if return_type else "" return_type = f" RETURNING {return_type}" if return_type else ""
format_json = " FORMAT JSON" if expression.args.get("format_json") else ""
encoding = self.sql(expression, "encoding") encoding = self.sql(expression, "encoding")
encoding = f" ENCODING {encoding}" if encoding else "" encoding = f" ENCODING {encoding}" if encoding else ""
return self.func( return self.func(
"JSON_OBJECT", "JSON_OBJECT",
*expression.expressions, *expression.expressions,
suffix=f"{null_handling}{unique_keys}{return_type}{format_json}{encoding})", suffix=f"{null_handling}{unique_keys}{return_type}{encoding})",
)
def jsonarray_sql(self, expression: exp.JSONArray) -> str:
null_handling = expression.args.get("null_handling")
null_handling = f" {null_handling}" if null_handling else ""
return_type = self.sql(expression, "return_type")
return_type = f" RETURNING {return_type}" if return_type else ""
strict = " STRICT" if expression.args.get("strict") else ""
return self.func(
"JSON_ARRAY", *expression.expressions, suffix=f"{null_handling}{return_type}{strict})"
)
def jsonarrayagg_sql(self, expression: exp.JSONArrayAgg) -> str:
this = self.sql(expression, "this")
order = self.sql(expression, "order")
null_handling = expression.args.get("null_handling")
null_handling = f" {null_handling}" if null_handling else ""
return_type = self.sql(expression, "return_type")
return_type = f" RETURNING {return_type}" if return_type else ""
strict = " STRICT" if expression.args.get("strict") else ""
return self.func(
"JSON_ARRAYAGG",
this,
suffix=f"{order}{null_handling}{return_type}{strict})",
)
def jsoncolumndef_sql(self, expression: exp.JSONColumnDef) -> str:
this = self.sql(expression, "this")
kind = self.sql(expression, "kind")
kind = f" {kind}" if kind else ""
path = self.sql(expression, "path")
path = f" PATH {path}" if path else ""
return f"{this}{kind}{path}"
def jsontable_sql(self, expression: exp.JSONTable) -> str:
this = self.sql(expression, "this")
path = self.sql(expression, "path")
path = f", {path}" if path else ""
error_handling = expression.args.get("error_handling")
error_handling = f" {error_handling}" if error_handling else ""
empty_handling = expression.args.get("empty_handling")
empty_handling = f" {empty_handling}" if empty_handling else ""
columns = f" COLUMNS ({self.expressions(expression, skip_first=True)})"
return self.func(
"JSON_TABLE", this, suffix=f"{path}{error_handling}{empty_handling}{columns})"
) )
def openjsoncolumndef_sql(self, expression: exp.OpenJSONColumnDef) -> str: def openjsoncolumndef_sql(self, expression: exp.OpenJSONColumnDef) -> str:
@ -2722,6 +2771,9 @@ class Generator:
condition = f" IF {condition}" if condition else "" condition = f" IF {condition}" if condition else ""
return f"{this} FOR {expr} IN {iterator}{condition}" return f"{this} FOR {expr} IN {iterator}{condition}"
def columnprefix_sql(self, expression: exp.ColumnPrefix) -> str:
return f"{self.sql(expression, 'this')}({self.sql(expression, 'expression')})"
def cached_generator( def cached_generator(
cache: t.Optional[t.Dict[int, str]] = None cache: t.Optional[t.Dict[int, str]] = None

View file

@ -128,7 +128,7 @@ def _mergeable(outer_scope, inner_scope, leave_tables_isolated, from_or_join):
def _is_a_window_expression_in_unmergable_operation(): def _is_a_window_expression_in_unmergable_operation():
window_expressions = inner_select.find_all(exp.Window) window_expressions = inner_select.find_all(exp.Window)
window_alias_names = {window.parent.alias_or_name for window in window_expressions} window_alias_names = {window.parent.alias_or_name for window in window_expressions}
inner_select_name = inner_select.parent.alias_or_name inner_select_name = from_or_join.alias_or_name
unmergable_window_columns = [ unmergable_window_columns = [
column column
for column in outer_scope.columns for column in outer_scope.columns

View file

@ -129,7 +129,7 @@ def _expand_using(scope: Scope, resolver: Resolver) -> t.Dict[str, t.Any]:
table = columns.get(identifier) table = columns.get(identifier)
if not table or identifier not in join_columns: if not table or identifier not in join_columns:
if columns and join_columns: if (columns and "*" not in columns) and join_columns:
raise OptimizeError(f"Cannot automatically join: {identifier}") raise OptimizeError(f"Cannot automatically join: {identifier}")
table = table or source_table table = table or source_table

View file

@ -155,6 +155,8 @@ class Parser(metaclass=_Parser):
TokenType.JSON, TokenType.JSON,
TokenType.JSONB, TokenType.JSONB,
TokenType.INTERVAL, TokenType.INTERVAL,
TokenType.TINYBLOB,
TokenType.TINYTEXT,
TokenType.TIME, TokenType.TIME,
TokenType.TIMETZ, TokenType.TIMETZ,
TokenType.TIMESTAMP, TokenType.TIMESTAMP,
@ -764,6 +766,7 @@ class Parser(metaclass=_Parser):
"ANY_VALUE": lambda self: self._parse_any_value(), "ANY_VALUE": lambda self: self._parse_any_value(),
"CAST": lambda self: self._parse_cast(self.STRICT_CAST), "CAST": lambda self: self._parse_cast(self.STRICT_CAST),
"CONCAT": lambda self: self._parse_concat(), "CONCAT": lambda self: self._parse_concat(),
"CONCAT_WS": lambda self: self._parse_concat_ws(),
"CONVERT": lambda self: self._parse_convert(self.STRICT_CAST), "CONVERT": lambda self: self._parse_convert(self.STRICT_CAST),
"DECODE": lambda self: self._parse_decode(), "DECODE": lambda self: self._parse_decode(),
"EXTRACT": lambda self: self._parse_extract(), "EXTRACT": lambda self: self._parse_extract(),
@ -1942,7 +1945,7 @@ class Parser(metaclass=_Parser):
def _parse_update(self) -> exp.Update: def _parse_update(self) -> exp.Update:
comments = self._prev_comments comments = self._prev_comments
this = self._parse_table(alias_tokens=self.UPDATE_ALIAS_TOKENS) this = self._parse_table(joins=True, alias_tokens=self.UPDATE_ALIAS_TOKENS)
expressions = self._match(TokenType.SET) and self._parse_csv(self._parse_equality) expressions = self._match(TokenType.SET) and self._parse_csv(self._parse_equality)
returning = self._parse_returning() returning = self._parse_returning()
return self.expression( return self.expression(
@ -3269,7 +3272,7 @@ class Parser(metaclass=_Parser):
if tokens[0].token_type in self.TYPE_TOKENS: if tokens[0].token_type in self.TYPE_TOKENS:
self._prev = tokens[0] self._prev = tokens[0]
elif self.SUPPORTS_USER_DEFINED_TYPES: elif self.SUPPORTS_USER_DEFINED_TYPES:
return identifier return exp.DataType.build(identifier.name, udt=True)
else: else:
return None return None
else: else:
@ -3888,6 +3891,9 @@ class Parser(metaclass=_Parser):
exp.ForeignKey, expressions=expressions, reference=reference, **options # type: ignore exp.ForeignKey, expressions=expressions, reference=reference, **options # type: ignore
) )
def _parse_primary_key_part(self) -> t.Optional[exp.Expression]:
return self._parse_field()
def _parse_primary_key( def _parse_primary_key(
self, wrapped_optional: bool = False, in_props: bool = False self, wrapped_optional: bool = False, in_props: bool = False
) -> exp.PrimaryKeyColumnConstraint | exp.PrimaryKey: ) -> exp.PrimaryKeyColumnConstraint | exp.PrimaryKey:
@ -3899,7 +3905,9 @@ class Parser(metaclass=_Parser):
if not in_props and not self._match(TokenType.L_PAREN, advance=False): if not in_props and not self._match(TokenType.L_PAREN, advance=False):
return self.expression(exp.PrimaryKeyColumnConstraint, desc=desc) return self.expression(exp.PrimaryKeyColumnConstraint, desc=desc)
expressions = self._parse_wrapped_csv(self._parse_field, optional=wrapped_optional) expressions = self._parse_wrapped_csv(
self._parse_primary_key_part, optional=wrapped_optional
)
options = self._parse_key_constraint_options() options = self._parse_key_constraint_options()
return self.expression(exp.PrimaryKey, expressions=expressions, options=options) return self.expression(exp.PrimaryKey, expressions=expressions, options=options)
@ -4066,11 +4074,7 @@ class Parser(metaclass=_Parser):
def _parse_concat(self) -> t.Optional[exp.Expression]: def _parse_concat(self) -> t.Optional[exp.Expression]:
args = self._parse_csv(self._parse_conjunction) args = self._parse_csv(self._parse_conjunction)
if self.CONCAT_NULL_OUTPUTS_STRING: if self.CONCAT_NULL_OUTPUTS_STRING:
args = [ args = self._ensure_string_if_null(args)
exp.func("COALESCE", exp.cast(arg, "text"), exp.Literal.string(""))
for arg in args
if arg
]
# Some dialects (e.g. Trino) don't allow a single-argument CONCAT call, so when # Some dialects (e.g. Trino) don't allow a single-argument CONCAT call, so when
# we find such a call we replace it with its argument. # we find such a call we replace it with its argument.
@ -4081,6 +4085,16 @@ class Parser(metaclass=_Parser):
exp.Concat if self.STRICT_STRING_CONCAT else exp.SafeConcat, expressions=args exp.Concat if self.STRICT_STRING_CONCAT else exp.SafeConcat, expressions=args
) )
def _parse_concat_ws(self) -> t.Optional[exp.Expression]:
args = self._parse_csv(self._parse_conjunction)
if len(args) < 2:
return self.expression(exp.ConcatWs, expressions=args)
delim, *values = args
if self.CONCAT_NULL_OUTPUTS_STRING:
values = self._ensure_string_if_null(values)
return self.expression(exp.ConcatWs, expressions=[delim] + values)
def _parse_string_agg(self) -> exp.Expression: def _parse_string_agg(self) -> exp.Expression:
if self._match(TokenType.DISTINCT): if self._match(TokenType.DISTINCT):
args: t.List[t.Optional[exp.Expression]] = [ args: t.List[t.Optional[exp.Expression]] = [
@ -4181,15 +4195,28 @@ class Parser(metaclass=_Parser):
return None return None
return self.expression(exp.JSONKeyValue, this=key, expression=value) return self.expression(exp.JSONKeyValue, this=key, expression=value)
def _parse_format_json(self, this: t.Optional[exp.Expression]) -> t.Optional[exp.Expression]:
if not this or not self._match_text_seq("FORMAT", "JSON"):
return this
return self.expression(exp.FormatJson, this=this)
def _parse_on_handling(self, on: str, *values: str) -> t.Optional[str]:
# Parses the "X ON Y" syntax, i.e. NULL ON NULL (Oracle, T-SQL)
for value in values:
if self._match_text_seq(value, "ON", on):
return f"{value} ON {on}"
return None
def _parse_json_object(self) -> exp.JSONObject: def _parse_json_object(self) -> exp.JSONObject:
star = self._parse_star() star = self._parse_star()
expressions = [star] if star else self._parse_csv(self._parse_json_key_value) expressions = (
[star]
null_handling = None if star
if self._match_text_seq("NULL", "ON", "NULL"): else self._parse_csv(lambda: self._parse_format_json(self._parse_json_key_value()))
null_handling = "NULL ON NULL" )
elif self._match_text_seq("ABSENT", "ON", "NULL"): null_handling = self._parse_on_handling("NULL", "NULL", "ABSENT")
null_handling = "ABSENT ON NULL"
unique_keys = None unique_keys = None
if self._match_text_seq("WITH", "UNIQUE"): if self._match_text_seq("WITH", "UNIQUE"):
@ -4199,8 +4226,9 @@ class Parser(metaclass=_Parser):
self._match_text_seq("KEYS") self._match_text_seq("KEYS")
return_type = self._match_text_seq("RETURNING") and self._parse_type() return_type = self._match_text_seq("RETURNING") and self._parse_format_json(
format_json = self._match_text_seq("FORMAT", "JSON") self._parse_type()
)
encoding = self._match_text_seq("ENCODING") and self._parse_var() encoding = self._match_text_seq("ENCODING") and self._parse_var()
return self.expression( return self.expression(
@ -4209,7 +4237,6 @@ class Parser(metaclass=_Parser):
null_handling=null_handling, null_handling=null_handling,
unique_keys=unique_keys, unique_keys=unique_keys,
return_type=return_type, return_type=return_type,
format_json=format_json,
encoding=encoding, encoding=encoding,
) )
@ -4979,9 +5006,12 @@ class Parser(metaclass=_Parser):
self._match_r_paren() self._match_r_paren()
return self.expression(exp.DictRange, this=this, min=min, max=max) return self.expression(exp.DictRange, this=this, min=min, max=max)
def _parse_comprehension(self, this: exp.Expression) -> exp.Comprehension: def _parse_comprehension(self, this: exp.Expression) -> t.Optional[exp.Comprehension]:
index = self._index
expression = self._parse_column() expression = self._parse_column()
self._match(TokenType.IN) if not self._match(TokenType.IN):
self._retreat(index - 1)
return None
iterator = self._parse_column() iterator = self._parse_column()
condition = self._parse_conjunction() if self._match_text_seq("IF") else None condition = self._parse_conjunction() if self._match_text_seq("IF") else None
return self.expression( return self.expression(
@ -5125,3 +5155,10 @@ class Parser(metaclass=_Parser):
else: else:
column.replace(dot_or_id) column.replace(dot_or_id)
return node return node
def _ensure_string_if_null(self, values: t.List[exp.Expression]) -> t.List[exp.Expression]:
return [
exp.func("COALESCE", exp.cast(value, "text"), exp.Literal.string(""))
for value in values
if value
]

View file

@ -108,6 +108,8 @@ class TokenType(AutoName):
LONGTEXT = auto() LONGTEXT = auto()
MEDIUMBLOB = auto() MEDIUMBLOB = auto()
LONGBLOB = auto() LONGBLOB = auto()
TINYBLOB = auto()
TINYTEXT = auto()
BINARY = auto() BINARY = auto()
VARBINARY = auto() VARBINARY = auto()
JSON = auto() JSON = auto()
@ -675,6 +677,7 @@ class Tokenizer(metaclass=_Tokenizer):
"BOOL": TokenType.BOOLEAN, "BOOL": TokenType.BOOLEAN,
"BOOLEAN": TokenType.BOOLEAN, "BOOLEAN": TokenType.BOOLEAN,
"BYTE": TokenType.TINYINT, "BYTE": TokenType.TINYINT,
"MEDIUMINT": TokenType.MEDIUMINT,
"TINYINT": TokenType.TINYINT, "TINYINT": TokenType.TINYINT,
"SHORT": TokenType.SMALLINT, "SHORT": TokenType.SMALLINT,
"SMALLINT": TokenType.SMALLINT, "SMALLINT": TokenType.SMALLINT,
@ -712,10 +715,16 @@ class Tokenizer(metaclass=_Tokenizer):
"STR": TokenType.TEXT, "STR": TokenType.TEXT,
"STRING": TokenType.TEXT, "STRING": TokenType.TEXT,
"TEXT": TokenType.TEXT, "TEXT": TokenType.TEXT,
"LONGTEXT": TokenType.LONGTEXT,
"MEDIUMTEXT": TokenType.MEDIUMTEXT,
"TINYTEXT": TokenType.TINYTEXT,
"CLOB": TokenType.TEXT, "CLOB": TokenType.TEXT,
"LONGVARCHAR": TokenType.TEXT, "LONGVARCHAR": TokenType.TEXT,
"BINARY": TokenType.BINARY, "BINARY": TokenType.BINARY,
"BLOB": TokenType.VARBINARY, "BLOB": TokenType.VARBINARY,
"LONGBLOB": TokenType.LONGBLOB,
"MEDIUMBLOB": TokenType.MEDIUMBLOB,
"TINYBLOB": TokenType.TINYBLOB,
"BYTEA": TokenType.VARBINARY, "BYTEA": TokenType.VARBINARY,
"VARBINARY": TokenType.VARBINARY, "VARBINARY": TokenType.VARBINARY,
"TIME": TokenType.TIME, "TIME": TokenType.TIME,
@ -1159,7 +1168,11 @@ class Tokenizer(metaclass=_Tokenizer):
escapes = self._STRING_ESCAPES if escapes is None else escapes escapes = self._STRING_ESCAPES if escapes is None else escapes
while True: while True:
if self._char in escapes and (self._peek == delimiter or self._peek in escapes): if (
self._char in escapes
and (self._peek == delimiter or self._peek in escapes)
and (self._char not in self._QUOTES or self._char == self._peek)
):
if self._peek == delimiter: if self._peek == delimiter:
text += self._peek text += self._peek
else: else:

View file

@ -6,30 +6,19 @@ class TestClickhouse(Validator):
dialect = "clickhouse" dialect = "clickhouse"
def test_clickhouse(self): def test_clickhouse(self):
self.validate_all( string_types = [
"DATE_ADD('day', 1, x)", "BLOB",
read={ "LONGBLOB",
"clickhouse": "dateAdd(day, 1, x)", "LONGTEXT",
"presto": "DATE_ADD('day', 1, x)", "MEDIUMBLOB",
}, "MEDIUMTEXT",
write={ "TINYBLOB",
"clickhouse": "DATE_ADD('day', 1, x)", "TINYTEXT",
"presto": "DATE_ADD('day', 1, x)", "VARCHAR(255)",
"": "DATE_ADD(x, 1, 'day')", ]
},
) for string_type in string_types:
self.validate_all( self.validate_identity(f"CAST(x AS {string_type})", "CAST(x AS String)")
"DATE_DIFF('day', a, b)",
read={
"clickhouse": "dateDiff('day', a, b)",
"presto": "DATE_DIFF('day', a, b)",
},
write={
"clickhouse": "DATE_DIFF('day', a, b)",
"presto": "DATE_DIFF('day', a, b)",
"": "DATEDIFF(b, a, day)",
},
)
expr = parse_one("count(x)") expr = parse_one("count(x)")
self.assertEqual(expr.sql(dialect="clickhouse"), "COUNT(x)") self.assertEqual(expr.sql(dialect="clickhouse"), "COUNT(x)")
@ -72,8 +61,8 @@ class TestClickhouse(Validator):
self.validate_identity("position(haystack, needle)") self.validate_identity("position(haystack, needle)")
self.validate_identity("position(haystack, needle, position)") self.validate_identity("position(haystack, needle, position)")
self.validate_identity("CAST(x AS DATETIME)") self.validate_identity("CAST(x AS DATETIME)")
self.validate_identity("CAST(x AS VARCHAR(255))", "CAST(x AS String)") self.validate_identity("CAST(x as MEDIUMINT)", "CAST(x AS Int32)")
self.validate_identity("CAST(x AS BLOB)", "CAST(x AS String)")
self.validate_identity( self.validate_identity(
'SELECT CAST(tuple(1 AS "a", 2 AS "b", 3.0 AS "c").2 AS Nullable(String))' 'SELECT CAST(tuple(1 AS "a", 2 AS "b", 3.0 AS "c").2 AS Nullable(String))'
) )
@ -93,6 +82,30 @@ class TestClickhouse(Validator):
"CREATE MATERIALIZED VIEW test_view (id UInt8) TO db.table1 AS SELECT * FROM test_data" "CREATE MATERIALIZED VIEW test_view (id UInt8) TO db.table1 AS SELECT * FROM test_data"
) )
self.validate_all(
"DATE_ADD('day', 1, x)",
read={
"clickhouse": "dateAdd(day, 1, x)",
"presto": "DATE_ADD('day', 1, x)",
},
write={
"clickhouse": "DATE_ADD('day', 1, x)",
"presto": "DATE_ADD('day', 1, x)",
"": "DATE_ADD(x, 1, 'day')",
},
)
self.validate_all(
"DATE_DIFF('day', a, b)",
read={
"clickhouse": "dateDiff('day', a, b)",
"presto": "DATE_DIFF('day', a, b)",
},
write={
"clickhouse": "DATE_DIFF('day', a, b)",
"presto": "DATE_DIFF('day', a, b)",
"": "DATEDIFF(b, a, day)",
},
)
self.validate_all( self.validate_all(
"SELECT xor(1, 0)", "SELECT xor(1, 0)",
read={ read={

View file

@ -19,6 +19,7 @@ class TestDuckDB(Validator):
parse_one("a // b", read="duckdb").assert_is(exp.IntDiv).sql(dialect="duckdb"), "a // b" parse_one("a // b", read="duckdb").assert_is(exp.IntDiv).sql(dialect="duckdb"), "a // b"
) )
self.validate_identity("VAR_POP(a)")
self.validate_identity("SELECT * FROM foo ASOF LEFT JOIN bar ON a = b") self.validate_identity("SELECT * FROM foo ASOF LEFT JOIN bar ON a = b")
self.validate_identity("PIVOT Cities ON Year USING SUM(Population)") self.validate_identity("PIVOT Cities ON Year USING SUM(Population)")
self.validate_identity("PIVOT Cities ON Year USING FIRST(Population)") self.validate_identity("PIVOT Cities ON Year USING FIRST(Population)")
@ -34,6 +35,9 @@ class TestDuckDB(Validator):
self.validate_identity("SELECT (x, x + 1, y) FROM (SELECT 1 AS x, 'a' AS y)") self.validate_identity("SELECT (x, x + 1, y) FROM (SELECT 1 AS x, 'a' AS y)")
self.validate_identity("SELECT a.x FROM (SELECT {'x': 1, 'y': 2, 'z': 3} AS a)") self.validate_identity("SELECT a.x FROM (SELECT {'x': 1, 'y': 2, 'z': 3} AS a)")
self.validate_identity("ATTACH DATABASE ':memory:' AS new_database") self.validate_identity("ATTACH DATABASE ':memory:' AS new_database")
self.validate_identity("FROM x SELECT x UNION SELECT 1", "SELECT x FROM x UNION SELECT 1")
self.validate_identity("FROM (FROM tbl)", "SELECT * FROM (SELECT * FROM tbl)")
self.validate_identity("FROM tbl", "SELECT * FROM tbl")
self.validate_identity( self.validate_identity(
"SELECT {'yes': 'duck', 'maybe': 'goose', 'huh': NULL, 'no': 'heron'}" "SELECT {'yes': 'duck', 'maybe': 'goose', 'huh': NULL, 'no': 'heron'}"
) )
@ -53,13 +57,19 @@ class TestDuckDB(Validator):
"SELECT * FROM (PIVOT Cities ON Year USING SUM(Population) GROUP BY Country) AS pivot_alias" "SELECT * FROM (PIVOT Cities ON Year USING SUM(Population) GROUP BY Country) AS pivot_alias"
) )
self.validate_identity("FROM x SELECT x UNION SELECT 1", "SELECT x FROM x UNION SELECT 1")
self.validate_all("FROM (FROM tbl)", write={"duckdb": "SELECT * FROM (SELECT * FROM tbl)"})
self.validate_all("FROM tbl", write={"duckdb": "SELECT * FROM tbl"})
self.validate_all("0b1010", write={"": "0 AS b1010"}) self.validate_all("0b1010", write={"": "0 AS b1010"})
self.validate_all("0x1010", write={"": "0 AS x1010"}) self.validate_all("0x1010", write={"": "0 AS x1010"})
self.validate_all("x ~ y", write={"duckdb": "REGEXP_MATCHES(x, y)"}) self.validate_all("x ~ y", write={"duckdb": "REGEXP_MATCHES(x, y)"})
self.validate_all("SELECT * FROM 'x.y'", write={"duckdb": 'SELECT * FROM "x.y"'}) self.validate_all("SELECT * FROM 'x.y'", write={"duckdb": 'SELECT * FROM "x.y"'})
self.validate_all(
"VAR_POP(x)",
read={
"": "VARIANCE_POP(x)",
},
write={
"": "VARIANCE_POP(x)",
},
)
self.validate_all( self.validate_all(
"DATE_DIFF('day', CAST(b AS DATE), CAST(a AS DATE))", "DATE_DIFF('day', CAST(b AS DATE), CAST(a AS DATE))",
read={ read={

View file

@ -26,6 +26,9 @@ class TestMySQL(Validator):
self.validate_identity("CREATE TABLE foo (a BIGINT, INDEX USING BTREE (b))") self.validate_identity("CREATE TABLE foo (a BIGINT, INDEX USING BTREE (b))")
self.validate_identity("CREATE TABLE foo (a BIGINT, FULLTEXT INDEX (b))") self.validate_identity("CREATE TABLE foo (a BIGINT, FULLTEXT INDEX (b))")
self.validate_identity("CREATE TABLE foo (a BIGINT, SPATIAL INDEX (b))") self.validate_identity("CREATE TABLE foo (a BIGINT, SPATIAL INDEX (b))")
self.validate_identity(
"CREATE TABLE `x` (`username` VARCHAR(200), PRIMARY KEY (`username`(16)))"
)
self.validate_identity( self.validate_identity(
"UPDATE items SET items.price = 0 WHERE items.id >= 5 ORDER BY items.id LIMIT 10" "UPDATE items SET items.price = 0 WHERE items.id >= 5 ORDER BY items.id LIMIT 10"
) )
@ -204,21 +207,21 @@ class TestMySQL(Validator):
self.validate_identity("CAST(x AS MEDIUMINT) + CAST(y AS YEAR(4))") self.validate_identity("CAST(x AS MEDIUMINT) + CAST(y AS YEAR(4))")
self.validate_all( self.validate_all(
"CAST(x AS MEDIUMTEXT) + CAST(y AS LONGTEXT)", "CAST(x AS MEDIUMTEXT) + CAST(y AS LONGTEXT) + CAST(z AS TINYTEXT)",
read={ read={
"mysql": "CAST(x AS MEDIUMTEXT) + CAST(y AS LONGTEXT)", "mysql": "CAST(x AS MEDIUMTEXT) + CAST(y AS LONGTEXT) + CAST(z AS TINYTEXT)",
}, },
write={ write={
"spark": "CAST(x AS TEXT) + CAST(y AS TEXT)", "spark": "CAST(x AS TEXT) + CAST(y AS TEXT) + CAST(z AS TEXT)",
}, },
) )
self.validate_all( self.validate_all(
"CAST(x AS MEDIUMBLOB) + CAST(y AS LONGBLOB)", "CAST(x AS MEDIUMBLOB) + CAST(y AS LONGBLOB) + CAST(z AS TINYBLOB)",
read={ read={
"mysql": "CAST(x AS MEDIUMBLOB) + CAST(y AS LONGBLOB)", "mysql": "CAST(x AS MEDIUMBLOB) + CAST(y AS LONGBLOB) + CAST(z AS TINYBLOB)",
}, },
write={ write={
"spark": "CAST(x AS BLOB) + CAST(y AS BLOB)", "spark": "CAST(x AS BLOB) + CAST(y AS BLOB) + CAST(z AS BLOB)",
}, },
) )
self.validate_all("CAST(x AS TIMESTAMP)", write={"mysql": "CAST(x AS DATETIME)"}) self.validate_all("CAST(x AS TIMESTAMP)", write={"mysql": "CAST(x AS DATETIME)"})
@ -240,6 +243,15 @@ class TestMySQL(Validator):
) )
def test_escape(self): def test_escape(self):
self.validate_identity("""'"abc"'""")
self.validate_identity(
r"'\'a'",
"'''a'",
)
self.validate_identity(
'''"'abc'"''',
"'''abc'''",
)
self.validate_all( self.validate_all(
r"'a \' b '' '", r"'a \' b '' '",
write={ write={
@ -525,6 +537,7 @@ class TestMySQL(Validator):
"mysql": "SELECT DATE(DATE_SUB(`dt`, INTERVAL (DAYOFMONTH(`dt`) - 1) DAY)) AS __timestamp FROM tableT", "mysql": "SELECT DATE(DATE_SUB(`dt`, INTERVAL (DAYOFMONTH(`dt`) - 1) DAY)) AS __timestamp FROM tableT",
}, },
) )
self.validate_identity("SELECT name FROM temp WHERE name = ? FOR UPDATE")
self.validate_all( self.validate_all(
"SELECT a FROM tbl FOR UPDATE", "SELECT a FROM tbl FOR UPDATE",
write={ write={

View file

@ -6,6 +6,7 @@ class TestOracle(Validator):
dialect = "oracle" dialect = "oracle"
def test_oracle(self): def test_oracle(self):
self.validate_identity("SELECT JSON_OBJECT(k1: v1 FORMAT JSON, k2: v2 FORMAT JSON)")
self.validate_identity("SELECT JSON_OBJECT('name': first_name || ' ' || last_name) FROM t") self.validate_identity("SELECT JSON_OBJECT('name': first_name || ' ' || last_name) FROM t")
self.validate_identity("COALESCE(c1, c2, c3)") self.validate_identity("COALESCE(c1, c2, c3)")
self.validate_identity("SELECT * FROM TABLE(foo)") self.validate_identity("SELECT * FROM TABLE(foo)")
@ -25,6 +26,15 @@ class TestOracle(Validator):
self.validate_identity("SELECT * FROM table_name@dblink_name.database_link_domain") self.validate_identity("SELECT * FROM table_name@dblink_name.database_link_domain")
self.validate_identity("SELECT * FROM table_name SAMPLE (25) s") self.validate_identity("SELECT * FROM table_name SAMPLE (25) s")
self.validate_identity("SELECT * FROM V$SESSION") self.validate_identity("SELECT * FROM V$SESSION")
self.validate_identity(
"SELECT JSON_ARRAYAGG(JSON_OBJECT('RNK': RNK, 'RATING_CODE': RATING_CODE, 'DATE_VALUE': DATE_VALUE, 'AGENT_ID': AGENT_ID RETURNING CLOB) RETURNING CLOB) AS JSON_DATA FROM tablename"
)
self.validate_identity(
"SELECT JSON_ARRAY(FOO() FORMAT JSON, BAR() NULL ON NULL RETURNING CLOB STRICT)"
)
self.validate_identity(
"SELECT JSON_ARRAYAGG(FOO() FORMAT JSON ORDER BY bar NULL ON NULL RETURNING CLOB STRICT)"
)
self.validate_identity( self.validate_identity(
"SELECT COUNT(1) INTO V_Temp FROM TABLE(CAST(somelist AS data_list)) WHERE col LIKE '%contact'" "SELECT COUNT(1) INTO V_Temp FROM TABLE(CAST(somelist AS data_list)) WHERE col LIKE '%contact'"
) )
@ -190,3 +200,21 @@ MATCH_RECOGNIZE (
) MR""", ) MR""",
pretty=True, pretty=True,
) )
def test_json_table(self):
self.validate_identity(
"SELECT * FROM JSON_TABLE(foo FORMAT JSON, 'bla' ERROR ON ERROR NULL ON EMPTY COLUMNS (foo PATH 'bar'))"
)
self.validate_identity(
"SELECT * FROM JSON_TABLE(foo FORMAT JSON, 'bla' ERROR ON ERROR NULL ON EMPTY COLUMNS foo PATH 'bar')",
"SELECT * FROM JSON_TABLE(foo FORMAT JSON, 'bla' ERROR ON ERROR NULL ON EMPTY COLUMNS (foo PATH 'bar'))",
)
self.validate_identity(
"""SELECT
CASE WHEN DBMS_LOB.GETLENGTH(info) < 32000 THEN DBMS_LOB.SUBSTR(info) END AS info_txt,
info AS info_clob
FROM schemaname.tablename ar
INNER JOIN JSON_TABLE(:emps, '$[*]' COLUMNS (empno NUMBER PATH '$')) jt
ON ar.empno = jt.empno""",
pretty=True,
)

View file

@ -133,6 +133,9 @@ class TestPostgres(Validator):
alter_table_only = """ALTER TABLE ONLY "Album" ADD CONSTRAINT "FK_AlbumArtistId" FOREIGN KEY ("ArtistId") REFERENCES "Artist" ("ArtistId") ON DELETE NO ACTION ON UPDATE NO ACTION""" alter_table_only = """ALTER TABLE ONLY "Album" ADD CONSTRAINT "FK_AlbumArtistId" FOREIGN KEY ("ArtistId") REFERENCES "Artist" ("ArtistId") ON DELETE NO ACTION ON UPDATE NO ACTION"""
expr = parse_one(alter_table_only) expr = parse_one(alter_table_only)
# Checks that user-defined types are parsed into DataType instead of Identifier
parse_one("CREATE TABLE t (a udt)").this.expressions[0].args["kind"].assert_is(exp.DataType)
self.assertIsInstance(expr, exp.AlterTable) self.assertIsInstance(expr, exp.AlterTable)
self.assertEqual(expr.sql(dialect="postgres"), alter_table_only) self.assertEqual(expr.sql(dialect="postgres"), alter_table_only)

View file

@ -360,3 +360,17 @@ class TestRedshift(Validator):
"redshift": "CREATE OR REPLACE VIEW v1 AS SELECT cola, colb FROM t1 WITH NO SCHEMA BINDING", "redshift": "CREATE OR REPLACE VIEW v1 AS SELECT cola, colb FROM t1 WITH NO SCHEMA BINDING",
}, },
) )
def test_concat(self):
self.validate_all(
"SELECT CONCAT('abc', 'def')",
write={
"redshift": "SELECT COALESCE(CAST('abc' AS VARCHAR(MAX)), '') || COALESCE(CAST('def' AS VARCHAR(MAX)), '')",
},
)
self.validate_all(
"SELECT CONCAT_WS('DELIM', 'abc', 'def', 'ghi')",
write={
"redshift": "SELECT COALESCE(CAST('abc' AS VARCHAR(MAX)), '') || 'DELIM' || COALESCE(CAST('def' AS VARCHAR(MAX)), '') || 'DELIM' || COALESCE(CAST('ghi' AS VARCHAR(MAX)), '')",
},
)

View file

@ -717,6 +717,7 @@ UPDATE tbl_name SET foo = 123, bar = 345
UPDATE db.tbl_name SET foo = 123 WHERE tbl_name.bar = 234 UPDATE db.tbl_name SET foo = 123 WHERE tbl_name.bar = 234
UPDATE db.tbl_name SET foo = 123, foo_1 = 234 WHERE tbl_name.bar = 234 UPDATE db.tbl_name SET foo = 123, foo_1 = 234 WHERE tbl_name.bar = 234
UPDATE products SET price = price * 1.10 WHERE price <= 99.99 RETURNING name, price AS new_price UPDATE products SET price = price * 1.10 WHERE price <= 99.99 RETURNING name, price AS new_price
UPDATE t1 AS a, t2 AS b, t3 AS c LEFT JOIN t4 AS d ON c.id = d.id SET a.id = 1
TRUNCATE TABLE x TRUNCATE TABLE x
OPTIMIZE TABLE y OPTIMIZE TABLE y
VACUUM FREEZE my_table VACUUM FREEZE my_table

View file

@ -310,6 +310,21 @@ FROM
t1; t1;
SELECT x.a AS a, x.b AS b, ROW_NUMBER() OVER (PARTITION BY x.a ORDER BY x.a) AS row_num FROM x AS x; SELECT x.a AS a, x.b AS b, ROW_NUMBER() OVER (PARTITION BY x.a ORDER BY x.a) AS row_num FROM x AS x;
# title: Don't merge window functions, inner table is aliased in outer query
with t1 as (
SELECT
ROW_NUMBER() OVER (PARTITION BY x.a ORDER BY x.a) as row_num
FROM
x
)
SELECT
t2.row_num
FROM
t1 AS t2
WHERE
t2.row_num = 2;
WITH t1 AS (SELECT ROW_NUMBER() OVER (PARTITION BY x.a ORDER BY x.a) AS row_num FROM x AS x) SELECT t2.row_num AS row_num FROM t1 AS t2 WHERE t2.row_num = 2;
# title: Values Test # title: Values Test
# dialect: spark # dialect: spark
WITH t1 AS ( WITH t1 AS (

View file

@ -987,3 +987,39 @@ SELECT
FROM "SALES" AS "SALES" FROM "SALES" AS "SALES"
WHERE WHERE
"SALES"."INSERT_TS" > '2023-08-07 21:03:35.590 -0700'; "SALES"."INSERT_TS" > '2023-08-07 21:03:35.590 -0700';
# title: using join without select *
# execute: false
with
alias1 as (select * from table1),
alias2 as (select * from table2),
alias3 as (
select
cid,
min(od) as m_od,
count(odi) as c_od,
from alias2
group by 1
)
select
alias1.cid,
alias3.m_od,
coalesce(alias3.c_od, 0) as c_od,
from alias1
left join alias3 using (cid);
WITH "alias3" AS (
SELECT
"table2"."cid" AS "cid",
MIN("table2"."od") AS "m_od",
COUNT("table2"."odi") AS "c_od"
FROM "table2" AS "table2"
GROUP BY
"table2"."cid"
)
SELECT
"table1"."cid" AS "cid",
"alias3"."m_od" AS "m_od",
COALESCE("alias3"."c_od", 0) AS "c_od"
FROM "table1" AS "table1"
LEFT JOIN "alias3"
ON "table1"."cid" = "alias3"."cid";

View file

@ -858,3 +858,8 @@ FROM READ_CSV('tests/fixtures/optimizer/tpc-h/nation.csv.gz', 'delimiter', '|')
), ),
parse_one('SELECT "a"."a" AS "a", "a"."b" AS "b" FROM "a" AS "a"'), parse_one('SELECT "a"."a" AS "a", "a"."b" AS "b" FROM "a" AS "a"'),
) )
def test_semistructured(self):
query = parse_one("select a.b:c from d", read="snowflake")
qualified = optimizer.qualify.qualify(query)
self.assertEqual(qualified.expressions[0].alias, "c")

View file

@ -719,3 +719,18 @@ class TestParser(unittest.TestCase):
self.assertEqual(ast.find(exp.Interval).this.sql(), "'71'") self.assertEqual(ast.find(exp.Interval).this.sql(), "'71'")
self.assertEqual(ast.find(exp.Interval).unit.assert_is(exp.Var).sql(), "days") self.assertEqual(ast.find(exp.Interval).unit.assert_is(exp.Var).sql(), "days")
def test_parse_concat_ws(self):
ast = parse_one("CONCAT_WS(' ', 'John', 'Doe')")
self.assertEqual(ast.sql(), "CONCAT_WS(' ', 'John', 'Doe')")
self.assertEqual(ast.expressions[0].sql(), "' '")
self.assertEqual(ast.expressions[1].sql(), "'John'")
self.assertEqual(ast.expressions[2].sql(), "'Doe'")
# Ensure we can parse without argument when error level is ignore
ast = parse(
"CONCAT_WS()",
error_level=ErrorLevel.IGNORE,
)
self.assertEqual(ast[0].sql(), "CONCAT_WS()")