sqlglot 27.7.0__py3-none-any.whl → 27.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sqlglot/dialects/doris.py CHANGED
@@ -65,7 +65,11 @@ class Doris(MySQL):
65
65
  **MySQL.Parser.PROPERTY_PARSERS,
66
66
  "PROPERTIES": lambda self: self._parse_wrapped_properties(),
67
67
  "UNIQUE": lambda self: self._parse_composite_key_property(exp.UniqueKeyProperty),
68
+ # Plain KEY without UNIQUE/DUPLICATE/AGGREGATE prefixes should be treated as UniqueKeyProperty with unique=False
69
+ "KEY": lambda self: self._parse_composite_key_property(exp.UniqueKeyProperty),
68
70
  "PARTITION BY": lambda self: self._parse_partition_by_opt_range(),
71
+ "BUILD": lambda self: self._parse_build_property(),
72
+ "REFRESH": lambda self: self._parse_refresh_property(),
69
73
  }
70
74
 
71
75
  def _parse_partitioning_granularity_dynamic(self) -> exp.PartitionByRangePropertyDynamic:
@@ -104,9 +108,27 @@ class Doris(MySQL):
104
108
  part_range = self.expression(exp.PartitionRange, this=name, expressions=values)
105
109
  return self.expression(exp.Partition, expressions=[part_range])
106
110
 
111
+ def _parse_partition_definition_list(self) -> exp.Partition:
112
+ # PARTITION <name> VALUES IN (<value_csv>)
113
+ self._match_text_seq("PARTITION")
114
+ name = self._parse_id_var()
115
+ self._match_text_seq("VALUES", "IN")
116
+ values = self._parse_wrapped_csv(self._parse_expression)
117
+ part_list = self.expression(exp.PartitionList, this=name, expressions=values)
118
+ return self.expression(exp.Partition, expressions=[part_list])
119
+
107
120
  def _parse_partition_by_opt_range(
108
121
  self,
109
- ) -> exp.PartitionedByProperty | exp.PartitionByRangeProperty:
122
+ ) -> exp.PartitionedByProperty | exp.PartitionByRangeProperty | exp.PartitionByListProperty:
123
+ if self._match_text_seq("LIST"):
124
+ return self.expression(
125
+ exp.PartitionByListProperty,
126
+ partition_expressions=self._parse_wrapped_id_vars(),
127
+ create_expressions=self._parse_wrapped_csv(
128
+ self._parse_partition_definition_list
129
+ ),
130
+ )
131
+
110
132
  if not self._match_text_seq("RANGE"):
111
133
  return super()._parse_partitioned_by()
112
134
 
@@ -128,6 +150,28 @@ class Doris(MySQL):
128
150
  create_expressions=create_expressions,
129
151
  )
130
152
 
153
+ def _parse_build_property(self) -> exp.BuildProperty:
154
+ return self.expression(exp.BuildProperty, this=self._parse_var(upper=True))
155
+
156
+ def _parse_refresh_property(self) -> exp.RefreshTriggerProperty:
157
+ method = self._parse_var(upper=True)
158
+
159
+ self._match(TokenType.ON)
160
+
161
+ kind = self._match_texts(("MANUAL", "COMMIT", "SCHEDULE")) and self._prev.text.upper()
162
+ every = self._match_text_seq("EVERY") and self._parse_number()
163
+ unit = self._parse_var(any_token=True) if every else None
164
+ starts = self._match_text_seq("STARTS") and self._parse_string()
165
+
166
+ return self.expression(
167
+ exp.RefreshTriggerProperty,
168
+ method=method,
169
+ kind=kind,
170
+ every=every,
171
+ unit=unit,
172
+ starts=starts,
173
+ )
174
+
131
175
  class Generator(MySQL.Generator):
132
176
  LAST_DAY_SUPPORTS_DATE_PART = False
133
177
  VARCHAR_REQUIRES_SIZE = False
@@ -145,7 +189,10 @@ class Doris(MySQL):
145
189
  **MySQL.Generator.PROPERTIES_LOCATION,
146
190
  exp.UniqueKeyProperty: exp.Properties.Location.POST_SCHEMA,
147
191
  exp.PartitionByRangeProperty: exp.Properties.Location.POST_SCHEMA,
