sqlglot 28.4.0__py3-none-any.whl → 28.8.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.
Files changed (50) hide show
  1. sqlglot/_version.py +2 -2
  2. sqlglot/dialects/bigquery.py +20 -23
  3. sqlglot/dialects/clickhouse.py +2 -0
  4. sqlglot/dialects/dialect.py +355 -18
  5. sqlglot/dialects/doris.py +38 -90
  6. sqlglot/dialects/druid.py +1 -0
  7. sqlglot/dialects/duckdb.py +1739 -163
  8. sqlglot/dialects/exasol.py +17 -1
  9. sqlglot/dialects/hive.py +27 -2
  10. sqlglot/dialects/mysql.py +103 -11
  11. sqlglot/dialects/oracle.py +38 -1
  12. sqlglot/dialects/postgres.py +142 -33
  13. sqlglot/dialects/presto.py +6 -2
  14. sqlglot/dialects/redshift.py +7 -1
  15. sqlglot/dialects/singlestore.py +13 -3
  16. sqlglot/dialects/snowflake.py +271 -21
  17. sqlglot/dialects/spark.py +25 -0
  18. sqlglot/dialects/spark2.py +4 -3
  19. sqlglot/dialects/starrocks.py +152 -17
  20. sqlglot/dialects/trino.py +1 -0
  21. sqlglot/dialects/tsql.py +5 -0
  22. sqlglot/diff.py +1 -1
  23. sqlglot/expressions.py +239 -47
  24. sqlglot/generator.py +173 -44
  25. sqlglot/optimizer/annotate_types.py +129 -60
  26. sqlglot/optimizer/merge_subqueries.py +13 -2
  27. sqlglot/optimizer/qualify_columns.py +7 -0
  28. sqlglot/optimizer/resolver.py +19 -0
  29. sqlglot/optimizer/scope.py +12 -0
  30. sqlglot/optimizer/unnest_subqueries.py +7 -0
  31. sqlglot/parser.py +251 -58
  32. sqlglot/schema.py +186 -14
  33. sqlglot/tokens.py +36 -6
  34. sqlglot/transforms.py +6 -5
  35. sqlglot/typing/__init__.py +29 -10
  36. sqlglot/typing/bigquery.py +5 -10
  37. sqlglot/typing/duckdb.py +39 -0
  38. sqlglot/typing/hive.py +50 -1
  39. sqlglot/typing/mysql.py +32 -0
  40. sqlglot/typing/presto.py +0 -1
  41. sqlglot/typing/snowflake.py +80 -17
  42. sqlglot/typing/spark.py +29 -0
  43. sqlglot/typing/spark2.py +9 -1
  44. sqlglot/typing/tsql.py +21 -0
  45. {sqlglot-28.4.0.dist-info → sqlglot-28.8.0.dist-info}/METADATA +47 -2
  46. sqlglot-28.8.0.dist-info/RECORD +95 -0
  47. {sqlglot-28.4.0.dist-info → sqlglot-28.8.0.dist-info}/WHEEL +1 -1
  48. sqlglot-28.4.0.dist-info/RECORD +0 -92
  49. {sqlglot-28.4.0.dist-info → sqlglot-28.8.0.dist-info}/licenses/LICENSE +0 -0
  50. {sqlglot-28.4.0.dist-info → sqlglot-28.8.0.dist-info}/top_level.txt +0 -0
