1
0
Fork 0
sqlglot/sqlglot/dialects/teradata.py
Daniel Baumann 4e506fbac7
Adding upstream version 25.5.1.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-02-13 21:41:00 +01:00

349 lines
14 KiB
Python

from __future__ import annotations
import typing as t
from sqlglot import exp, generator, parser, tokens, transforms
from sqlglot.dialects.dialect import (
Dialect,
max_or_greatest,
min_or_least,
rename_func,
to_number_with_nls_param,
)
from sqlglot.helper import seq_get
from sqlglot.tokens import TokenType
def _date_add_sql(
kind: t.Literal["+", "-"],
) -> t.Callable[[Teradata.Generator, exp.DateAdd | exp.DateSub], str]:
def func(self: Teradata.Generator, expression: exp.DateAdd | exp.DateSub) -> str:
this = self.sql(expression, "this")
unit = expression.args.get("unit")
value = self._simplify_unless_literal(expression.expression)
if not isinstance(value, exp.Literal):
self.unsupported("Cannot add non literal")
if isinstance(value, exp.Neg):
kind_to_op = {"+": "-", "-": "+"}
value = exp.Literal.string(value.this.to_py())
else:
kind_to_op = {"+": "+", "-": "-"}
value.set("is_string", True)
return f"{this} {kind_to_op[kind]} {self.sql(exp.Interval(this=value, unit=unit))}"
return func
class Teradata(Dialect):
SUPPORTS_SEMI_ANTI_JOIN = False
TYPED_DIVISION = True
TIME_MAPPING = {
"YY": "%y",
"Y4": "%Y",
"YYYY": "%Y",
"M4": "%B",
"M3": "%b",
"M": "%-M",
"MI": "%M",
"MM": "%m",
"MMM": "%b",
"MMMM": "%B",
"D": "%-d",
"DD": "%d",
"D3": "%j",
"DDD": "%j",
"H": "%-H",
"HH": "%H",
"HH24": "%H",
"S": "%-S",
"SS": "%S",
"SSSSSS": "%f",
"E": "%a",
"EE": "%a",
"E3": "%a",
"E4": "%A",
"EEE": "%a",
"EEEE": "%A",
}
class Tokenizer(tokens.Tokenizer):
# https://docs.teradata.com/r/Teradata-Database-SQL-Functions-Operators-Expressions-and-Predicates/March-2017/Comparison-Operators-and-Functions/Comparison-Operators/ANSI-Compliance
# https://docs.teradata.com/r/SQL-Functions-Operators-Expressions-and-Predicates/June-2017/Arithmetic-Trigonometric-Hyperbolic-Operators/Functions
KEYWORDS = {
**tokens.Tokenizer.KEYWORDS,
"**": TokenType.DSTAR,
"^=": TokenType.NEQ,
"BYTEINT": TokenType.SMALLINT,
"COLLECT": TokenType.COMMAND,
"DEL": TokenType.DELETE,
"EQ": TokenType.EQ,
"GE": TokenType.GTE,
"GT": TokenType.GT,
"HELP": TokenType.COMMAND,
"INS": TokenType.INSERT,
"LE": TokenType.LTE,
"LT": TokenType.LT,
"MINUS": TokenType.EXCEPT,
"MOD": TokenType.MOD,
"NE": TokenType.NEQ,
"NOT=": TokenType.NEQ,
"SAMPLE": TokenType.TABLE_SAMPLE,
"SEL": TokenType.SELECT,
"ST_GEOMETRY": TokenType.GEOMETRY,
"TOP": TokenType.TOP,
"UPD": TokenType.UPDATE,
}
KEYWORDS.pop("/*+")
# Teradata does not support % as a modulo operator
SINGLE_TOKENS = {**tokens.Tokenizer.SINGLE_TOKENS}
SINGLE_TOKENS.pop("%")
class Parser(parser.Parser):
TABLESAMPLE_CSV = True
VALUES_FOLLOWED_BY_PAREN = False
CHARSET_TRANSLATORS = {
"GRAPHIC_TO_KANJISJIS",
"GRAPHIC_TO_LATIN",
"GRAPHIC_TO_UNICODE",
"GRAPHIC_TO_UNICODE_PadSpace",
"KANJI1_KanjiEBCDIC_TO_UNICODE",
"KANJI1_KanjiEUC_TO_UNICODE",
"KANJI1_KANJISJIS_TO_UNICODE",
"KANJI1_SBC_TO_UNICODE",
"KANJISJIS_TO_GRAPHIC",
"KANJISJIS_TO_LATIN",
"KANJISJIS_TO_UNICODE",
"LATIN_TO_GRAPHIC",
"LATIN_TO_KANJISJIS",
"LATIN_TO_UNICODE",
"LOCALE_TO_UNICODE",
"UNICODE_TO_GRAPHIC",
"UNICODE_TO_GRAPHIC_PadGraphic",
"UNICODE_TO_GRAPHIC_VarGraphic",
"UNICODE_TO_KANJI1_KanjiEBCDIC",
"UNICODE_TO_KANJI1_KanjiEUC",
"UNICODE_TO_KANJI1_KANJISJIS",
"UNICODE_TO_KANJI1_SBC",
"UNICODE_TO_KANJISJIS",
"UNICODE_TO_LATIN",
"UNICODE_TO_LOCALE",
"UNICODE_TO_UNICODE_FoldSpace",
"UNICODE_TO_UNICODE_Fullwidth",
"UNICODE_TO_UNICODE_Halfwidth",
"UNICODE_TO_UNICODE_NFC",
"UNICODE_TO_UNICODE_NFD",
"UNICODE_TO_UNICODE_NFKC",
"UNICODE_TO_UNICODE_NFKD",
}
FUNC_TOKENS = {*parser.Parser.FUNC_TOKENS}
FUNC_TOKENS.remove(TokenType.REPLACE)
STATEMENT_PARSERS = {
**parser.Parser.STATEMENT_PARSERS,
TokenType.DATABASE: lambda self: self.expression(
exp.Use, this=self._parse_table(schema=False)
),
TokenType.REPLACE: lambda self: self._parse_create(),
}
FUNCTION_PARSERS = {
**parser.Parser.FUNCTION_PARSERS,
# https://docs.teradata.com/r/SQL-Functions-Operators-Expressions-and-Predicates/June-2017/Data-Type-Conversions/TRYCAST
"TRYCAST": parser.Parser.FUNCTION_PARSERS["TRY_CAST"],
"RANGE_N": lambda self: self._parse_rangen(),
"TRANSLATE": lambda self: self._parse_translate(self.STRICT_CAST),
}
FUNCTIONS = {
**parser.Parser.FUNCTIONS,
"RANDOM": lambda args: exp.Rand(lower=seq_get(args, 0), upper=seq_get(args, 1)),
}
EXPONENT = {
TokenType.DSTAR: exp.Pow,
}
def _parse_translate(self, strict: bool) -> exp.Expression:
this = self._parse_assignment()
if not self._match(TokenType.USING):
self.raise_error("Expected USING in TRANSLATE")
if self._match_texts(self.CHARSET_TRANSLATORS):
charset_split = self._prev.text.split("_TO_")
to = self.expression(exp.CharacterSet, this=charset_split[1])
else:
self.raise_error("Expected a character set translator after USING in TRANSLATE")
return self.expression(exp.Cast if strict else exp.TryCast, this=this, to=to)
# FROM before SET in Teradata UPDATE syntax
# https://docs.teradata.com/r/Enterprise_IntelliFlex_VMware/Teradata-VantageTM-SQL-Data-Manipulation-Language-17.20/Statement-Syntax/UPDATE/UPDATE-Syntax-Basic-Form-FROM-Clause
def _parse_update(self) -> exp.Update:
return self.expression(
exp.Update,
**{ # type: ignore
"this": self._parse_table(alias_tokens=self.UPDATE_ALIAS_TOKENS),
"from": self._parse_from(joins=True),
"expressions": self._match(TokenType.SET)
and self._parse_csv(self._parse_equality),
"where": self._parse_where(),
},
)
def _parse_rangen(self):
this = self._parse_id_var()
self._match(TokenType.BETWEEN)
expressions = self._parse_csv(self._parse_assignment)
each = self._match_text_seq("EACH") and self._parse_assignment()
return self.expression(exp.RangeN, this=this, expressions=expressions, each=each)
def _parse_index_params(self) -> exp.IndexParameters:
this = super()._parse_index_params()
if this.args.get("on"):
this.set("on", None)
self._retreat(self._index - 2)
return this
class Generator(generator.Generator):
LIMIT_IS_TOP = True
JOIN_HINTS = False
TABLE_HINTS = False
QUERY_HINTS = False
TABLESAMPLE_KEYWORDS = "SAMPLE"
LAST_DAY_SUPPORTS_DATE_PART = False
CAN_IMPLEMENT_ARRAY_ANY = True
TZ_TO_WITH_TIME_ZONE = True
TYPE_MAPPING = {
**generator.Generator.TYPE_MAPPING,
exp.DataType.Type.GEOMETRY: "ST_GEOMETRY",
exp.DataType.Type.DOUBLE: "DOUBLE PRECISION",
exp.DataType.Type.TIMESTAMPTZ: "TIMESTAMP",
}
PROPERTIES_LOCATION = {
**generator.Generator.PROPERTIES_LOCATION,
exp.OnCommitProperty: exp.Properties.Location.POST_INDEX,
exp.PartitionedByProperty: exp.Properties.Location.POST_EXPRESSION,
exp.StabilityProperty: exp.Properties.Location.POST_CREATE,
}
TRANSFORMS = {
**generator.Generator.TRANSFORMS,
exp.ArgMax: rename_func("MAX_BY"),
exp.ArgMin: rename_func("MIN_BY"),
exp.ArraySize: rename_func("CARDINALITY"),
exp.Max: max_or_greatest,
exp.Min: min_or_least,
exp.Pow: lambda self, e: self.binary(e, "**"),
exp.Rand: lambda self, e: self.func("RANDOM", e.args.get("lower"), e.args.get("upper")),
exp.Select: transforms.preprocess(
[transforms.eliminate_distinct_on, transforms.eliminate_semi_and_anti_joins]
),
exp.StrToDate: lambda self,
e: f"CAST({self.sql(e, 'this')} AS DATE FORMAT {self.format_time(e)})",
exp.ToChar: lambda self, e: self.function_fallback_sql(e),
exp.ToNumber: to_number_with_nls_param,
exp.Use: lambda self, e: f"DATABASE {self.sql(e, 'this')}",
exp.DateAdd: _date_add_sql("+"),
exp.DateSub: _date_add_sql("-"),
exp.Quarter: lambda self, e: self.sql(exp.Extract(this="QUARTER", expression=e.this)),
}
def currenttimestamp_sql(self, expression: exp.CurrentTimestamp) -> str:
prefix, suffix = ("(", ")") if expression.this else ("", "")
return self.func("CURRENT_TIMESTAMP", expression.this, prefix=prefix, suffix=suffix)
def cast_sql(self, expression: exp.Cast, safe_prefix: t.Optional[str] = None) -> str:
if expression.to.this == exp.DataType.Type.UNKNOWN and expression.args.get("format"):
# We don't actually want to print the unknown type in CAST(<value> AS FORMAT <format>)
expression.to.pop()
return super().cast_sql(expression, safe_prefix=safe_prefix)
def trycast_sql(self, expression: exp.TryCast) -> str:
return self.cast_sql(expression, safe_prefix="TRY")
def tablesample_sql(
self,
expression: exp.TableSample,
sep: str = " AS ",
tablesample_keyword: t.Optional[str] = None,
) -> str:
return f"{self.sql(expression, 'this')} SAMPLE {self.expressions(expression)}"
def partitionedbyproperty_sql(self, expression: exp.PartitionedByProperty) -> str:
return f"PARTITION BY {self.sql(expression, 'this')}"
# FROM before SET in Teradata UPDATE syntax
# https://docs.teradata.com/r/Enterprise_IntelliFlex_VMware/Teradata-VantageTM-SQL-Data-Manipulation-Language-17.20/Statement-Syntax/UPDATE/UPDATE-Syntax-Basic-Form-FROM-Clause
def update_sql(self, expression: exp.Update) -> str:
this = self.sql(expression, "this")
from_sql = self.sql(expression, "from")
set_sql = self.expressions(expression, flat=True)
where_sql = self.sql(expression, "where")
sql = f"UPDATE {this}{from_sql} SET {set_sql}{where_sql}"
return self.prepend_ctes(expression, sql)
def mod_sql(self, expression: exp.Mod) -> str:
return self.binary(expression, "MOD")
def datatype_sql(self, expression: exp.DataType) -> str:
type_sql = super().datatype_sql(expression)
prefix_sql = expression.args.get("prefix")
return f"SYSUDTLIB.{type_sql}" if prefix_sql else type_sql
def rangen_sql(self, expression: exp.RangeN) -> str:
this = self.sql(expression, "this")
expressions_sql = self.expressions(expression)
each_sql = self.sql(expression, "each")
each_sql = f" EACH {each_sql}" if each_sql else ""
return f"RANGE_N({this} BETWEEN {expressions_sql}{each_sql})"
def createable_sql(self, expression: exp.Create, locations: t.DefaultDict) -> str:
kind = self.sql(expression, "kind").upper()
if kind == "TABLE" and locations.get(exp.Properties.Location.POST_NAME):
this_name = self.sql(expression.this, "this")
this_properties = self.properties(
exp.Properties(expressions=locations[exp.Properties.Location.POST_NAME]),
wrapped=False,
prefix=",",
)
this_schema = self.schema_columns_sql(expression.this)
return f"{this_name}{this_properties}{self.sep()}{this_schema}"
return super().createable_sql(expression, locations)
def extract_sql(self, expression: exp.Extract) -> str:
this = self.sql(expression, "this")
if this.upper() != "QUARTER":
return super().extract_sql(expression)
to_char = exp.func("to_char", expression.expression, exp.Literal.string("Q"))
return self.sql(exp.cast(to_char, exp.DataType.Type.INT))
def interval_sql(self, expression: exp.Interval) -> str:
multiplier = 0
unit = expression.text("unit")
if unit.startswith("WEEK"):
multiplier = 7
elif unit.startswith("QUARTER"):
multiplier = 90
if multiplier:
return f"({multiplier} * {super().interval_sql(exp.Interval(this=expression.this, unit=exp.var('DAY')))})"
return super().interval_sql(expression)