192
+ exp.PartitionByListProperty: exp.Properties.Location.POST_SCHEMA,
148
193
  exp.PartitionedByProperty: exp.Properties.Location.POST_SCHEMA,
194
+ exp.BuildProperty: exp.Properties.Location.POST_SCHEMA,
195
+ exp.RefreshTriggerProperty: exp.Properties.Location.POST_SCHEMA,
149
196
  }
150
197
 
151
198
  CAST_MAPPING = {}
@@ -662,9 +709,18 @@ class Doris(MySQL):
662
709
  "year",
663
710
  }
664
711
 
712
+ def uniquekeyproperty_sql(
713
+ self, expression: exp.UniqueKeyProperty, prefix: str = "UNIQUE KEY"
714
+ ) -> str:
715
+ create_stmt = expression.find_ancestor(exp.Create)
716
+ if create_stmt and create_stmt.args["properties"].find(exp.MaterializedProperty):
717
+ return super().uniquekeyproperty_sql(expression, prefix="KEY")
718
+
719
+ return super().uniquekeyproperty_sql(expression)
720
+
665
721
  def partition_sql(self, expression: exp.Partition) -> str:
666
722
  parent = expression.parent
667
- if isinstance(parent, exp.PartitionByRangeProperty):
723
+ if isinstance(parent, (exp.PartitionByRangeProperty, exp.PartitionByListProperty)):
668
724
  return ", ".join(self.sql(e) for e in expression.expressions)
669
725
  return super().partition_sql(expression)
670
726
 
@@ -685,7 +741,9 @@ class Doris(MySQL):
685
741
 
686
742
  return f"PARTITION {name} VALUES LESS THAN ({self.sql(values[0])})"
687
743
 
688
- def partitionbyrangepropertydynamic_sql(self, expression):
744
+ def partitionbyrangepropertydynamic_sql(
745
+ self, expression: exp.PartitionByRangePropertyDynamic
746
+ ) -> str:
689
747
  # Generates: FROM ("start") TO ("end") INTERVAL N UNIT
690
748
  start = self.sql(expression, "start")
691
749
  end = self.sql(expression, "end")
@@ -699,15 +757,25 @@ class Doris(MySQL):
699
757
 
700
758
  return f"FROM ({start}) TO ({end}) {interval}"
701
759
 