@@ -8,11 +8,14 @@ from sqlglot.dialects.dialect import (
8
8
  Dialect,
9
9
  JSON_EXTRACT_TYPE,
10
10
  any_value_to_max_sql,
11
+ array_append_sql,
12
+ array_concat_sql,
11
13
  binary_from_function,
12
14
  bool_xor_sql,
13
15
  datestrtodate_sql,
14
16
  build_formatted_time,
15
17
  filter_array_using_unnest,
18
+ getbit_sql,
16
19
  inline_array_sql,
17
20
  json_extract_segments,
18
21
  json_path_key_only_name,
@@ -345,6 +348,7 @@ class Postgres(Dialect):
345
348
  BIT_STRINGS = [("b'", "'"), ("B'", "'")]
346
349
  HEX_STRINGS = [("x'", "'"), ("X'", "'")]
347
350
  BYTE_STRINGS = [("e'", "'"), ("E'", "'")]
351
+ BYTE_STRING_ESCAPES = ["'", "\\"]
348
352
  HEREDOC_STRINGS = ["$"]
349
353
 
350
354
  HEREDOC_TAG_IS_IDENTIFIER = True
@@ -358,8 +362,6 @@ class Postgres(Dialect):
358
362
  "<@": TokenType.LT_AT,
359
363
  "?&": TokenType.QMARK_AMP,
360
364
  "?|": TokenType.QMARK_PIPE,
361
- "&<": TokenType.AMP_LT,
362
- "&>": TokenType.AMP_GT,
363
365
  "#-": TokenType.HASH_DASH,
364
366
  "|/": TokenType.PIPE_SLASH,
365
367
  "||/": TokenType.DPIPE_SLASH,
@@ -376,7 +378,7 @@ class Postgres(Dialect):
376
378
  "NAME": TokenType.NAME,
377
379
  "OID": TokenType.OBJECT_IDENTIFIER,
378
380
  "ONLY": TokenType.ONLY,
379
- "OPERATOR": TokenType.OPERATOR,
381
+ "POINT": TokenType.POINT,
380
382
  "REFRESH": TokenType.COMMAND,
381
383
  "REINDEX": TokenType.COMMAND,
382
384
  "RESET": TokenType.COMMAND,
@@ -396,6 +398,8 @@ class Postgres(Dialect):
396
398
  "REGTYPE": TokenType.OBJECT_IDENTIFIER,
397
399
  "FLOAT": TokenType.DOUBLE,
398
400
  "XML": TokenType.XML,
401
+ "VARIADIC": TokenType.VARIADIC,
402
+ "INOUT": TokenType.INOUT,
399
403
  }
400
404
  KEYWORDS.pop("/*+")
401
405
  KEYWORDS.pop("DIV")
@@ -424,6 +428,9 @@ class Postgres(Dialect):
424
428
 
425
429
  FUNCTIONS = {
426
430
  **parser.Parser.FUNCTIONS,
431
+ "ARRAY_PREPEND": lambda args: exp.ArrayPrepend(
432
+ this=seq_get(args, 1), expression=seq_get(args, 0)
433
+ ),
427
434
  "BIT_AND": exp.BitwiseAndAgg.from_arg_list,
428
435
  "BIT_OR": exp.BitwiseOrAgg.from_arg_list,
429
436
  "BIT_XOR": exp.BitwiseXorAgg.from_arg_list,
@@ -432,6 +439,9 @@ class Postgres(Dialect):
432
439
  binary_from_function(exp.IntDiv)(args), exp.DataType.Type.DECIMAL
433
440
  ),
434
441
  "GENERATE_SERIES": _build_generate_series,
442
+ "GET_BIT": lambda args: exp.Getbit(
443
+ this=seq_get(args, 0), expression=seq_get(args, 1), zero_is_msb=True
444
+ ),
435
445
  "JSON_EXTRACT_PATH": build_json_extract_path(exp.JSONExtract),
436
446
  "JSON_EXTRACT_PATH_TEXT": build_json_extract_path(exp.JSONExtractScalar),
437
447
  "LENGTH": lambda args: exp.Length(this=seq_get(args, 0), encoding=seq_get(args, 1)),
@@ -449,6 +459,16 @@ class Postgres(Dialect):
449
459
  "LEVENSHTEIN_LESS_EQUAL": _build_levenshtein_less_equal,
450
460
  "JSON_OBJECT_AGG": lambda args: exp.JSONObjectAgg(expressions=args),
451
461
  "JSONB_OBJECT_AGG": exp.JSONBObjectAgg.from_arg_list,