702
- def partitionbyrangeproperty_sql(self, expression):
703
- partition_expressions = ", ".join(
704
- self.sql(e) for e in expression.args.get("partition_expressions") or []
760
+ def partitionbyrangeproperty_sql(self, expression: exp.PartitionByRangeProperty) -> str:
761
+ partition_expressions = self.expressions(
762
+ expression, key="partition_expressions", indent=False
705
763
  )
706
- create_expressions = expression.args.get("create_expressions") or []
707
- # Handle both static and dynamic partition definitions
708
- create_sql = ", ".join(self.sql(e) for e in create_expressions)
764
+ create_sql = self.expressions(expression, key="create_expressions", indent=False)
709
765
  return f"PARTITION BY RANGE ({partition_expressions}) ({create_sql})"
710
766
 
767
+ def partitionbylistproperty_sql(self, expression: exp.PartitionByListProperty) -> str:
768
+ partition_expressions = self.expressions(
769
+ expression, key="partition_expressions", indent=False
770
+ )
771
+ create_sql = self.expressions(expression, key="create_expressions", indent=False)
772
+ return f"PARTITION BY LIST ({partition_expressions}) ({create_sql})"
773
+
774
+ def partitionlist_sql(self, expression: exp.PartitionList) -> str:
775
+ name = self.sql(expression, "this")
776
+ values = self.expressions(expression, indent=False)
777
+ return f"PARTITION {name} VALUES IN ({values})"
778
+
711
779
  def partitionedbyproperty_sql(self, expression: exp.PartitionedByProperty) -> str:
712
780
  node = expression.this
713
781
  if isinstance(node, exp.Schema):
@@ -1,36 +1,79 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import typing as t
3
4
  from sqlglot import expressions as exp
4
5
  from sqlglot import parser, generator, tokens
5
- from sqlglot.dialects.dialect import Dialect, build_formatted_time, unit_to_var
6
- import typing as t
6
+ from sqlglot.dialects.dialect import (
7
+ Dialect,
8
+ build_timetostr_or_tochar,
9
+ build_formatted_time,
10
+ build_date_delta,
11
+ rename_func,
12
+ )
13
+ from sqlglot.helper import seq_get
14
+ from sqlglot.tokens import TokenType
7
15
 
8
- DATE_DELTA = t.Union[
9
- exp.DateAdd,
10
- exp.DateSub,
11
- ]
16
+ if t.TYPE_CHECKING:
17
+ from sqlglot.dialects.dialect import DialectType
18
+
19
+ DATE_DELTA = t.Union[exp.DateAdd, exp.DateSub]
12
20
 
13
21
 
14
22
  def _date_delta_sql(name: str) -> t.Callable[[Dremio.Generator, DATE_DELTA], str]:
15
23
  def _delta_sql(self: Dremio.Generator, expression: DATE_DELTA) -> str:
16
- unit = expression.text("unit")
24
+ unit = expression.text("unit").upper()
17
25
 
18
- if not unit or unit.upper() == "DAY":
26
+ # Fallback to default behavior if unit is missing or 'DAY'
27
+ if not unit or unit == "DAY":
19
28
  return self.func(name, expression.this, expression.expression)
20
29
 
21
- # to support units we need to use TIMESTAMPADD function
22
- increment = expression.expression
23
- if isinstance(expression, exp.DateSub):
24
- if isinstance(increment, exp.Literal):
25
- value = increment.to_py() if increment.is_number else int(increment.name)
26
- increment = exp.Literal.number(value * -1)
27
- else:
28
- increment *= exp.Literal.number(-1)
29
- return self.func("TIMESTAMPADD", unit_to_var(expression), increment, expression.this)
30
+ this_sql = self.sql(expression, "this")
31
+ expr_sql = self.sql(expression, "expression")
32
+
33
+ interval_sql = f"CAST({expr_sql} AS INTERVAL {unit})"
34
+ return f"{name}({this_sql}, {interval_sql})"
30
35
 
31
36
  return _delta_sql
32
37
 
33
38
 
39
+ def to_char_is_numeric_handler(args: t.List, dialect: DialectType) -> exp.TimeToStr | exp.ToChar:
40
+ expression = build_timetostr_or_tochar(args, dialect)
41
+ fmt = seq_get(args, 1)
42
+
43
+ if fmt and isinstance(expression, exp.ToChar) and fmt.is_string and "#" in fmt.name:
44
+ # Only mark as numeric if format is a literal containing #
45
+ expression.set("is_numeric", True)
46
+
47
+ return expression
48
+
49
+
50
+ def build_date_delta_with_cast_interval(
51
+ expression_class: t.Type[DATE_DELTA],
52
+ ) -> t.Callable[[t.List[exp.Expression]], exp.Expression]:
53
+ fallback_builder = build_date_delta(expression_class)
54
+
55
+ def _builder(args):
56
+ if len(args) == 2:
57
+ date_arg, interval_arg = args
58
+
59
+ if (
60
+ isinstance(interval_arg, exp.Cast)
61
+ and isinstance(interval_arg.to, exp.DataType)
62
+ and isinstance(interval_arg.to.this, exp.Interval)
63
+ ):
64
+ return expression_class(
65
+ this=date_arg,
66
+ expression=interval_arg.this,
67
+ unit=interval_arg.to.this.unit,
68
+ )
69
+
70
+ return expression_class(this=date_arg, expression=interval_arg)
71
+
72
+ return fallback_builder(args)
73
+
74
+ return _builder
75
+
76
+
34
77
  class Dremio(Dialect):
35
78
  SUPPORTS_USER_DEFINED_TYPES = False
36
79
  CONCAT_COALESCE = True
@@ -89,14 +132,39 @@ class Dremio(Dialect):
89
132
  "tzo": "%z", # numeric offset (+0200)
90
133
  }
91
134
 
135
+ class Tokenizer(tokens.Tokenizer):
136
+ COMMENTS = ["--", "//", ("/*", "*/")]
137
+
92
138
  class Parser(parser.Parser):
93
139
  LOG_DEFAULTS_TO_LN = True
94
140
 