462
+ "WIDTH_BUCKET": lambda args: exp.WidthBucket(
463
+ this=seq_get(args, 0), threshold=seq_get(args, 1)
464
+ )
465
+ if len(args) == 2
466
+ else exp.WidthBucket.from_arg_list(args),
467
+ }
468
+
469
+ NO_PAREN_FUNCTION_PARSERS = {
470
+ **parser.Parser.NO_PAREN_FUNCTION_PARSERS,
471
+ "VARIADIC": lambda self: self.expression(exp.Variadic, this=self._parse_bitwise()),
452
472
  }
453
473
 
454
474
  NO_PAREN_FUNCTIONS = {
@@ -479,12 +499,9 @@ class Postgres(Dialect):
479
499
  RANGE_PARSERS = {
480
500
  **parser.Parser.RANGE_PARSERS,
481
501
  TokenType.DAMP: binary_range_parser(exp.ArrayOverlaps),
482
- TokenType.AMP_LT: binary_range_parser(exp.ExtendsLeft),
483
- TokenType.AMP_GT: binary_range_parser(exp.ExtendsRight),
484
502
  TokenType.DAT: lambda self, this: self.expression(
485
503
  exp.MatchAgainst, this=self._parse_bitwise(), expressions=[this]
486
504
  ),
487
- TokenType.OPERATOR: lambda self, this: self._parse_operator(this),
488
505
  }
489
506
 
490
507
  STATEMENT_PARSERS = {
@@ -492,6 +509,12 @@ class Postgres(Dialect):
492
509
  TokenType.END: lambda self: self._parse_commit_or_rollback(),
493
510
  }
494
511
 
512
+ UNARY_PARSERS = {
513
+ **parser.Parser.UNARY_PARSERS,
514
+ # The `~` token is remapped from TILDE to RLIKE in Postgres due to the binary REGEXP LIKE operator
515
+ TokenType.RLIKE: lambda self: self.expression(exp.BitwiseNot, this=self._parse_unary()),
516
+ }
517
+
495
518
  JSON_ARROWS_REQUIRE_JSON_TYPE = True
496
519
 
497
520
  COLUMN_OPERATORS = {
@@ -508,6 +531,88 @@ class Postgres(Dialect):
508
531
  ),
509
532
  }
510
533
 
534
+ ARG_MODE_TOKENS = {TokenType.IN, TokenType.OUT, TokenType.INOUT, TokenType.VARIADIC}
535
+
536
+ def _parse_parameter_mode(self) -> t.Optional[TokenType]:
537
+ """
538
+ Parse PostgreSQL function parameter mode (IN, OUT, INOUT, VARIADIC).
539
+
540
+ Disambiguates between mode keywords and identifiers with the same name:
541
+ - MODE TYPE → keyword is identifier (e.g., "out INT")
542
+ - MODE NAME TYPE → keyword is mode (e.g., "OUT x INT")
543
+
544
+ Returns:
545
+ Mode token type if current token is a mode keyword, None otherwise.
546
+ """
547
+ if not self._match_set(self.ARG_MODE_TOKENS, advance=False) or not self._next:
548
+ return None
549
+
550
+ mode_token = self._curr
551
+
552
+ # Check Pattern 1: MODE TYPE
553
+ # Try parsing next token as a built-in type (not UDT)
554
+ # If successful, the keyword is an identifier, not a mode
555
+ is_followed_by_builtin_type = self._try_parse(
556
+ lambda: self._advance() # type: ignore
557
+ or self._parse_types(check_func=False, allow_identifiers=False),
558
+ retreat=True,
559
+ )
560
+ if is_followed_by_builtin_type:
561
+ return None # Pattern: "out INT" → out is parameter name
562
+
563
+ # Check Pattern 2: MODE NAME TYPE
564
+ # If next token is an identifier, check if there's a type after it
565
+ # The type can be built-in or user-defined (allow_identifiers=True)
566
+ if self._next.token_type not in self.ID_VAR_TOKENS:
567
+ return None
568
+
569
+ is_followed_by_any_type = self._try_parse(
570
+ lambda: self._advance(2) # type: ignore
571
+ or self._parse_types(check_func=False, allow_identifiers=True),
572
+ retreat=True,
573
+ )
574
+
575
+ if is_followed_by_any_type:
576
+ return mode_token.token_type # Pattern: "OUT x INT" → OUT is mode
577
+
578
+ return None
579
+
580
+ def _create_mode_constraint(self, param_mode: TokenType) -> exp.InOutColumnConstraint:
581
+ """
582
+ Create parameter mode constraint for function parameters.
583
+
584
+ Args:
585
+ param_mode: The parameter mode token (IN, OUT, INOUT, or VARIADIC).
586
+
587
+ Returns:
588
+ InOutColumnConstraint expression representing the parameter mode.
589
+ """
590
+ return self.expression(
591
+ exp.InOutColumnConstraint,
592
+ input_=(param_mode in {TokenType.IN, TokenType.INOUT}),
593
+ output=(param_mode in {TokenType.OUT, TokenType.INOUT}),
594
+ variadic=(param_mode == TokenType.VARIADIC),
595
+ )
596
+
597
+ def _parse_function_parameter(self) -> t.Optional[exp.Expression]:
598
+ param_mode = self._parse_parameter_mode()
599
+
600
+ if param_mode:
601
+ self._advance()
602
+
603
+ # Parse parameter name and type
604
+ param_name = self._parse_id_var()
605
+ column_def = self._parse_column_def(this=param_name, computed_column=False)
606
+
607
+ # Attach mode as constraint
608
+ if param_mode and column_def:
609
+ constraint = self._create_mode_constraint(param_mode)
610
+ if not column_def.args.get("constraints"):
611
+ column_def.set("constraints", [])
612
+ column_def.args["constraints"].insert(0, constraint)
613
+
614
+ return column_def
615
+
511
616
  def _parse_query_parameter(self) -> t.Optional[exp.Expression]:
512
617
  this = (
513
618
  self._parse_wrapped(self._parse_id_var)
@@ -517,29 +622,6 @@ class Postgres(Dialect):
517
622
  self._match_text_seq("S")
518
623
  return self.expression(exp.Placeholder, this=this)
519
624
 
520
- def _parse_operator(self, this: t.Optional[exp.Expression]) -> t.Optional[exp.Expression]:
521
- while True:
522
- if not self._match(TokenType.L_PAREN):
523
- break
524
-
525
- op = ""
526
- while self._curr and not self._match(TokenType.R_PAREN):
527
- op += self._curr.text
528
- self._advance()
529
-
530
- this = self.expression(
531
- exp.Operator,
532
- comments=self._prev_comments,
533
- this=this,
534
- operator=op,
535
- expression=self._parse_bitwise(),
536
- )
537
-
538
- if not self._match(TokenType.OPERATOR):
539
- break
540
-
541
- return this
542
-
543
625
  def _parse_date_part(self) -> exp.Expression:
544
626
  part = self._parse_type()
545
627
  self._match(TokenType.COMMA)
@@ -611,6 +693,7 @@ class Postgres(Dialect):
611
693
  SUPPORTS_MEDIAN = False
612
694
  ARRAY_SIZE_DIM_REQUIRED = True
613
695
  SUPPORTS_BETWEEN_FLAGS = True
696
+ INOUT_SEPARATOR = "" # PostgreSQL uses "INOUT" (no space)
614
697
 
615
698
  SUPPORTED_JSON_PATH_PARTS = {
616
699
  exp.JSONPathKey,
@@ -618,6 +701,14 @@ class Postgres(Dialect):
618
701
  exp.JSONPathSubscript,
619
702
  }
620
703
 
704
+ def lateral_sql(self, expression: exp.Lateral) -> str:
705
+ sql = super().lateral_sql(expression)
706
+
707
+ if expression.args.get("cross_apply") is not None:
708
+ sql = f"{sql} ON TRUE"
709
+
710
+ return sql
711
+
621
712
  TYPE_MAPPING = {
622
713
  **generator.Generator.TYPE_MAPPING,
623
714
  exp.DataType.Type.TINYINT: "SMALLINT",
@@ -634,8 +725,10 @@ class Postgres(Dialect):
634
725
  TRANSFORMS = {
635
726
  **generator.Generator.TRANSFORMS,
636
727
  exp.AnyValue: _versioned_anyvalue_sql,
637
- exp.ArrayConcat: lambda self, e: self.arrayconcat_sql(e, name="ARRAY_CAT"),
728
+ exp.ArrayConcat: array_concat_sql("ARRAY_CAT"),
638
729
  exp.ArrayFilter: filter_array_using_unnest,
730
+ exp.ArrayAppend: array_append_sql("ARRAY_APPEND"),
731
+ exp.ArrayPrepend: array_append_sql("ARRAY_PREPEND", swap_params=True),
639
732
  exp.BitwiseAndAgg: rename_func("BIT_AND"),
640
733
  exp.BitwiseOrAgg: rename_func("BIT_OR"),
641
734
  exp.BitwiseXor: lambda self, e: self.binary(e, "#"),
@@ -650,6 +743,7 @@ class Postgres(Dialect):
650
743
  exp.DateSub: _date_add_sql("-"),
651
744
  exp.Explode: rename_func("UNNEST"),
652
745
  exp.ExplodingGenerateSeries: rename_func("GENERATE_SERIES"),
746
+ exp.Getbit: getbit_sql,
653
747
  exp.GroupConcat: lambda self, e: groupconcat_sql(
654
748
  self, e, func_name="STRING_AGG", within_group=False
655
749
  ),
@@ -714,7 +808,9 @@ class Postgres(Dialect):
714
808
  exp.TimestampTrunc: timestamptrunc_sql(zone=True),
715
809
  exp.TimeStrToTime: timestrtotime_sql,
716
810
  exp.TimeToStr: lambda self, e: self.func("TO_CHAR", e.this, self.format_time(e)),
717
- exp.ToChar: lambda self, e: self.function_fallback_sql(e),
811
+ exp.ToChar: lambda self, e: self.function_fallback_sql(e)
812
+ if e.args.get("format")
813
+ else self.tochar_sql(e),
718
814
  exp.Trim: trim_sql,
719
815
  exp.TryCast: no_trycast_sql,
720
816
  exp.TsOrDsAdd: _date_add_sql("+"),
@@ -752,6 +848,18 @@ class Postgres(Dialect):
752
848
  self.unsupported("Column comments are not supported in the CREATE statement")
753
849
  return ""
754
850
 
851
+ def columndef_sql(self, expression: exp.ColumnDef, sep: str = " ") -> str:
852
+ # PostgreSQL places parameter modes BEFORE parameter name
853
+ param_constraint = expression.find(exp.InOutColumnConstraint)
854
+
855
+ if param_constraint:
856
+ mode_sql = self.sql(param_constraint)
857
+ param_constraint.pop() # Remove to prevent double-rendering
858
+ base_sql = super().columndef_sql(expression, sep)
859
+ return f"{mode_sql} {base_sql}"
860
+
861
+ return super().columndef_sql(expression, sep)
862
+
755
863
  def unnest_sql(self, expression: exp.Unnest) -> str:
756
864
  if len(expression.expressions) == 1:
757
865
  arg = expression.expressions[0]
@@ -865,8 +973,9 @@ class Postgres(Dialect):
865
973
  def interval_sql(self, expression: exp.Interval) -> str:
866
974
  unit = expression.text("unit").lower()
867
975
 
868
- if unit.startswith("quarter") and isinstance(expression.this, exp.Literal):
869
- expression.this.replace(exp.Literal.number(int(expression.this.to_py()) * 3))
976
+ this = expression.this
977
+ if unit.startswith("quarter") and isinstance(this, exp.Literal):
978
+ this.replace(exp.Literal.string(int(this.to_py()) * 3))
870
979
  expression.args["unit"].replace(exp.var("MONTH"))
871
980
 
872
981
  return super().interval_sql(expression)
@@ -267,6 +267,7 @@ class Presto(Dialect):
267
267
  TABLESAMPLE_SIZE_IS_PERCENT = True
268
268
  LOG_BASE_FIRST: t.Optional[bool] = None
269
269
  SUPPORTS_VALUES_DEFAULT = False
270
+ LEAST_GREATEST_IGNORES_NULLS = False
270
271
 
271
272
  TIME_MAPPING = MySQL.TIME_MAPPING
272
273
 
@@ -373,6 +374,7 @@ class Presto(Dialect):
373
374
  "MD5": exp.MD5Digest.from_arg_list,
374
375
  "SHA256": lambda args: exp.SHA2(this=seq_get(args, 0), length=exp.Literal.number(256)),
375
376
  "SHA512": lambda args: exp.SHA2(this=seq_get(args, 0), length=exp.Literal.number(512)),
377
+ "WEEK": exp.WeekOfYear.from_arg_list,
376
378
  }
377
379
 
378
380
  FUNCTION_PARSERS = parser.Parser.FUNCTION_PARSERS.copy()
@@ -707,9 +709,11 @@ class Presto(Dialect):
707
709
  return super().bracket_sql(expression)
708
710
 
709
711
  def struct_sql(self, expression: exp.Struct) -> str:
710
- from sqlglot.optimizer.annotate_types import annotate_types
712
+ if not expression.type:
713
+ from sqlglot.optimizer.annotate_types import annotate_types
714
+
715
+ annotate_types(expression, dialect=self.dialect)
711
716
 
712
- expression = annotate_types(expression, dialect=self.dialect)
713
717
  values: t.List[str] = []
714
718
  schema: t.List[str] = []
715
719
  unknown_type = False
@@ -5,6 +5,7 @@ import typing as t
5
5
  from sqlglot import exp, transforms
6
6
  from sqlglot.dialects.dialect import (
7
7
  NormalizationStrategy,
8
+ array_concat_sql,
8
9
  concat_to_dpipe_sql,
9
10
  concat_ws_to_dpipe_sql,
10
11
  date_delta_sql,
@@ -49,6 +50,7 @@ class Redshift(Postgres):
49
50
  HAS_DISTINCT_ARRAY_CONSTRUCTORS = True
50
51
  COALESCE_COMPARISON_NON_STANDARD = True
51
52
  REGEXP_EXTRACT_POSITION_OVERFLOW_RETURNS_NULL = False
53
+ ARRAY_FUNCS_PROPAGATES_NULLS = True
52
54
 
53
55
  # ref: https://docs.aws.amazon.com/redshift/latest/dg/r_FORMAT_strings.html
54
56
  TIME_FORMAT = "'YYYY-MM-DD HH24:MI:SS'"
@@ -82,6 +84,7 @@ class Redshift(Postgres):
82
84
  ),
83
85
  "STRTOL": exp.FromBase.from_arg_list,
84
86
  }
87
+ FUNCTIONS.pop("GET_BIT")
85
88
 
86
89
  NO_PAREN_FUNCTION_PARSERS = {
87
90
  **Postgres.Parser.NO_PAREN_FUNCTION_PARSERS,
@@ -189,7 +192,7 @@ class Redshift(Postgres):
189
192
 
190
193
  TRANSFORMS = {
191
194
  **Postgres.Generator.TRANSFORMS,
192
- exp.ArrayConcat: lambda self, e: self.arrayconcat_sql(e, name="ARRAY_CONCAT"),
195
+ exp.ArrayConcat: array_concat_sql("ARRAY_CONCAT"),
193
196
  exp.Concat: concat_to_dpipe_sql,
194
197
  exp.ConcatWs: concat_ws_to_dpipe_sql,
195
198
  exp.ApproxDistinct: lambda self,
@@ -244,6 +247,9 @@ class Redshift(Postgres):
244
247
  TRANSFORMS.pop(exp.LastDay)
245
248
  TRANSFORMS.pop(exp.SHA2)
246
249
 
250
+ # Postgres and Redshift have different semantics for Getbit
251
+ TRANSFORMS.pop(exp.Getbit)
252
+
247
253
  # Postgres does not permit a double precision argument in ROUND; Redshift does
248
254
  TRANSFORMS.pop(exp.Round)
249
255
 
@@ -82,6 +82,7 @@ class SingleStore(MySQL):
82
82
  "::$": TokenType.DCOLONDOLLAR,
83
83
  "::%": TokenType.DCOLONPERCENT,
84
84
  "::?": TokenType.DCOLONQMARK,
85
+ "RECORD": TokenType.STRUCT,
85
86
  }
86
87
 
87
88
  class Parser(MySQL.Parser):
@@ -176,6 +177,10 @@ class SingleStore(MySQL):
176
177
  expression=seq_get(args, 0),
177
178
  json_type="JSON",
178
179
  ),
180
+ "JSON_KEYS": lambda args: exp.JSONKeys(
181
+ this=seq_get(args, 0),
182
+ expressions=args[1:],
183
+ ),
179
184
  "JSON_PRETTY": exp.JSONFormat.from_arg_list,
180
185
  "JSON_BUILD_ARRAY": lambda args: exp.JSONArray(expressions=args),
181
186
  "JSON_BUILD_OBJECT": lambda args: exp.JSONObject(expressions=args),
@@ -328,6 +333,7 @@ class SingleStore(MySQL):
328
333
  SUPPORTS_UESCAPE = False
329
334
  NULL_ORDERING_SUPPORTED = True
330
335
  MATCH_AGAINST_TABLE_PREFIX = "TABLE "
336
+ STRUCT_DELIMITER = ("(", ")")
331
337
 
332
338
  @staticmethod
333
339
  def _unicode_substitute(m: re.Match[str]) -> str:
@@ -497,7 +503,6 @@ class SingleStore(MySQL):
497
503
  ),