141
+ NO_PAREN_FUNCTION_PARSERS = {
142
+ **parser.Parser.NO_PAREN_FUNCTION_PARSERS,
143
+ "CURRENT_DATE_UTC": lambda self: self._parse_current_date_utc(),
144
+ }
145
+
95
146
  FUNCTIONS = {
96
147
  **parser.Parser.FUNCTIONS,
97
- "TO_CHAR": build_formatted_time(exp.TimeToStr, "dremio"),
148
+ "TO_CHAR": to_char_is_numeric_handler,
149
+ "DATE_FORMAT": build_formatted_time(exp.TimeToStr, "dremio"),
150
+ "TO_DATE": build_formatted_time(exp.TsOrDsToDate, "dremio"),
151
+ "DATE_ADD": build_date_delta_with_cast_interval(exp.DateAdd),
152
+ "DATE_SUB": build_date_delta_with_cast_interval(exp.DateSub),
153
+ "ARRAY_GENERATE_RANGE": exp.GenerateSeries.from_arg_list,
98
154
  }
99
155
 
156
+ def _parse_current_date_utc(self) -> exp.Cast:
157
+ if self._match(TokenType.L_PAREN):
158
+ self._match_r_paren()
159
+
160
+ return exp.Cast(
161
+ this=exp.AtTimeZone(
162
+ this=exp.CurrentTimestamp(),
163
+ zone=exp.Literal.string("UTC"),
164
+ ),
165
+ to=exp.DataType.build("DATE"),
166
+ )
167
+
100
168
  class Generator(generator.Generator):
101
169
  NVL2_SUPPORTED = False
102
170
  SUPPORTS_CONVERT_TIMEZONE = True
@@ -123,10 +191,11 @@ class Dremio(Dialect):
123
191
 
124
192
  TRANSFORMS = {
125
193
  **generator.Generator.TRANSFORMS,
194
+ exp.ToChar: rename_func("TO_CHAR"),
126
195
  exp.TimeToStr: lambda self, e: self.func("TO_CHAR", e.this, self.format_time(e)),
127
- exp.ToChar: lambda self, e: self.function_fallback_sql(e),
128
196
  exp.DateAdd: _date_delta_sql("DATE_ADD"),
129
197
  exp.DateSub: _date_delta_sql("DATE_SUB"),
198
+ exp.GenerateSeries: rename_func("ARRAY_GENERATE_RANGE"),
130
199
  }
131
200
 
132
201
  def datatype_sql(self, expression: exp.DataType) -> str:
@@ -141,5 +210,17 @@ class Dremio(Dialect):
141
210
 
142
211
  return super().datatype_sql(expression)
143
212
 
144
- class Tokenizer(tokens.Tokenizer):
145
- COMMENTS = ["--", "//", ("/*", "*/")]
213
+ def cast_sql(self, expression: exp.Cast, safe_prefix: str | None = None) -> str:
214
+ # Match: CAST(CURRENT_TIMESTAMP AT TIME ZONE 'UTC' AS DATE)
215
+ if expression.is_type(exp.DataType.Type.DATE):
216
+ at_time_zone = expression.this
217
+
218
+ if (
219
+ isinstance(at_time_zone, exp.AtTimeZone)
220
+ and isinstance(at_time_zone.this, exp.CurrentTimestamp)
221
+ and isinstance(at_time_zone.args["zone"], exp.Literal)
222
+ and at_time_zone.text("zone").upper() == "UTC"
223
+ ):
224
+ return "CURRENT_DATE_UTC"
225
+
226
+ return super().cast_sql(expression, safe_prefix)
@@ -4,7 +4,6 @@ import typing as t
4
4
 
5
5
  from sqlglot import exp, generator, parser, tokens, transforms
6
6
 