498
504
  exp.IsAscii: lambda self, e: f"({self.sql(e, 'this')} RLIKE '^[\x00-\x7f]*$')",
499
505
  exp.MD5Digest: lambda self, e: self.func("UNHEX", self.func("MD5", e.this)),
500
- exp.Chr: rename_func("CHAR"),
501
506
  exp.Contains: rename_func("INSTR"),
502
507
  exp.RegexpExtractAll: unsupported_args("position", "occurrence", "group")(
503
508
  lambda self, e: self.func(
@@ -613,7 +618,6 @@ class SingleStore(MySQL):
613
618
  exp.DataType.Type.SERIAL,
614
619
  exp.DataType.Type.SMALLSERIAL,
615
620
  exp.DataType.Type.SMALLMONEY,
616
- exp.DataType.Type.STRUCT,
617
621
  exp.DataType.Type.SUPER,
618
622
  exp.DataType.Type.TIMETZ,
619
623
  exp.DataType.Type.TIMESTAMPNTZ,
@@ -654,6 +658,7 @@ class SingleStore(MySQL):
654
658
  exp.DataType.Type.LINESTRING: "GEOGRAPHY",
655
659
  exp.DataType.Type.POLYGON: "GEOGRAPHY",
656
660
  exp.DataType.Type.MULTIPOLYGON: "GEOGRAPHY",
661
+ exp.DataType.Type.STRUCT: "RECORD",
657
662
  exp.DataType.Type.JSONB: "BSON",
658
663
  exp.DataType.Type.TIMESTAMP: "TIMESTAMP",
659
664
  exp.DataType.Type.TIMESTAMP_S: "TIMESTAMP",
@@ -1760,8 +1765,13 @@ class SingleStore(MySQL):
1760
1765
  self.func("TO_JSON", expression.this),
1761
1766
  )
1762
1767
 
1763
- @unsupported_args("kind", "nested", "values")
1768
+ @unsupported_args("kind", "values")
1764
1769
  def datatype_sql(self, expression: exp.DataType) -> str:
1770
+ if expression.args.get("nested") and not expression.is_type(exp.DataType.Type.STRUCT):
1771
+ self.unsupported(
1772
+ f"Argument 'nested' is not supported for representation of '{expression.this.value}' in SingleStore"
1773
+ )
1774
+
1765
1775
  if expression.is_type(exp.DataType.Type.VARBINARY) and not expression.expressions:
1766
1776
  # `VARBINARY` must always have a size - if it doesn't, we always generate `BLOB`
1767
1777
  return "BLOB"