7
- from sqlglot.expressions import DATA_TYPE
8
7
  from sqlglot.dialects.dialect import (
9
8
  Dialect,
10
9
  JSON_EXTRACT_TYPE,
@@ -16,6 +15,7 @@ from sqlglot.dialects.dialect import (
16
15
  bool_xor_sql,
17
16
  build_default_decimal_type,
18
17
  count_if_to_sum,
18
+ date_delta_to_binary_interval_op,
19
19
  date_trunc_to_time,
20
20
  datestrtodate_sql,
21
21
  no_datetime_sql,
@@ -32,7 +32,6 @@ from sqlglot.dialects.dialect import (
32
32
  str_to_time_sql,
33
33
  timestamptrunc_sql,
34
34
  timestrtotime_sql,
35
- unit_to_var,
36
35
  unit_to_str,
37
36
  sha256_sql,
38
37
  build_regexp_extract,
@@ -45,38 +44,6 @@ from sqlglot.helper import seq_get
45
44
  from sqlglot.tokens import TokenType
46
45
  from sqlglot.parser import binary_range_parser
47
46
 
48
- DATETIME_DELTA = t.Union[
49
- exp.DateAdd, exp.TimeAdd, exp.DatetimeAdd, exp.TsOrDsAdd, exp.DateSub, exp.DatetimeSub
50
- ]
51
-
52
-
53
- def _date_delta_sql(self: DuckDB.Generator, expression: DATETIME_DELTA) -> str:
54
- this = expression.this
55
- unit = unit_to_var(expression)
56
- op = (
57
- "+"
58
- if isinstance(expression, (exp.DateAdd, exp.TimeAdd, exp.DatetimeAdd, exp.TsOrDsAdd))
59
- else "-"
60
- )
61
-
62
- to_type: t.Optional[DATA_TYPE] = None
63
- if isinstance(expression, exp.TsOrDsAdd):
64
- to_type = expression.return_type
65
- elif this.is_string:
66
- # Cast string literals (i.e function parameters) to the appropriate type for +/- interval to work
67
- to_type = (
68
- exp.DataType.Type.DATETIME
69
- if isinstance(expression, (exp.DatetimeAdd, exp.DatetimeSub))
70
- else exp.DataType.Type.DATE
71
- )
72
-
73
- this = exp.cast(this, to_type) if to_type else this
74
-
75
- expr = expression.expression
76
- interval = expr if isinstance(expr, exp.Interval) else exp.Interval(this=expr, unit=unit)
77
-
78
- return f"{self.sql(this)} {op} {self.sql(interval)}"
79
-
80
47
 
81
48
  # BigQuery -> DuckDB conversion for the DATE function
82
49
  def _date_sql(self: DuckDB.Generator, expression: exp.Date) -> str:
@@ -419,10 +386,12 @@ class DuckDB(Dialect):
419
386
  "JSON_EXTRACT_PATH": parser.build_extract_json_with_path(exp.JSONExtract),
420
387
  "JSON_EXTRACT_STRING": parser.build_extract_json_with_path(exp.JSONExtractScalar),
421
388
  "LIST_CONTAINS": exp.ArrayContains.from_arg_list,
389
+ "LIST_FILTER": exp.ArrayFilter.from_arg_list,
422
390
  "LIST_HAS": exp.ArrayContains.from_arg_list,
423
391
  "LIST_HAS_ANY": exp.ArrayOverlaps.from_arg_list,
424
392
  "LIST_REVERSE_SORT": _build_sort_array_desc,
425
393
  "LIST_SORT": exp.SortArray.from_arg_list,
394
+ "LIST_TRANSFORM": exp.Transform.from_arg_list,
426
395
  "LIST_VALUE": lambda args: exp.Array(expressions=args),
427
396
  "MAKE_TIME": exp.TimeFromParts.from_arg_list,
428
397
  "MAKE_TIMESTAMP": _build_make_timestamp,
@@ -676,6 +645,9 @@ class DuckDB(Dialect):
676
645
  exp.ArrayRemove: remove_from_array_using_filter,
677
646
  exp.ArraySort: _array_sort_sql,
678
647
  exp.ArraySum: rename_func("LIST_SUM"),
648
+ exp.ArrayUniqueAgg: lambda self, e: self.func(
649
+ "LIST", exp.Distinct(expressions=[e.this])
650
+ ),
679
651
  exp.BitwiseXor: rename_func("XOR"),
680
652
  exp.CommentColumnConstraint: no_comment_column_constraint_sql,
681
653
  exp.CurrentDate: lambda *_: "CURRENT_DATE",
@@ -687,14 +659,14 @@ class DuckDB(Dialect):
687
659
  exp.DayOfYear: rename_func("DAYOFYEAR"),
688
660
  exp.DataType: _datatype_sql,
689
661
  exp.Date: _date_sql,
690
- exp.DateAdd: _date_delta_sql,
662
+ exp.DateAdd: date_delta_to_binary_interval_op(),
691
663
  exp.DateFromParts: rename_func("MAKE_DATE"),
692
- exp.DateSub: _date_delta_sql,
664
+ exp.DateSub: date_delta_to_binary_interval_op(),
693
665
  exp.DateDiff: _date_diff_sql,
694
666
  exp.DateStrToDate: datestrtodate_sql,
695
667
  exp.Datetime: no_datetime_sql,
696
- exp.DatetimeSub: _date_delta_sql,
697
- exp.DatetimeAdd: _date_delta_sql,
668
+ exp.DatetimeSub: date_delta_to_binary_interval_op(),
669
+ exp.DatetimeAdd: date_delta_to_binary_interval_op(),
698
670
  exp.DateToDi: lambda self,
699
671
  e: f"CAST(STRFTIME({self.sql(e, 'this')}, {DuckDB.DATEINT_FORMAT}) AS INT)",
700
672
  exp.Decode: lambda self, e: encode_decode_sql(self, e, "DECODE", replace=False),
@@ -756,7 +728,7 @@ class DuckDB(Dialect):
756
728
  ),
757
729
  exp.Struct: _struct_sql,
758
730
  exp.Transform: rename_func("LIST_TRANSFORM"),
759
- exp.TimeAdd: _date_delta_sql,
731
+ exp.TimeAdd: date_delta_to_binary_interval_op(),
760
732
  exp.Time: no_time_sql,
761
733
  exp.TimeDiff: _timediff_sql,
762
734
  exp.Timestamp: no_timestamp_sql,
@@ -773,7 +745,7 @@ class DuckDB(Dialect):
773
745
  exp.TimeToUnix: rename_func("EPOCH"),
774
746
  exp.TsOrDiToDi: lambda self,
775
747
  e: f"CAST(SUBSTR(REPLACE(CAST({self.sql(e, 'this')} AS TEXT), '-', ''), 1, 8) AS INT)",
776
- exp.TsOrDsAdd: _date_delta_sql,
748
+ exp.TsOrDsAdd: date_delta_to_binary_interval_op(),
777
749
  exp.TsOrDsDiff: lambda self, e: self.func(
778
750
  "DATE_DIFF",
779
751
  f"'{e.args.get('unit') or 'DAY'}'",
@@ -1150,15 +1122,20 @@ class DuckDB(Dialect):
1150
1122
  return super().unnest_sql(expression)
1151
1123
 
1152
1124
  def ignorenulls_sql(self, expression: exp.IgnoreNulls) -> str:
1153
- if isinstance(expression.this, self.IGNORE_RESPECT_NULLS_WINDOW_FUNCTIONS):
1125
+ this = expression.this
1126
+
1127
+ if isinstance(this, self.IGNORE_RESPECT_NULLS_WINDOW_FUNCTIONS):
1154
1128
  # DuckDB should render IGNORE NULLS only for the general-purpose
1155
1129
  # window functions that accept it e.g. FIRST_VALUE(... IGNORE NULLS) OVER (...)
1156
1130
  return super().ignorenulls_sql(expression)
1157
1131
 
1158
- if not isinstance(expression.this, exp.AnyValue):
1132
+ if isinstance(this, exp.First):
1133
+ this = exp.AnyValue(this=this.this)
1134
+
1135
+ if not isinstance(this, exp.AnyValue):
1159
1136
  self.unsupported("IGNORE NULLS is not supported for non-window functions.")
1160
1137
 
1161
- return self.sql(expression, "this")
1138
+ return self.sql(this)
1162
1139
 
1163
1140
  def respectnulls_sql(self, expression: exp.RespectNulls) -> str:
1164
1141
  if isinstance(expression.this, self.IGNORE_RESPECT_NULLS_WINDOW_FUNCTIONS):
@@ -5,8 +5,10 @@ import typing as t
5
5
  from sqlglot import exp, generator, parser, tokens
6
6
  from sqlglot.dialects.dialect import (
7
7
  Dialect,
8
+ NormalizationStrategy,
8
9
  binary_from_function,
9
10
  build_formatted_time,
11
+ groupconcat_sql,
10
12
  rename_func,
11
13
  strposition_sql,
12
14
  timestrtotime_sql,
@@ -73,6 +75,17 @@ DATE_UNITS = {"DAY", "WEEK", "MONTH", "YEAR", "HOUR", "MINUTE", "SECOND"}
73
75
 
74
76
 
75
77
  class Exasol(Dialect):
78
+ # https://docs.exasol.com/db/latest/sql_references/basiclanguageelements.htm#SQLidentifier
79
+ NORMALIZATION_STRATEGY = NormalizationStrategy.UPPERCASE
80
+ # https://docs.exasol.com/db/latest/sql_references/data_types/datatypesoverview.htm
81
+ SUPPORTS_USER_DEFINED_TYPES = False
82
+ # https://docs.exasol.com/db/latest/sql/select.htm
83
+ SUPPORTS_SEMI_ANTI_JOIN = False
84
+ SUPPORTS_COLUMN_JOIN_MARKS = True
85
+ NULL_ORDERING = "nulls_are_last"
86
+ # https://docs.exasol.com/db/latest/sql_references/literals.htm#StringLiterals
87
+ CONCAT_COALESCE = True
88
+
76
89
  TIME_MAPPING = {
77
90
  "yyyy": "%Y",
78
91
  "YYYY": "%Y",
@@ -108,7 +121,9 @@ class Exasol(Dialect):
108
121
  # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/if.htm
109
122
  "ENDIF": TokenType.END,
110
123
  "LONG VARCHAR": TokenType.TEXT,
124
+ "SEPARATOR": TokenType.SEPARATOR,
111
125
  }
126
+ KEYWORDS.pop("DIV")
112
127
 
113
128
  class Parser(parser.Parser):
114
129
  FUNCTIONS = {
@@ -131,6 +146,7 @@ class Exasol(Dialect):
131
146
  "DATE_TRUNC": lambda args: exp.TimestampTrunc(
132
147
  this=seq_get(args, 1), unit=seq_get(args, 0)
133
148
  ),
149
+ "DIV": binary_from_function(exp.IntDiv),
134
150
  "EVERY": lambda args: exp.All(this=seq_get(args, 0)),
135
151
  "EDIT_DISTANCE": exp.Levenshtein.from_arg_list,
136
152
  "HASH_SHA": exp.SHA.from_arg_list,
@@ -174,6 +190,12 @@ class Exasol(Dialect):
174
190
  this=self._match(TokenType.IS) and self._parse_string(),
175
191
  ),
176
192
  }
193
+ FUNCTION_PARSERS = {
194
+ **parser.Parser.FUNCTION_PARSERS,
195
+ # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/listagg.htm
196
+ # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/group_concat.htm
197
+ **dict.fromkeys(("GROUP_CONCAT", "LISTAGG"), lambda self: self._parse_group_concat()),
198
+ }
177
199
 
178
200
  class Generator(generator.Generator):
179
201
  # https://docs.exasol.com/db/latest/sql_references/data_types/datatypedetails.htm#StringDataType
@@ -228,9 +250,14 @@ class Exasol(Dialect):
228
250
  # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/bit_xor.htm
229
251
  exp.BitwiseXor: rename_func("BIT_XOR"),
230
252
  exp.DateDiff: _date_diff_sql,
253
+ # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/div.htm#DIV
254
+ exp.IntDiv: rename_func("DIV"),
231
255
  exp.TsOrDsDiff: _date_diff_sql,
232
256
  exp.DateTrunc: lambda self, e: self.func("TRUNC", e.this, unit_to_str(e)),
233
257
  exp.DatetimeTrunc: timestamptrunc_sql(),
258
+ exp.GroupConcat: lambda self, e: groupconcat_sql(
259
+ self, e, func_name="LISTAGG", within_group=True
260
+ ),
234
261
  # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/edit_distance.htm#EDIT_DISTANCE
235
262
  exp.Levenshtein: unsupported_args("ins_cost", "del_cost", "sub_cost", "max_dist")(
236
263
  rename_func("EDIT_DISTANCE")
@@ -280,6 +307,7 @@ class Exasol(Dialect):
280
307
  exp.MD5Digest: rename_func("HASHTYPE_MD5"),
281
308
  # https://docs.exasol.com/db/latest/sql/create_view.htm
282
309
  exp.CommentColumnConstraint: lambda self, e: f"COMMENT IS {self.sql(e, 'this')}",
310
+ exp.WeekOfYear: rename_func("WEEK"),
283
311
  }
284
312
 
285
313
  def converttimezone_sql(self, expression: exp.ConvertTimezone) -> str:
sqlglot/dialects/mysql.py CHANGED
@@ -676,54 +676,6 @@ class MySQL(Dialect):
676
676
  parse_interval=parse_interval, fallback_to_identifier=fallback_to_identifier
677
677
  )
678
678
 
679
- def _parse_group_concat(self) -> t.Optional[exp.Expression]:
680
- def concat_exprs(
681
- node: t.Optional[exp.Expression], exprs: t.List[exp.Expression]
682
- ) -> exp.Expression:
683
- if isinstance(node, exp.Distinct) and len(node.expressions) > 1:
684
- concat_exprs = [
685
- self.expression(exp.Concat, expressions=node.expressions, safe=True)
686
- ]
687
- node.set("expressions", concat_exprs)
688
- return node
689
- if len(exprs) == 1:
690
- return exprs[0]
691
- return self.expression(exp.Concat, expressions=args, safe=True)
692
-
693
- args = self._parse_csv(self._parse_lambda)
694
-
695
- if args:
696
- order = args[-1] if isinstance(args[-1], exp.Order) else None
697
-
698
- if order:
699
- # Order By is the last (or only) expression in the list and has consumed the 'expr' before it,
700
- # remove 'expr' from exp.Order and add it back to args
701
- args[-1] = order.this
702
- order.set("this", concat_exprs(order.this, args))
703
-
704
- this = order or concat_exprs(args[0], args)
705
- else:
706
- this = None
707
-
708
- separator = self._parse_field() if self._match(TokenType.SEPARATOR) else None
709
-
710
- return self.expression(exp.GroupConcat, this=this, separator=separator)
711
-
712
- def _parse_json_value(self) -> exp.JSONValue:
713
- this = self._parse_bitwise()
714
- self._match(TokenType.COMMA)
715
- path = self._parse_bitwise()
716
-
717
- returning = self._match(TokenType.RETURNING) and self._parse_type()
718
-
719
- return self.expression(
720
- exp.JSONValue,
721
- this=this,
722
- path=self.dialect.to_json_path(path),
723
- returning=returning,
724
- on_condition=self._parse_on_condition(),
725
- )
726
-
727
679
  def _parse_alter_table_alter_index(self) -> exp.AlterIndex:
728
680
  index = self._parse_field(any_token=True)
729
681
 
@@ -31,7 +31,6 @@ from sqlglot.dialects.dialect import (
31
31
  sequence_sql,
32
32
  build_regexp_extract,
33
33
  explode_to_unnest_sql,
34
- space_sql,
35
34
  )
36
35
  from sqlglot.dialects.hive import Hive
37
36
  from sqlglot.dialects.mysql import MySQL
@@ -506,7 +505,6 @@ class Presto(Dialect):
506
505
  amend_exploded_column_table,
507
506
  ]
508
507
  ),
509
- exp.Space: space_sql,
510
508
  exp.SortArray: _no_sort_array,
511
509
  exp.StrPosition: lambda self, e: strposition_sql(self, e, supports_occurrence=True),
512
510
  exp.StrToDate: lambda self, e: f"CAST({_str_to_time_sql(self, e)} AS DATE)",
@@ -192,6 +192,7 @@ class Redshift(Postgres):
192
192
  exp.DistKeyProperty: lambda self, e: self.func("DISTKEY", e.this),
193
193
  exp.DistStyleProperty: lambda self, e: self.naked_property(e),
194
194
  exp.Explode: lambda self, e: self.explode_sql(e),
195
+ exp.FarmFingerprint: rename_func("FARMFINGERPRINT64"),
195
196
  exp.FromBase: rename_func("STRTOL"),
196
197
  exp.GeneratedAsIdentityColumnConstraint: generatedasidentitycolumnconstraint_sql,
197
198
  exp.JSONExtract: json_extract_segments("JSON_EXTRACT_PATH_TEXT"),