sqlglot.dialects.tsql
1from __future__ import annotations 2 3import datetime 4import re 5import typing as t 6from functools import partial, reduce 7 8from sqlglot import exp, generator, parser, tokens, transforms 9from sqlglot.dialects.dialect import ( 10 Dialect, 11 NormalizationStrategy, 12 any_value_to_max_sql, 13 build_date_delta, 14 date_delta_sql, 15 datestrtodate_sql, 16 generatedasidentitycolumnconstraint_sql, 17 max_or_greatest, 18 min_or_least, 19 rename_func, 20 strposition_sql, 21 timestrtotime_sql, 22 trim_sql, 23) 24from sqlglot.helper import seq_get 25from sqlglot.parser import build_coalesce 26from sqlglot.time import format_time 27from sqlglot.tokens import TokenType 28 29if t.TYPE_CHECKING: 30 from sqlglot._typing import E 31 32FULL_FORMAT_TIME_MAPPING = { 33 "weekday": "%A", 34 "dw": "%A", 35 "w": "%A", 36 "month": "%B", 37 "mm": "%B", 38 "m": "%B", 39} 40 41DATE_DELTA_INTERVAL = { 42 "year": "year", 43 "yyyy": "year", 44 "yy": "year", 45 "quarter": "quarter", 46 "qq": "quarter", 47 "q": "quarter", 48 "month": "month", 49 "mm": "month", 50 "m": "month", 51 "week": "week", 52 "ww": "week", 53 "wk": "week", 54 "day": "day", 55 "dd": "day", 56 "d": "day", 57} 58 59 60DATE_FMT_RE = re.compile("([dD]{1,2})|([mM]{1,2})|([yY]{1,4})|([hH]{1,2})|([sS]{1,2})") 61 62# N = Numeric, C=Currency 63TRANSPILE_SAFE_NUMBER_FMT = {"N", "C"} 64 65DEFAULT_START_DATE = datetime.date(1900, 1, 1) 66 67BIT_TYPES = {exp.EQ, exp.NEQ, exp.Is, exp.In, exp.Select, exp.Alias} 68 69# Unsupported options: 70# - OPTIMIZE FOR ( @variable_name { UNKNOWN | = <literal_constant> } [ , ...n ] ) 71# - TABLE HINT 72OPTIONS: parser.OPTIONS_TYPE = { 73 **dict.fromkeys( 74 ( 75 "DISABLE_OPTIMIZED_PLAN_FORCING", 76 "FAST", 77 "IGNORE_NONCLUSTERED_COLUMNSTORE_INDEX", 78 "LABEL", 79 "MAXDOP", 80 "MAXRECURSION", 81 "MAX_GRANT_PERCENT", 82 "MIN_GRANT_PERCENT", 83 "NO_PERFORMANCE_SPOOL", 84 "QUERYTRACEON", 85 "RECOMPILE", 86 ), 87 tuple(), 88 ), 89 "CONCAT": ("UNION",), 90 "DISABLE": ("EXTERNALPUSHDOWN", "SCALEOUTEXECUTION"), 91 "EXPAND": ("VIEWS",), 92 "FORCE": ("EXTERNALPUSHDOWN", "ORDER", "SCALEOUTEXECUTION"), 93 "HASH": ("GROUP", "JOIN", "UNION"), 94 "KEEP": ("PLAN",), 95 "KEEPFIXED": ("PLAN",), 96 "LOOP": ("JOIN",), 97 "MERGE": ("JOIN", "UNION"), 98 "OPTIMIZE": (("FOR", "UNKNOWN"),), 99 "ORDER": ("GROUP",), 100 "PARAMETERIZATION": ("FORCED", "SIMPLE"), 101 "ROBUST": ("PLAN",), 102 "USE": ("PLAN",), 103} 104 105OPTIONS_THAT_REQUIRE_EQUAL = ("MAX_GRANT_PERCENT", "MIN_GRANT_PERCENT", "LABEL") 106 107 108def _build_formatted_time( 109 exp_class: t.Type[E], full_format_mapping: t.Optional[bool] = None 110) -> t.Callable[[t.List], E]: 111 def _builder(args: t.List) -> E: 112 fmt = seq_get(args, 0) 113 if isinstance(fmt, exp.Expression): 114 fmt = exp.Literal.string( 115 format_time( 116 fmt.name.lower(), 117 ( 118 {**TSQL.TIME_MAPPING, **FULL_FORMAT_TIME_MAPPING} 119 if full_format_mapping 120 else TSQL.TIME_MAPPING 121 ), 122 ) 123 ) 124 125 this = seq_get(args, 1) 126 if isinstance(this, exp.Expression): 127 this = exp.cast(this, exp.DataType.Type.DATETIME2) 128 129 return exp_class(this=this, format=fmt) 130 131 return _builder 132 133 134def _build_format(args: t.List) -> exp.NumberToStr | exp.TimeToStr: 135 this = seq_get(args, 0) 136 fmt = seq_get(args, 1) 137 culture = seq_get(args, 2) 138 139 number_fmt = fmt and (fmt.name in TRANSPILE_SAFE_NUMBER_FMT or not DATE_FMT_RE.search(fmt.name)) 140 141 if number_fmt: 142 return exp.NumberToStr(this=this, format=fmt, culture=culture) 143 144 if fmt: 145 fmt = exp.Literal.string( 146 format_time(fmt.name, TSQL.FORMAT_TIME_MAPPING) 147 if len(fmt.name) == 1 148 else format_time(fmt.name, TSQL.TIME_MAPPING) 149 ) 150 151 return exp.TimeToStr(this=this, format=fmt, culture=culture) 152 153 154def _build_eomonth(args: t.List) -> exp.LastDay: 155 date = exp.TsOrDsToDate(this=seq_get(args, 0)) 156 month_lag = seq_get(args, 1) 157 158 if month_lag is None: 159 this: exp.Expression = date 160 else: 161 unit = DATE_DELTA_INTERVAL.get("month") 162 this = exp.DateAdd(this=date, expression=month_lag, unit=unit and exp.var(unit)) 163 164 return exp.LastDay(this=this) 165 166 167def _build_hashbytes(args: t.List) -> exp.Expression: 168 kind, data = args 169 kind = kind.name.upper() if kind.is_string else "" 170 171 if kind == "MD5": 172 args.pop(0) 173 return exp.MD5(this=data) 174 if kind in ("SHA", "SHA1"): 175 args.pop(0) 176 return exp.SHA(this=data) 177 if kind == "SHA2_256": 178 return exp.SHA2(this=data, length=exp.Literal.number(256)) 179 if kind == "SHA2_512": 180 return exp.SHA2(this=data, length=exp.Literal.number(512)) 181 182 return exp.func("HASHBYTES", *args) 183 184 185DATEPART_ONLY_FORMATS = {"DW", "WK", "HOUR", "QUARTER"} 186 187 188def _format_sql(self: TSQL.Generator, expression: exp.NumberToStr | exp.TimeToStr) -> str: 189 fmt = expression.args["format"] 190 191 if not isinstance(expression, exp.NumberToStr): 192 if fmt.is_string: 193 mapped_fmt = format_time(fmt.name, TSQL.INVERSE_TIME_MAPPING) 194 195 name = (mapped_fmt or "").upper() 196 if name in DATEPART_ONLY_FORMATS: 197 return self.func("DATEPART", name, expression.this) 198 199 fmt_sql = self.sql(exp.Literal.string(mapped_fmt)) 200 else: 201 fmt_sql = self.format_time(expression) or self.sql(fmt) 202 else: 203 fmt_sql = self.sql(fmt) 204 205 return self.func("FORMAT", expression.this, fmt_sql, expression.args.get("culture")) 206 207 208def _string_agg_sql(self: TSQL.Generator, expression: exp.GroupConcat) -> str: 209 this = expression.this 210 distinct = expression.find(exp.Distinct) 211 if distinct: 212 # exp.Distinct can appear below an exp.Order or an exp.GroupConcat expression 213 self.unsupported("T-SQL STRING_AGG doesn't support DISTINCT.") 214 this = distinct.pop().expressions[0] 215 216 order = "" 217 if isinstance(expression.this, exp.Order): 218 if expression.this.this: 219 this = expression.this.this.pop() 220 # Order has a leading space 221 order = f" WITHIN GROUP ({self.sql(expression.this)[1:]})" 222 223 separator = expression.args.get("separator") or exp.Literal.string(",") 224 return f"STRING_AGG({self.format_args(this, separator)}){order}" 225 226 227def _build_date_delta( 228 exp_class: t.Type[E], unit_mapping: t.Optional[t.Dict[str, str]] = None 229) -> t.Callable[[t.List], E]: 230 def _builder(args: t.List) -> E: 231 unit = seq_get(args, 0) 232 if unit and unit_mapping: 233 unit = exp.var(unit_mapping.get(unit.name.lower(), unit.name)) 234 235 start_date = seq_get(args, 1) 236 if start_date and start_date.is_number: 237 # Numeric types are valid DATETIME values 238 if start_date.is_int: 239 adds = DEFAULT_START_DATE + datetime.timedelta(days=start_date.to_py()) 240 start_date = exp.Literal.string(adds.strftime("%F")) 241 else: 242 # We currently don't handle float values, i.e. they're not converted to equivalent DATETIMEs. 243 # This is not a problem when generating T-SQL code, it is when transpiling to other dialects. 244 return exp_class(this=seq_get(args, 2), expression=start_date, unit=unit) 245 246 return exp_class( 247 this=exp.TimeStrToTime(this=seq_get(args, 2)), 248 expression=exp.TimeStrToTime(this=start_date), 249 unit=unit, 250 ) 251 252 return _builder 253 254 255def qualify_derived_table_outputs(expression: exp.Expression) -> exp.Expression: 256 """Ensures all (unnamed) output columns are aliased for CTEs and Subqueries.""" 257 alias = expression.args.get("alias") 258 259 if ( 260 isinstance(expression, (exp.CTE, exp.Subquery)) 261 and isinstance(alias, exp.TableAlias) 262 and not alias.columns 263 ): 264 from sqlglot.optimizer.qualify_columns import qualify_outputs 265 266 # We keep track of the unaliased column projection indexes instead of the expressions 267 # themselves, because the latter are going to be replaced by new nodes when the aliases 268 # are added and hence we won't be able to reach these newly added Alias parents 269 query = expression.this 270 unaliased_column_indexes = ( 271 i for i, c in enumerate(query.selects) if isinstance(c, exp.Column) and not c.alias 272 ) 273 274 qualify_outputs(query) 275 276 # Preserve the quoting information of columns for newly added Alias nodes 277 query_selects = query.selects 278 for select_index in unaliased_column_indexes: 279 alias = query_selects[select_index] 280 column = alias.this 281 if isinstance(column.this, exp.Identifier): 282 alias.args["alias"].set("quoted", column.this.quoted) 283 284 return expression 285 286 287# https://learn.microsoft.com/en-us/sql/t-sql/functions/datetimefromparts-transact-sql?view=sql-server-ver16#syntax 288def _build_datetimefromparts(args: t.List) -> exp.TimestampFromParts: 289 return exp.TimestampFromParts( 290 year=seq_get(args, 0), 291 month=seq_get(args, 1), 292 day=seq_get(args, 2), 293 hour=seq_get(args, 3), 294 min=seq_get(args, 4), 295 sec=seq_get(args, 5), 296 milli=seq_get(args, 6), 297 ) 298 299 300# https://learn.microsoft.com/en-us/sql/t-sql/functions/timefromparts-transact-sql?view=sql-server-ver16#syntax 301def _build_timefromparts(args: t.List) -> exp.TimeFromParts: 302 return exp.TimeFromParts( 303 hour=seq_get(args, 0), 304 min=seq_get(args, 1), 305 sec=seq_get(args, 2), 306 fractions=seq_get(args, 3), 307 precision=seq_get(args, 4), 308 ) 309 310 311def _build_with_arg_as_text( 312 klass: t.Type[exp.Expression], 313) -> t.Callable[[t.List[exp.Expression]], exp.Expression]: 314 def _parse(args: t.List[exp.Expression]) -> exp.Expression: 315 this = seq_get(args, 0) 316 317 if this and not this.is_string: 318 this = exp.cast(this, exp.DataType.Type.TEXT) 319 320 expression = seq_get(args, 1) 321 kwargs = {"this": this} 322 323 if expression: 324 kwargs["expression"] = expression 325 326 return klass(**kwargs) 327 328 return _parse 329 330 331# https://learn.microsoft.com/en-us/sql/t-sql/functions/parsename-transact-sql?view=sql-server-ver16 332def _build_parsename(args: t.List) -> exp.SplitPart | exp.Anonymous: 333 # PARSENAME(...) will be stored into exp.SplitPart if: 334 # - All args are literals 335 # - The part index (2nd arg) is <= 4 (max valid value, otherwise TSQL returns NULL) 336 if len(args) == 2 and all(isinstance(arg, exp.Literal) for arg in args): 337 this = args[0] 338 part_index = args[1] 339 split_count = len(this.name.split(".")) 340 if split_count <= 4: 341 return exp.SplitPart( 342 this=this, 343 delimiter=exp.Literal.string("."), 344 part_index=exp.Literal.number(split_count + 1 - part_index.to_py()), 345 ) 346 347 return exp.Anonymous(this="PARSENAME", expressions=args) 348 349 350def _build_json_query(args: t.List, dialect: Dialect) -> exp.JSONExtract: 351 if len(args) == 1: 352 # The default value for path is '$'. As a result, if you don't provide a 353 # value for path, JSON_QUERY returns the input expression. 354 args.append(exp.Literal.string("$")) 355 356 return parser.build_extract_json_with_path(exp.JSONExtract)(args, dialect) 357 358 359def _json_extract_sql( 360 self: TSQL.Generator, expression: exp.JSONExtract | exp.JSONExtractScalar 361) -> str: 362 json_query = self.func("JSON_QUERY", expression.this, expression.expression) 363 json_value = self.func("JSON_VALUE", expression.this, expression.expression) 364 return self.func("ISNULL", json_query, json_value) 365 366 367def _timestrtotime_sql(self: TSQL.Generator, expression: exp.TimeStrToTime): 368 sql = timestrtotime_sql(self, expression) 369 if expression.args.get("zone"): 370 # If there is a timezone, produce an expression like: 371 # CAST('2020-01-01 12:13:14-08:00' AS DATETIMEOFFSET) AT TIME ZONE 'UTC' 372 # If you dont have AT TIME ZONE 'UTC', wrapping that expression in another cast back to DATETIME2 just drops the timezone information 373 return self.sql(exp.AtTimeZone(this=sql, zone=exp.Literal.string("UTC"))) 374 return sql 375 376 377def _build_datetrunc(args: t.List) -> exp.TimestampTrunc: 378 unit = seq_get(args, 0) 379 this = seq_get(args, 1) 380 381 if this and this.is_string: 382 this = exp.cast(this, exp.DataType.Type.DATETIME2) 383 384 return exp.TimestampTrunc(this=this, unit=unit) 385 386 387class TSQL(Dialect): 388 SUPPORTS_SEMI_ANTI_JOIN = False 389 LOG_BASE_FIRST = False 390 TYPED_DIVISION = True 391 CONCAT_COALESCE = True 392 NORMALIZATION_STRATEGY = NormalizationStrategy.CASE_INSENSITIVE 393 394 TIME_FORMAT = "'yyyy-mm-dd hh:mm:ss'" 395 396 TIME_MAPPING = { 397 "year": "%Y", 398 "dayofyear": "%j", 399 "day": "%d", 400 "dy": "%d", 401 "y": "%Y", 402 "week": "%W", 403 "ww": "%W", 404 "wk": "%W", 405 "hour": "%h", 406 "hh": "%I", 407 "minute": "%M", 408 "mi": "%M", 409 "n": "%M", 410 "second": "%S", 411 "ss": "%S", 412 "s": "%-S", 413 "millisecond": "%f", 414 "ms": "%f", 415 "weekday": "%w", 416 "dw": "%w", 417 "month": "%m", 418 "mm": "%M", 419 "m": "%-M", 420 "Y": "%Y", 421 "YYYY": "%Y", 422 "YY": "%y", 423 "MMMM": "%B", 424 "MMM": "%b", 425 "MM": "%m", 426 "M": "%-m", 427 "dddd": "%A", 428 "dd": "%d", 429 "d": "%-d", 430 "HH": "%H", 431 "H": "%-H", 432 "h": "%-I", 433 "ffffff": "%f", 434 "yyyy": "%Y", 435 "yy": "%y", 436 } 437 438 CONVERT_FORMAT_MAPPING = { 439 "0": "%b %d %Y %-I:%M%p", 440 "1": "%m/%d/%y", 441 "2": "%y.%m.%d", 442 "3": "%d/%m/%y", 443 "4": "%d.%m.%y", 444 "5": "%d-%m-%y", 445 "6": "%d %b %y", 446 "7": "%b %d, %y", 447 "8": "%H:%M:%S", 448 "9": "%b %d %Y %-I:%M:%S:%f%p", 449 "10": "mm-dd-yy", 450 "11": "yy/mm/dd", 451 "12": "yymmdd", 452 "13": "%d %b %Y %H:%M:ss:%f", 453 "14": "%H:%M:%S:%f", 454 "20": "%Y-%m-%d %H:%M:%S", 455 "21": "%Y-%m-%d %H:%M:%S.%f", 456 "22": "%m/%d/%y %-I:%M:%S %p", 457 "23": "%Y-%m-%d", 458 "24": "%H:%M:%S", 459 "25": "%Y-%m-%d %H:%M:%S.%f", 460 "100": "%b %d %Y %-I:%M%p", 461 "101": "%m/%d/%Y", 462 "102": "%Y.%m.%d", 463 "103": "%d/%m/%Y", 464 "104": "%d.%m.%Y", 465 "105": "%d-%m-%Y", 466 "106": "%d %b %Y", 467 "107": "%b %d, %Y", 468 "108": "%H:%M:%S", 469 "109": "%b %d %Y %-I:%M:%S:%f%p", 470 "110": "%m-%d-%Y", 471 "111": "%Y/%m/%d", 472 "112": "%Y%m%d", 473 "113": "%d %b %Y %H:%M:%S:%f", 474 "114": "%H:%M:%S:%f", 475 "120": "%Y-%m-%d %H:%M:%S", 476 "121": "%Y-%m-%d %H:%M:%S.%f", 477 } 478 479 FORMAT_TIME_MAPPING = { 480 "y": "%B %Y", 481 "d": "%m/%d/%Y", 482 "H": "%-H", 483 "h": "%-I", 484 "s": "%Y-%m-%d %H:%M:%S", 485 "D": "%A,%B,%Y", 486 "f": "%A,%B,%Y %-I:%M %p", 487 "F": "%A,%B,%Y %-I:%M:%S %p", 488 "g": "%m/%d/%Y %-I:%M %p", 489 "G": "%m/%d/%Y %-I:%M:%S %p", 490 "M": "%B %-d", 491 "m": "%B %-d", 492 "O": "%Y-%m-%dT%H:%M:%S", 493 "u": "%Y-%M-%D %H:%M:%S%z", 494 "U": "%A, %B %D, %Y %H:%M:%S%z", 495 "T": "%-I:%M:%S %p", 496 "t": "%-I:%M", 497 "Y": "%a %Y", 498 } 499 500 class Tokenizer(tokens.Tokenizer): 501 IDENTIFIERS = [("[", "]"), '"'] 502 QUOTES = ["'", '"'] 503 HEX_STRINGS = [("0x", ""), ("0X", "")] 504 VAR_SINGLE_TOKENS = {"@", "$", "#"} 505 506 KEYWORDS = { 507 **tokens.Tokenizer.KEYWORDS, 508 "CLUSTERED INDEX": TokenType.INDEX, 509 "DATETIME2": TokenType.DATETIME2, 510 "DATETIMEOFFSET": TokenType.TIMESTAMPTZ, 511 "DECLARE": TokenType.DECLARE, 512 "EXEC": TokenType.COMMAND, 513 "FOR SYSTEM_TIME": TokenType.TIMESTAMP_SNAPSHOT, 514 "GO": TokenType.COMMAND, 515 "IMAGE": TokenType.IMAGE, 516 "MONEY": TokenType.MONEY, 517 "NONCLUSTERED INDEX": TokenType.INDEX, 518 "NTEXT": TokenType.TEXT, 519 "OPTION": TokenType.OPTION, 520 "OUTPUT": TokenType.RETURNING, 521 "PRINT": TokenType.COMMAND, 522 "PROC": TokenType.PROCEDURE, 523 "REAL": TokenType.FLOAT, 524 "ROWVERSION": TokenType.ROWVERSION, 525 "SMALLDATETIME": TokenType.SMALLDATETIME, 526 "SMALLMONEY": TokenType.SMALLMONEY, 527 "SQL_VARIANT": TokenType.VARIANT, 528 "SYSTEM_USER": TokenType.CURRENT_USER, 529 "TOP": TokenType.TOP, 530 "TIMESTAMP": TokenType.ROWVERSION, 531 "TINYINT": TokenType.UTINYINT, 532 "UNIQUEIDENTIFIER": TokenType.UUID, 533 "UPDATE STATISTICS": TokenType.COMMAND, 534 "XML": TokenType.XML, 535 } 536 KEYWORDS.pop("/*+") 537 538 COMMANDS = {*tokens.Tokenizer.COMMANDS, TokenType.END} 539 540 class Parser(parser.Parser): 541 SET_REQUIRES_ASSIGNMENT_DELIMITER = False 542 LOG_DEFAULTS_TO_LN = True 543 ALTER_TABLE_ADD_REQUIRED_FOR_EACH_COLUMN = False 544 STRING_ALIASES = True 545 NO_PAREN_IF_COMMANDS = False 546 547 QUERY_MODIFIER_PARSERS = { 548 **parser.Parser.QUERY_MODIFIER_PARSERS, 549 TokenType.OPTION: lambda self: ("options", self._parse_options()), 550 } 551 552 # T-SQL does not allow BEGIN to be used as an identifier 553 ID_VAR_TOKENS = parser.Parser.ID_VAR_TOKENS - {TokenType.BEGIN} 554 ALIAS_TOKENS = parser.Parser.ALIAS_TOKENS - {TokenType.BEGIN} 555 TABLE_ALIAS_TOKENS = parser.Parser.TABLE_ALIAS_TOKENS - {TokenType.BEGIN} 556 COMMENT_TABLE_ALIAS_TOKENS = parser.Parser.COMMENT_TABLE_ALIAS_TOKENS - {TokenType.BEGIN} 557 UPDATE_ALIAS_TOKENS = parser.Parser.UPDATE_ALIAS_TOKENS - {TokenType.BEGIN} 558 559 FUNCTIONS = { 560 **parser.Parser.FUNCTIONS, 561 "CHARINDEX": lambda args: exp.StrPosition( 562 this=seq_get(args, 1), 563 substr=seq_get(args, 0), 564 position=seq_get(args, 2), 565 ), 566 "COUNT": lambda args: exp.Count( 567 this=seq_get(args, 0), expressions=args[1:], big_int=False 568 ), 569 "COUNT_BIG": lambda args: exp.Count( 570 this=seq_get(args, 0), expressions=args[1:], big_int=True 571 ), 572 "DATEADD": build_date_delta(exp.DateAdd, unit_mapping=DATE_DELTA_INTERVAL), 573 "DATEDIFF": _build_date_delta(exp.DateDiff, unit_mapping=DATE_DELTA_INTERVAL), 574 "DATENAME": _build_formatted_time(exp.TimeToStr, full_format_mapping=True), 575 "DATEPART": _build_formatted_time(exp.TimeToStr), 576 "DATETIMEFROMPARTS": _build_datetimefromparts, 577 "EOMONTH": _build_eomonth, 578 "FORMAT": _build_format, 579 "GETDATE": exp.CurrentTimestamp.from_arg_list, 580 "HASHBYTES": _build_hashbytes, 581 "ISNULL": lambda args: build_coalesce(args=args, is_null=True), 582 "JSON_QUERY": _build_json_query, 583 "JSON_VALUE": parser.build_extract_json_with_path(exp.JSONExtractScalar), 584 "LEN": _build_with_arg_as_text(exp.Length), 585 "LEFT": _build_with_arg_as_text(exp.Left), 586 "NEWID": exp.Uuid.from_arg_list, 587 "RIGHT": _build_with_arg_as_text(exp.Right), 588 "PARSENAME": _build_parsename, 589 "REPLICATE": exp.Repeat.from_arg_list, 590 "SCHEMA_NAME": exp.CurrentSchema.from_arg_list, 591 "SQUARE": lambda args: exp.Pow(this=seq_get(args, 0), expression=exp.Literal.number(2)), 592 "SYSDATETIME": exp.CurrentTimestamp.from_arg_list, 593 "SUSER_NAME": exp.CurrentUser.from_arg_list, 594 "SUSER_SNAME": exp.CurrentUser.from_arg_list, 595 "SYSTEM_USER": exp.CurrentUser.from_arg_list, 596 "TIMEFROMPARTS": _build_timefromparts, 597 "DATETRUNC": _build_datetrunc, 598 } 599 600 JOIN_HINTS = {"LOOP", "HASH", "MERGE", "REMOTE"} 601 602 PROCEDURE_OPTIONS = dict.fromkeys( 603 ("ENCRYPTION", "RECOMPILE", "SCHEMABINDING", "NATIVE_COMPILATION", "EXECUTE"), tuple() 604 ) 605 606 COLUMN_DEFINITION_MODES = {"OUT", "OUTPUT", "READ_ONLY"} 607 608 RETURNS_TABLE_TOKENS = parser.Parser.ID_VAR_TOKENS - { 609 TokenType.TABLE, 610 *parser.Parser.TYPE_TOKENS, 611 } 612 613 STATEMENT_PARSERS = { 614 **parser.Parser.STATEMENT_PARSERS, 615 TokenType.DECLARE: lambda self: self._parse_declare(), 616 } 617 618 RANGE_PARSERS = { 619 **parser.Parser.RANGE_PARSERS, 620 TokenType.DCOLON: lambda self, this: self.expression( 621 exp.ScopeResolution, 622 this=this, 623 expression=self._parse_function() or self._parse_var(any_token=True), 624 ), 625 } 626 627 NO_PAREN_FUNCTION_PARSERS = { 628 **parser.Parser.NO_PAREN_FUNCTION_PARSERS, 629 "NEXT": lambda self: self._parse_next_value_for(), 630 } 631 632 # The DCOLON (::) operator serves as a scope resolution (exp.ScopeResolution) operator in T-SQL 633 COLUMN_OPERATORS = { 634 **parser.Parser.COLUMN_OPERATORS, 635 TokenType.DCOLON: lambda self, this, to: self.expression(exp.Cast, this=this, to=to) 636 if isinstance(to, exp.DataType) and to.this != exp.DataType.Type.USERDEFINED 637 else self.expression(exp.ScopeResolution, this=this, expression=to), 638 } 639 640 def _parse_wrapped_select(self, table: bool = False) -> t.Optional[exp.Expression]: 641 if self._match(TokenType.MERGE): 642 comments = self._prev_comments 643 merge = self._parse_merge() 644 merge.add_comments(comments, prepend=True) 645 return merge 646 647 return super()._parse_wrapped_select(table=table) 648 649 def _parse_dcolon(self) -> t.Optional[exp.Expression]: 650 # We want to use _parse_types() if the first token after :: is a known type, 651 # otherwise we could parse something like x::varchar(max) into a function 652 if self._match_set(self.TYPE_TOKENS, advance=False): 653 return self._parse_types() 654 655 return self._parse_function() or self._parse_types() 656 657 def _parse_options(self) -> t.Optional[t.List[exp.Expression]]: 658 if not self._match(TokenType.OPTION): 659 return None 660 661 def _parse_option() -> t.Optional[exp.Expression]: 662 option = self._parse_var_from_options(OPTIONS) 663 if not option: 664 return None 665 666 self._match(TokenType.EQ) 667 return self.expression( 668 exp.QueryOption, this=option, expression=self._parse_primary_or_var() 669 ) 670 671 return self._parse_wrapped_csv(_parse_option) 672 673 def _parse_projections(self) -> t.List[exp.Expression]: 674 """ 675 T-SQL supports the syntax alias = expression in the SELECT's projection list, 676 so we transform all parsed Selects to convert their EQ projections into Aliases. 677 678 See: https://learn.microsoft.com/en-us/sql/t-sql/queries/select-clause-transact-sql?view=sql-server-ver16#syntax 679 """ 680 return [ 681 ( 682 exp.alias_(projection.expression, projection.this.this, copy=False) 683 if isinstance(projection, exp.EQ) and isinstance(projection.this, exp.Column) 684 else projection 685 ) 686 for projection in super()._parse_projections() 687 ] 688 689 def _parse_commit_or_rollback(self) -> exp.Commit | exp.Rollback: 690 """Applies to SQL Server and Azure SQL Database 691 COMMIT [ { TRAN | TRANSACTION } 692 [ transaction_name | @tran_name_variable ] ] 693 [ WITH ( DELAYED_DURABILITY = { OFF | ON } ) ] 694 695 ROLLBACK { TRAN | TRANSACTION } 696 [ transaction_name | @tran_name_variable 697 | savepoint_name | @savepoint_variable ] 698 """ 699 rollback = self._prev.token_type == TokenType.ROLLBACK 700 701 self._match_texts(("TRAN", "TRANSACTION")) 702 this = self._parse_id_var() 703 704 if rollback: 705 return self.expression(exp.Rollback, this=this) 706 707 durability = None 708 if self._match_pair(TokenType.WITH, TokenType.L_PAREN): 709 self._match_text_seq("DELAYED_DURABILITY") 710 self._match(TokenType.EQ) 711 712 if self._match_text_seq("OFF"): 713 durability = False 714 else: 715 self._match(TokenType.ON) 716 durability = True 717 718 self._match_r_paren() 719 720 return self.expression(exp.Commit, this=this, durability=durability) 721 722 def _parse_transaction(self) -> exp.Transaction | exp.Command: 723 """Applies to SQL Server and Azure SQL Database 724 BEGIN { TRAN | TRANSACTION } 725 [ { transaction_name | @tran_name_variable } 726 [ WITH MARK [ 'description' ] ] 727 ] 728 """ 729 if self._match_texts(("TRAN", "TRANSACTION")): 730 transaction = self.expression(exp.Transaction, this=self._parse_id_var()) 731 if self._match_text_seq("WITH", "MARK"): 732 transaction.set("mark", self._parse_string()) 733 734 return transaction 735 736 return self._parse_as_command(self._prev) 737 738 def _parse_returns(self) -> exp.ReturnsProperty: 739 table = self._parse_id_var(any_token=False, tokens=self.RETURNS_TABLE_TOKENS) 740 returns = super()._parse_returns() 741 returns.set("table", table) 742 return returns 743 744 def _parse_convert( 745 self, strict: bool, safe: t.Optional[bool] = None 746 ) -> t.Optional[exp.Expression]: 747 this = self._parse_types() 748 self._match(TokenType.COMMA) 749 args = [this, *self._parse_csv(self._parse_assignment)] 750 convert = exp.Convert.from_arg_list(args) 751 convert.set("safe", safe) 752 convert.set("strict", strict) 753 return convert 754 755 def _parse_column_def( 756 self, this: t.Optional[exp.Expression], computed_column: bool = True 757 ) -> t.Optional[exp.Expression]: 758 this = super()._parse_column_def(this=this, computed_column=computed_column) 759 if not this: 760 return None 761 if self._match(TokenType.EQ): 762 this.set("default", self._parse_disjunction()) 763 if self._match_texts(self.COLUMN_DEFINITION_MODES): 764 this.set("output", self._prev.text) 765 return this 766 767 def _parse_user_defined_function( 768 self, kind: t.Optional[TokenType] = None 769 ) -> t.Optional[exp.Expression]: 770 this = super()._parse_user_defined_function(kind=kind) 771 772 if ( 773 kind == TokenType.FUNCTION 774 or isinstance(this, exp.UserDefinedFunction) 775 or self._match(TokenType.ALIAS, advance=False) 776 ): 777 return this 778 779 if not self._match(TokenType.WITH, advance=False): 780 expressions = self._parse_csv(self._parse_function_parameter) 781 else: 782 expressions = None 783 784 return self.expression(exp.UserDefinedFunction, this=this, expressions=expressions) 785 786 def _parse_into(self) -> t.Optional[exp.Into]: 787 into = super()._parse_into() 788 789 table = isinstance(into, exp.Into) and into.find(exp.Table) 790 if isinstance(table, exp.Table): 791 table_identifier = table.this 792 if table_identifier.args.get("temporary"): 793 # Promote the temporary property from the Identifier to the Into expression 794 t.cast(exp.Into, into).set("temporary", True) 795 796 return into 797 798 def _parse_id_var( 799 self, 800 any_token: bool = True, 801 tokens: t.Optional[t.Collection[TokenType]] = None, 802 ) -> t.Optional[exp.Expression]: 803 is_temporary = self._match(TokenType.HASH) 804 is_global = is_temporary and self._match(TokenType.HASH) 805 806 this = super()._parse_id_var(any_token=any_token, tokens=tokens) 807 if this: 808 if is_global: 809 this.set("global", True) 810 elif is_temporary: 811 this.set("temporary", True) 812 813 return this 814 815 def _parse_create(self) -> exp.Create | exp.Command: 816 create = super()._parse_create() 817 818 if isinstance(create, exp.Create): 819 table = create.this.this if isinstance(create.this, exp.Schema) else create.this 820 if isinstance(table, exp.Table) and table.this and table.this.args.get("temporary"): 821 if not create.args.get("properties"): 822 create.set("properties", exp.Properties(expressions=[])) 823 824 create.args["properties"].append("expressions", exp.TemporaryProperty()) 825 826 return create 827 828 def _parse_if(self) -> t.Optional[exp.Expression]: 829 index = self._index 830 831 if self._match_text_seq("OBJECT_ID"): 832 self._parse_wrapped_csv(self._parse_string) 833 if self._match_text_seq("IS", "NOT", "NULL") and self._match(TokenType.DROP): 834 return self._parse_drop(exists=True) 835 self._retreat(index) 836 837 return super()._parse_if() 838 839 def _parse_unique(self) -> exp.UniqueColumnConstraint: 840 if self._match_texts(("CLUSTERED", "NONCLUSTERED")): 841 this = self.CONSTRAINT_PARSERS[self._prev.text.upper()](self) 842 else: 843 this = self._parse_schema(self._parse_id_var(any_token=False)) 844 845 return self.expression(exp.UniqueColumnConstraint, this=this) 846 847 def _parse_partition(self) -> t.Optional[exp.Partition]: 848 if not self._match_text_seq("WITH", "(", "PARTITIONS"): 849 return None 850 851 def parse_range(): 852 low = self._parse_bitwise() 853 high = self._parse_bitwise() if self._match_text_seq("TO") else None 854 855 return ( 856 self.expression(exp.PartitionRange, this=low, expression=high) if high else low 857 ) 858 859 partition = self.expression( 860 exp.Partition, expressions=self._parse_wrapped_csv(parse_range) 861 ) 862 863 self._match_r_paren() 864 865 return partition 866 867 def _parse_declare(self) -> exp.Declare | exp.Command: 868 index = self._index 869 expressions = self._try_parse(partial(self._parse_csv, self._parse_declareitem)) 870 871 if not expressions or self._curr: 872 self._retreat(index) 873 return self._parse_as_command(self._prev) 874 875 return self.expression(exp.Declare, expressions=expressions) 876 877 def _parse_declareitem(self) -> t.Optional[exp.DeclareItem]: 878 var = self._parse_id_var() 879 if not var: 880 return None 881 882 value = None 883 self._match(TokenType.ALIAS) 884 if self._match(TokenType.TABLE): 885 data_type = self._parse_schema() 886 else: 887 data_type = self._parse_types() 888 if self._match(TokenType.EQ): 889 value = self._parse_bitwise() 890 891 return self.expression(exp.DeclareItem, this=var, kind=data_type, default=value) 892 893 def _parse_alter_table_alter(self) -> t.Optional[exp.Expression]: 894 expression = super()._parse_alter_table_alter() 895 896 if expression is not None: 897 collation = expression.args.get("collate") 898 if isinstance(collation, exp.Column) and isinstance(collation.this, exp.Identifier): 899 identifier = collation.this 900 collation.set("this", exp.Var(this=identifier.name)) 901 902 return expression 903 904 class Generator(generator.Generator): 905 LIMIT_IS_TOP = True 906 QUERY_HINTS = False 907 RETURNING_END = False 908 NVL2_SUPPORTED = False 909 ALTER_TABLE_INCLUDE_COLUMN_KEYWORD = False 910 LIMIT_FETCH = "FETCH" 911 COMPUTED_COLUMN_WITH_TYPE = False 912 CTE_RECURSIVE_KEYWORD_REQUIRED = False 913 ENSURE_BOOLS = True 914 NULL_ORDERING_SUPPORTED = None 915 SUPPORTS_SINGLE_ARG_CONCAT = False 916 TABLESAMPLE_SEED_KEYWORD = "REPEATABLE" 917 SUPPORTS_SELECT_INTO = True 918 JSON_PATH_BRACKETED_KEY_SUPPORTED = False 919 SUPPORTS_TO_NUMBER = False 920 SET_OP_MODIFIERS = False 921 COPY_PARAMS_EQ_REQUIRED = True 922 PARSE_JSON_NAME = None 923 EXCEPT_INTERSECT_SUPPORT_ALL_CLAUSE = False 924 ALTER_SET_TYPE = "" 925 926 EXPRESSIONS_WITHOUT_NESTED_CTES = { 927 exp.Create, 928 exp.Delete, 929 exp.Insert, 930 exp.Intersect, 931 exp.Except, 932 exp.Merge, 933 exp.Select, 934 exp.Subquery, 935 exp.Union, 936 exp.Update, 937 } 938 939 SUPPORTED_JSON_PATH_PARTS = { 940 exp.JSONPathKey, 941 exp.JSONPathRoot, 942 exp.JSONPathSubscript, 943 } 944 945 TYPE_MAPPING = { 946 **generator.Generator.TYPE_MAPPING, 947 exp.DataType.Type.BOOLEAN: "BIT", 948 exp.DataType.Type.DATETIME2: "DATETIME2", 949 exp.DataType.Type.DECIMAL: "NUMERIC", 950 exp.DataType.Type.DOUBLE: "FLOAT", 951 exp.DataType.Type.INT: "INTEGER", 952 exp.DataType.Type.ROWVERSION: "ROWVERSION", 953 exp.DataType.Type.TEXT: "VARCHAR(MAX)", 954 exp.DataType.Type.TIMESTAMP: "DATETIME2", 955 exp.DataType.Type.TIMESTAMPNTZ: "DATETIME2", 956 exp.DataType.Type.TIMESTAMPTZ: "DATETIMEOFFSET", 957 exp.DataType.Type.SMALLDATETIME: "SMALLDATETIME", 958 exp.DataType.Type.UTINYINT: "TINYINT", 959 exp.DataType.Type.VARIANT: "SQL_VARIANT", 960 exp.DataType.Type.UUID: "UNIQUEIDENTIFIER", 961 } 962 963 TYPE_MAPPING.pop(exp.DataType.Type.NCHAR) 964 TYPE_MAPPING.pop(exp.DataType.Type.NVARCHAR) 965 966 TRANSFORMS = { 967 **generator.Generator.TRANSFORMS, 968 exp.AnyValue: any_value_to_max_sql, 969 exp.ArrayToString: rename_func("STRING_AGG"), 970 exp.AutoIncrementColumnConstraint: lambda *_: "IDENTITY", 971 exp.Chr: rename_func("CHAR"), 972 exp.DateAdd: date_delta_sql("DATEADD"), 973 exp.DateDiff: date_delta_sql("DATEDIFF"), 974 exp.CTE: transforms.preprocess([qualify_derived_table_outputs]), 975 exp.CurrentDate: rename_func("GETDATE"), 976 exp.CurrentTimestamp: rename_func("GETDATE"), 977 exp.DateStrToDate: datestrtodate_sql, 978 exp.Extract: rename_func("DATEPART"), 979 exp.GeneratedAsIdentityColumnConstraint: generatedasidentitycolumnconstraint_sql, 980 exp.GroupConcat: _string_agg_sql, 981 exp.If: rename_func("IIF"), 982 exp.JSONExtract: _json_extract_sql, 983 exp.JSONExtractScalar: _json_extract_sql, 984 exp.LastDay: lambda self, e: self.func("EOMONTH", e.this), 985 exp.Ln: rename_func("LOG"), 986 exp.Max: max_or_greatest, 987 exp.MD5: lambda self, e: self.func("HASHBYTES", exp.Literal.string("MD5"), e.this), 988 exp.Min: min_or_least, 989 exp.NumberToStr: _format_sql, 990 exp.Repeat: rename_func("REPLICATE"), 991 exp.CurrentSchema: rename_func("SCHEMA_NAME"), 992 exp.Select: transforms.preprocess( 993 [ 994 transforms.eliminate_distinct_on, 995 transforms.eliminate_semi_and_anti_joins, 996 transforms.eliminate_qualify, 997 transforms.unnest_generate_date_array_using_recursive_cte, 998 ] 999 ), 1000 exp.Stddev: rename_func("STDEV"), 1001 exp.StrPosition: lambda self, e: strposition_sql( 1002 self, e, func_name="CHARINDEX", supports_position=True 1003 ), 1004 exp.Subquery: transforms.preprocess([qualify_derived_table_outputs]), 1005 exp.SHA: lambda self, e: self.func("HASHBYTES", exp.Literal.string("SHA1"), e.this), 1006 exp.SHA2: lambda self, e: self.func( 1007 "HASHBYTES", exp.Literal.string(f"SHA2_{e.args.get('length', 256)}"), e.this 1008 ), 1009 exp.TemporaryProperty: lambda self, e: "", 1010 exp.TimeStrToTime: _timestrtotime_sql, 1011 exp.TimeToStr: _format_sql, 1012 exp.Trim: trim_sql, 1013 exp.TsOrDsAdd: date_delta_sql("DATEADD", cast=True), 1014 exp.TsOrDsDiff: date_delta_sql("DATEDIFF"), 1015 exp.TimestampTrunc: lambda self, e: self.func("DATETRUNC", e.unit, e.this), 1016 exp.Uuid: lambda *_: "NEWID()", 1017 exp.DateFromParts: rename_func("DATEFROMPARTS"), 1018 } 1019 1020 TRANSFORMS.pop(exp.ReturnsProperty) 1021 1022 PROPERTIES_LOCATION = { 1023 **generator.Generator.PROPERTIES_LOCATION, 1024 exp.VolatileProperty: exp.Properties.Location.UNSUPPORTED, 1025 } 1026 1027 def scope_resolution(self, rhs: str, scope_name: str) -> str: 1028 return f"{scope_name}::{rhs}" 1029 1030 def select_sql(self, expression: exp.Select) -> str: 1031 limit = expression.args.get("limit") 1032 offset = expression.args.get("offset") 1033 1034 if isinstance(limit, exp.Fetch) and not offset: 1035 # Dialects like Oracle can FETCH directly from a row set but 1036 # T-SQL requires an ORDER BY + OFFSET clause in order to FETCH 1037 offset = exp.Offset(expression=exp.Literal.number(0)) 1038 expression.set("offset", offset) 1039 1040 if offset: 1041 if not expression.args.get("order"): 1042 # ORDER BY is required in order to use OFFSET in a query, so we use 1043 # a noop order by, since we don't really care about the order. 1044 # See: https://www.microsoftpressstore.com/articles/article.aspx?p=2314819 1045 expression.order_by(exp.select(exp.null()).subquery(), copy=False) 1046 1047 if isinstance(limit, exp.Limit): 1048 # TOP and OFFSET can't be combined, we need use FETCH instead of TOP 1049 # we replace here because otherwise TOP would be generated in select_sql 1050 limit.replace(exp.Fetch(direction="FIRST", count=limit.expression)) 1051 1052 return super().select_sql(expression) 1053 1054 def convert_sql(self, expression: exp.Convert) -> str: 1055 name = "TRY_CONVERT" if expression.args.get("safe") else "CONVERT" 1056 return self.func( 1057 name, expression.this, expression.expression, expression.args.get("style") 1058 ) 1059 1060 def queryoption_sql(self, expression: exp.QueryOption) -> str: 1061 option = self.sql(expression, "this") 1062 value = self.sql(expression, "expression") 1063 if value: 1064 optional_equal_sign = "= " if option in OPTIONS_THAT_REQUIRE_EQUAL else "" 1065 return f"{option} {optional_equal_sign}{value}" 1066 return option 1067 1068 def lateral_op(self, expression: exp.Lateral) -> str: 1069 cross_apply = expression.args.get("cross_apply") 1070 if cross_apply is True: 1071 return "CROSS APPLY" 1072 if cross_apply is False: 1073 return "OUTER APPLY" 1074 1075 # TODO: perhaps we can check if the parent is a Join and transpile it appropriately 1076 self.unsupported("LATERAL clause is not supported.") 1077 return "LATERAL" 1078 1079 def splitpart_sql(self: TSQL.Generator, expression: exp.SplitPart) -> str: 1080 this = expression.this 1081 split_count = len(this.name.split(".")) 1082 delimiter = expression.args.get("delimiter") 1083 part_index = expression.args.get("part_index") 1084 1085 if ( 1086 not all(isinstance(arg, exp.Literal) for arg in (this, delimiter, part_index)) 1087 or (delimiter and delimiter.name != ".") 1088 or not part_index 1089 or split_count > 4 1090 ): 1091 self.unsupported( 1092 "SPLIT_PART can be transpiled to PARSENAME only for '.' delimiter and literal values" 1093 ) 1094 return "" 1095 1096 return self.func( 1097 "PARSENAME", this, exp.Literal.number(split_count + 1 - part_index.to_py()) 1098 ) 1099 1100 def timefromparts_sql(self, expression: exp.TimeFromParts) -> str: 1101 nano = expression.args.get("nano") 1102 if nano is not None: 1103 nano.pop() 1104 self.unsupported("Specifying nanoseconds is not supported in TIMEFROMPARTS.") 1105 1106 if expression.args.get("fractions") is None: 1107 expression.set("fractions", exp.Literal.number(0)) 1108 if expression.args.get("precision") is None: 1109 expression.set("precision", exp.Literal.number(0)) 1110 1111 return rename_func("TIMEFROMPARTS")(self, expression) 1112 1113 def timestampfromparts_sql(self, expression: exp.TimestampFromParts) -> str: 1114 zone = expression.args.get("zone") 1115 if zone is not None: 1116 zone.pop() 1117 self.unsupported("Time zone is not supported in DATETIMEFROMPARTS.") 1118 1119 nano = expression.args.get("nano") 1120 if nano is not None: 1121 nano.pop() 1122 self.unsupported("Specifying nanoseconds is not supported in DATETIMEFROMPARTS.") 1123 1124 if expression.args.get("milli") is None: 1125 expression.set("milli", exp.Literal.number(0)) 1126 1127 return rename_func("DATETIMEFROMPARTS")(self, expression) 1128 1129 def setitem_sql(self, expression: exp.SetItem) -> str: 1130 this = expression.this 1131 if isinstance(this, exp.EQ) and not isinstance(this.left, exp.Parameter): 1132 # T-SQL does not use '=' in SET command, except when the LHS is a variable. 1133 return f"{self.sql(this.left)} {self.sql(this.right)}" 1134 1135 return super().setitem_sql(expression) 1136 1137 def boolean_sql(self, expression: exp.Boolean) -> str: 1138 if type(expression.parent) in BIT_TYPES or isinstance( 1139 expression.find_ancestor(exp.Values, exp.Select), exp.Values 1140 ): 1141 return "1" if expression.this else "0" 1142 1143 return "(1 = 1)" if expression.this else "(1 = 0)" 1144 1145 def is_sql(self, expression: exp.Is) -> str: 1146 if isinstance(expression.expression, exp.Boolean): 1147 return self.binary(expression, "=") 1148 return self.binary(expression, "IS") 1149 1150 def createable_sql(self, expression: exp.Create, locations: t.DefaultDict) -> str: 1151 sql = self.sql(expression, "this") 1152 properties = expression.args.get("properties") 1153 1154 if sql[:1] != "#" and any( 1155 isinstance(prop, exp.TemporaryProperty) 1156 for prop in (properties.expressions if properties else []) 1157 ): 1158 sql = f"[#{sql[1:]}" if sql.startswith("[") else f"#{sql}" 1159 1160 return sql 1161 1162 def create_sql(self, expression: exp.Create) -> str: 1163 kind = expression.kind 1164 exists = expression.args.pop("exists", None) 1165 1166 like_property = expression.find(exp.LikeProperty) 1167 if like_property: 1168 ctas_expression = like_property.this 1169 else: 1170 ctas_expression = expression.expression 1171 1172 if kind == "VIEW": 1173 expression.this.set("catalog", None) 1174 with_ = expression.args.get("with") 1175 if ctas_expression and with_: 1176 # We've already preprocessed the Create expression to bubble up any nested CTEs, 1177 # but CREATE VIEW actually requires the WITH clause to come after it so we need 1178 # to amend the AST by moving the CTEs to the CREATE VIEW statement's query. 1179 ctas_expression.set("with", with_.pop()) 1180 1181 sql = super().create_sql(expression) 1182 1183 table = expression.find(exp.Table) 1184 1185 # Convert CTAS statement to SELECT .. INTO .. 1186 if kind == "TABLE" and ctas_expression: 1187 if isinstance(ctas_expression, exp.UNWRAPPED_QUERIES): 1188 ctas_expression = ctas_expression.subquery() 1189 1190 properties = expression.args.get("properties") or exp.Properties() 1191 is_temp = any(isinstance(p, exp.TemporaryProperty) for p in properties.expressions) 1192 1193 select_into = exp.select("*").from_(exp.alias_(ctas_expression, "temp", table=True)) 1194 select_into.set("into", exp.Into(this=table, temporary=is_temp)) 1195 1196 if like_property: 1197 select_into.limit(0, copy=False) 1198 1199 sql = self.sql(select_into) 1200 1201 if exists: 1202 identifier = self.sql(exp.Literal.string(exp.table_name(table) if table else "")) 1203 sql_with_ctes = self.prepend_ctes(expression, sql) 1204 sql_literal = self.sql(exp.Literal.string(sql_with_ctes)) 1205 if kind == "SCHEMA": 1206 return f"""IF NOT EXISTS (SELECT * FROM information_schema.schemata WHERE schema_name = {identifier}) EXEC({sql_literal})""" 1207 elif kind == "TABLE": 1208 assert table 1209 where = exp.and_( 1210 exp.column("table_name").eq(table.name), 1211 exp.column("table_schema").eq(table.db) if table.db else None, 1212 exp.column("table_catalog").eq(table.catalog) if table.catalog else None, 1213 ) 1214 return f"""IF NOT EXISTS (SELECT * FROM information_schema.tables WHERE {where}) EXEC({sql_literal})""" 1215 elif kind == "INDEX": 1216 index = self.sql(exp.Literal.string(expression.this.text("this"))) 1217 return f"""IF NOT EXISTS (SELECT * FROM sys.indexes WHERE object_id = object_id({identifier}) AND name = {index}) EXEC({sql_literal})""" 1218 elif expression.args.get("replace"): 1219 sql = sql.replace("CREATE OR REPLACE ", "CREATE OR ALTER ", 1) 1220 1221 return self.prepend_ctes(expression, sql) 1222 1223 @generator.unsupported_args("unlogged", "expressions") 1224 def into_sql(self, expression: exp.Into) -> str: 1225 if expression.args.get("temporary"): 1226 # If the Into expression has a temporary property, push this down to the Identifier 1227 table = expression.find(exp.Table) 1228 if table and isinstance(table.this, exp.Identifier): 1229 table.this.set("temporary", True) 1230 1231 return f"{self.seg('INTO')} {self.sql(expression, 'this')}" 1232 1233 def count_sql(self, expression: exp.Count) -> str: 1234 func_name = "COUNT_BIG" if expression.args.get("big_int") else "COUNT" 1235 return rename_func(func_name)(self, expression) 1236 1237 def offset_sql(self, expression: exp.Offset) -> str: 1238 return f"{super().offset_sql(expression)} ROWS" 1239 1240 def version_sql(self, expression: exp.Version) -> str: 1241 name = "SYSTEM_TIME" if expression.name == "TIMESTAMP" else expression.name 1242 this = f"FOR {name}" 1243 expr = expression.expression 1244 kind = expression.text("kind") 1245 if kind in ("FROM", "BETWEEN"): 1246 args = expr.expressions 1247 sep = "TO" if kind == "FROM" else "AND" 1248 expr_sql = f"{self.sql(seq_get(args, 0))} {sep} {self.sql(seq_get(args, 1))}" 1249 else: 1250 expr_sql = self.sql(expr) 1251 1252 expr_sql = f" {expr_sql}" if expr_sql else "" 1253 return f"{this} {kind}{expr_sql}" 1254 1255 def returnsproperty_sql(self, expression: exp.ReturnsProperty) -> str: 1256 table = expression.args.get("table") 1257 table = f"{table} " if table else "" 1258 return f"RETURNS {table}{self.sql(expression, 'this')}" 1259 1260 def returning_sql(self, expression: exp.Returning) -> str: 1261 into = self.sql(expression, "into") 1262 into = self.seg(f"INTO {into}") if into else "" 1263 return f"{self.seg('OUTPUT')} {self.expressions(expression, flat=True)}{into}" 1264 1265 def transaction_sql(self, expression: exp.Transaction) -> str: 1266 this = self.sql(expression, "this") 1267 this = f" {this}" if this else "" 1268 mark = self.sql(expression, "mark") 1269 mark = f" WITH MARK {mark}" if mark else "" 1270 return f"BEGIN TRANSACTION{this}{mark}" 1271 1272 def commit_sql(self, expression: exp.Commit) -> str: 1273 this = self.sql(expression, "this") 1274 this = f" {this}" if this else "" 1275 durability = expression.args.get("durability") 1276 durability = ( 1277 f" WITH (DELAYED_DURABILITY = {'ON' if durability else 'OFF'})" 1278 if durability is not None 1279 else "" 1280 ) 1281 return f"COMMIT TRANSACTION{this}{durability}" 1282 1283 def rollback_sql(self, expression: exp.Rollback) -> str: 1284 this = self.sql(expression, "this") 1285 this = f" {this}" if this else "" 1286 return f"ROLLBACK TRANSACTION{this}" 1287 1288 def identifier_sql(self, expression: exp.Identifier) -> str: 1289 identifier = super().identifier_sql(expression) 1290 1291 if expression.args.get("global"): 1292 identifier = f"##{identifier}" 1293 elif expression.args.get("temporary"): 1294 identifier = f"#{identifier}" 1295 1296 return identifier 1297 1298 def constraint_sql(self, expression: exp.Constraint) -> str: 1299 this = self.sql(expression, "this") 1300 expressions = self.expressions(expression, flat=True, sep=" ") 1301 return f"CONSTRAINT {this} {expressions}" 1302 1303 def length_sql(self, expression: exp.Length) -> str: 1304 return self._uncast_text(expression, "LEN") 1305 1306 def right_sql(self, expression: exp.Right) -> str: 1307 return self._uncast_text(expression, "RIGHT") 1308 1309 def left_sql(self, expression: exp.Left) -> str: 1310 return self._uncast_text(expression, "LEFT") 1311 1312 def _uncast_text(self, expression: exp.Expression, name: str) -> str: 1313 this = expression.this 1314 if isinstance(this, exp.Cast) and this.is_type(exp.DataType.Type.TEXT): 1315 this_sql = self.sql(this, "this") 1316 else: 1317 this_sql = self.sql(this) 1318 expression_sql = self.sql(expression, "expression") 1319 return self.func(name, this_sql, expression_sql if expression_sql else None) 1320 1321 def partition_sql(self, expression: exp.Partition) -> str: 1322 return f"WITH (PARTITIONS({self.expressions(expression, flat=True)}))" 1323 1324 def alter_sql(self, expression: exp.Alter) -> str: 1325 action = seq_get(expression.args.get("actions") or [], 0) 1326 if isinstance(action, exp.AlterRename): 1327 return f"EXEC sp_rename '{self.sql(expression.this)}', '{action.this.name}'" 1328 return super().alter_sql(expression) 1329 1330 def drop_sql(self, expression: exp.Drop) -> str: 1331 if expression.args["kind"] == "VIEW": 1332 expression.this.set("catalog", None) 1333 return super().drop_sql(expression) 1334 1335 def options_modifier(self, expression: exp.Expression) -> str: 1336 options = self.expressions(expression, key="options") 1337 return f" OPTION{self.wrap(options)}" if options else "" 1338 1339 def dpipe_sql(self, expression: exp.DPipe) -> str: 1340 return self.sql( 1341 reduce(lambda x, y: exp.Add(this=x, expression=y), expression.flatten()) 1342 ) 1343 1344 def isascii_sql(self, expression: exp.IsAscii) -> str: 1345 return f"(PATINDEX(CONVERT(VARCHAR(MAX), 0x255b5e002d7f5d25) COLLATE Latin1_General_BIN, {self.sql(expression.this)}) = 0)" 1346 1347 def columndef_sql(self, expression: exp.ColumnDef, sep: str = " ") -> str: 1348 this = super().columndef_sql(expression, sep) 1349 default = self.sql(expression, "default") 1350 default = f" = {default}" if default else "" 1351 output = self.sql(expression, "output") 1352 output = f" {output}" if output else "" 1353 return f"{this}{default}{output}" 1354 1355 def coalesce_sql(self, expression: exp.Coalesce) -> str: 1356 func_name = "ISNULL" if expression.args.get("is_null") else "COALESCE" 1357 return rename_func(func_name)(self, expression)
256def qualify_derived_table_outputs(expression: exp.Expression) -> exp.Expression: 257 """Ensures all (unnamed) output columns are aliased for CTEs and Subqueries.""" 258 alias = expression.args.get("alias") 259 260 if ( 261 isinstance(expression, (exp.CTE, exp.Subquery)) 262 and isinstance(alias, exp.TableAlias) 263 and not alias.columns 264 ): 265 from sqlglot.optimizer.qualify_columns import qualify_outputs 266 267 # We keep track of the unaliased column projection indexes instead of the expressions 268 # themselves, because the latter are going to be replaced by new nodes when the aliases 269 # are added and hence we won't be able to reach these newly added Alias parents 270 query = expression.this 271 unaliased_column_indexes = ( 272 i for i, c in enumerate(query.selects) if isinstance(c, exp.Column) and not c.alias 273 ) 274 275 qualify_outputs(query) 276 277 # Preserve the quoting information of columns for newly added Alias nodes 278 query_selects = query.selects 279 for select_index in unaliased_column_indexes: 280 alias = query_selects[select_index] 281 column = alias.this 282 if isinstance(column.this, exp.Identifier): 283 alias.args["alias"].set("quoted", column.this.quoted) 284 285 return expression
Ensures all (unnamed) output columns are aliased for CTEs and Subqueries.
388class TSQL(Dialect): 389 SUPPORTS_SEMI_ANTI_JOIN = False 390 LOG_BASE_FIRST = False 391 TYPED_DIVISION = True 392 CONCAT_COALESCE = True 393 NORMALIZATION_STRATEGY = NormalizationStrategy.CASE_INSENSITIVE 394 395 TIME_FORMAT = "'yyyy-mm-dd hh:mm:ss'" 396 397 TIME_MAPPING = { 398 "year": "%Y", 399 "dayofyear": "%j", 400 "day": "%d", 401 "dy": "%d", 402 "y": "%Y", 403 "week": "%W", 404 "ww": "%W", 405 "wk": "%W", 406 "hour": "%h", 407 "hh": "%I", 408 "minute": "%M", 409 "mi": "%M", 410 "n": "%M", 411 "second": "%S", 412 "ss": "%S", 413 "s": "%-S", 414 "millisecond": "%f", 415 "ms": "%f", 416 "weekday": "%w", 417 "dw": "%w", 418 "month": "%m", 419 "mm": "%M", 420 "m": "%-M", 421 "Y": "%Y", 422 "YYYY": "%Y", 423 "YY": "%y", 424 "MMMM": "%B", 425 "MMM": "%b", 426 "MM": "%m", 427 "M": "%-m", 428 "dddd": "%A", 429 "dd": "%d", 430 "d": "%-d", 431 "HH": "%H", 432 "H": "%-H", 433 "h": "%-I", 434 "ffffff": "%f", 435 "yyyy": "%Y", 436 "yy": "%y", 437 } 438 439 CONVERT_FORMAT_MAPPING = { 440 "0": "%b %d %Y %-I:%M%p", 441 "1": "%m/%d/%y", 442 "2": "%y.%m.%d", 443 "3": "%d/%m/%y", 444 "4": "%d.%m.%y", 445 "5": "%d-%m-%y", 446 "6": "%d %b %y", 447 "7": "%b %d, %y", 448 "8": "%H:%M:%S", 449 "9": "%b %d %Y %-I:%M:%S:%f%p", 450 "10": "mm-dd-yy", 451 "11": "yy/mm/dd", 452 "12": "yymmdd", 453 "13": "%d %b %Y %H:%M:ss:%f", 454 "14": "%H:%M:%S:%f", 455 "20": "%Y-%m-%d %H:%M:%S", 456 "21": "%Y-%m-%d %H:%M:%S.%f", 457 "22": "%m/%d/%y %-I:%M:%S %p", 458 "23": "%Y-%m-%d", 459 "24": "%H:%M:%S", 460 "25": "%Y-%m-%d %H:%M:%S.%f", 461 "100": "%b %d %Y %-I:%M%p", 462 "101": "%m/%d/%Y", 463 "102": "%Y.%m.%d", 464 "103": "%d/%m/%Y", 465 "104": "%d.%m.%Y", 466 "105": "%d-%m-%Y", 467 "106": "%d %b %Y", 468 "107": "%b %d, %Y", 469 "108": "%H:%M:%S", 470 "109": "%b %d %Y %-I:%M:%S:%f%p", 471 "110": "%m-%d-%Y", 472 "111": "%Y/%m/%d", 473 "112": "%Y%m%d", 474 "113": "%d %b %Y %H:%M:%S:%f", 475 "114": "%H:%M:%S:%f", 476 "120": "%Y-%m-%d %H:%M:%S", 477 "121": "%Y-%m-%d %H:%M:%S.%f", 478 } 479 480 FORMAT_TIME_MAPPING = { 481 "y": "%B %Y", 482 "d": "%m/%d/%Y", 483 "H": "%-H", 484 "h": "%-I", 485 "s": "%Y-%m-%d %H:%M:%S", 486 "D": "%A,%B,%Y", 487 "f": "%A,%B,%Y %-I:%M %p", 488 "F": "%A,%B,%Y %-I:%M:%S %p", 489 "g": "%m/%d/%Y %-I:%M %p", 490 "G": "%m/%d/%Y %-I:%M:%S %p", 491 "M": "%B %-d", 492 "m": "%B %-d", 493 "O": "%Y-%m-%dT%H:%M:%S", 494 "u": "%Y-%M-%D %H:%M:%S%z", 495 "U": "%A, %B %D, %Y %H:%M:%S%z", 496 "T": "%-I:%M:%S %p", 497 "t": "%-I:%M", 498 "Y": "%a %Y", 499 } 500 501 class Tokenizer(tokens.Tokenizer): 502 IDENTIFIERS = [("[", "]"), '"'] 503 QUOTES = ["'", '"'] 504 HEX_STRINGS = [("0x", ""), ("0X", "")] 505 VAR_SINGLE_TOKENS = {"@", "$", "#"} 506 507 KEYWORDS = { 508 **tokens.Tokenizer.KEYWORDS, 509 "CLUSTERED INDEX": TokenType.INDEX, 510 "DATETIME2": TokenType.DATETIME2, 511 "DATETIMEOFFSET": TokenType.TIMESTAMPTZ, 512 "DECLARE": TokenType.DECLARE, 513 "EXEC": TokenType.COMMAND, 514 "FOR SYSTEM_TIME": TokenType.TIMESTAMP_SNAPSHOT, 515 "GO": TokenType.COMMAND, 516 "IMAGE": TokenType.IMAGE, 517 "MONEY": TokenType.MONEY, 518 "NONCLUSTERED INDEX": TokenType.INDEX, 519 "NTEXT": TokenType.TEXT, 520 "OPTION": TokenType.OPTION, 521 "OUTPUT": TokenType.RETURNING, 522 "PRINT": TokenType.COMMAND, 523 "PROC": TokenType.PROCEDURE, 524 "REAL": TokenType.FLOAT, 525 "ROWVERSION": TokenType.ROWVERSION, 526 "SMALLDATETIME": TokenType.SMALLDATETIME, 527 "SMALLMONEY": TokenType.SMALLMONEY, 528 "SQL_VARIANT": TokenType.VARIANT, 529 "SYSTEM_USER": TokenType.CURRENT_USER, 530 "TOP": TokenType.TOP, 531 "TIMESTAMP": TokenType.ROWVERSION, 532 "TINYINT": TokenType.UTINYINT, 533 "UNIQUEIDENTIFIER": TokenType.UUID, 534 "UPDATE STATISTICS": TokenType.COMMAND, 535 "XML": TokenType.XML, 536 } 537 KEYWORDS.pop("/*+") 538 539 COMMANDS = {*tokens.Tokenizer.COMMANDS, TokenType.END} 540 541 class Parser(parser.Parser): 542 SET_REQUIRES_ASSIGNMENT_DELIMITER = False 543 LOG_DEFAULTS_TO_LN = True 544 ALTER_TABLE_ADD_REQUIRED_FOR_EACH_COLUMN = False 545 STRING_ALIASES = True 546 NO_PAREN_IF_COMMANDS = False 547 548 QUERY_MODIFIER_PARSERS = { 549 **parser.Parser.QUERY_MODIFIER_PARSERS, 550 TokenType.OPTION: lambda self: ("options", self._parse_options()), 551 } 552 553 # T-SQL does not allow BEGIN to be used as an identifier 554 ID_VAR_TOKENS = parser.Parser.ID_VAR_TOKENS - {TokenType.BEGIN} 555 ALIAS_TOKENS = parser.Parser.ALIAS_TOKENS - {TokenType.BEGIN} 556 TABLE_ALIAS_TOKENS = parser.Parser.TABLE_ALIAS_TOKENS - {TokenType.BEGIN} 557 COMMENT_TABLE_ALIAS_TOKENS = parser.Parser.COMMENT_TABLE_ALIAS_TOKENS - {TokenType.BEGIN} 558 UPDATE_ALIAS_TOKENS = parser.Parser.UPDATE_ALIAS_TOKENS - {TokenType.BEGIN} 559 560 FUNCTIONS = { 561 **parser.Parser.FUNCTIONS, 562 "CHARINDEX": lambda args: exp.StrPosition( 563 this=seq_get(args, 1), 564 substr=seq_get(args, 0), 565 position=seq_get(args, 2), 566 ), 567 "COUNT": lambda args: exp.Count( 568 this=seq_get(args, 0), expressions=args[1:], big_int=False 569 ), 570 "COUNT_BIG": lambda args: exp.Count( 571 this=seq_get(args, 0), expressions=args[1:], big_int=True 572 ), 573 "DATEADD": build_date_delta(exp.DateAdd, unit_mapping=DATE_DELTA_INTERVAL), 574 "DATEDIFF": _build_date_delta(exp.DateDiff, unit_mapping=DATE_DELTA_INTERVAL), 575 "DATENAME": _build_formatted_time(exp.TimeToStr, full_format_mapping=True), 576 "DATEPART": _build_formatted_time(exp.TimeToStr), 577 "DATETIMEFROMPARTS": _build_datetimefromparts, 578 "EOMONTH": _build_eomonth, 579 "FORMAT": _build_format, 580 "GETDATE": exp.CurrentTimestamp.from_arg_list, 581 "HASHBYTES": _build_hashbytes, 582 "ISNULL": lambda args: build_coalesce(args=args, is_null=True), 583 "JSON_QUERY": _build_json_query, 584 "JSON_VALUE": parser.build_extract_json_with_path(exp.JSONExtractScalar), 585 "LEN": _build_with_arg_as_text(exp.Length), 586 "LEFT": _build_with_arg_as_text(exp.Left), 587 "NEWID": exp.Uuid.from_arg_list, 588 "RIGHT": _build_with_arg_as_text(exp.Right), 589 "PARSENAME": _build_parsename, 590 "REPLICATE": exp.Repeat.from_arg_list, 591 "SCHEMA_NAME": exp.CurrentSchema.from_arg_list, 592 "SQUARE": lambda args: exp.Pow(this=seq_get(args, 0), expression=exp.Literal.number(2)), 593 "SYSDATETIME": exp.CurrentTimestamp.from_arg_list, 594 "SUSER_NAME": exp.CurrentUser.from_arg_list, 595 "SUSER_SNAME": exp.CurrentUser.from_arg_list, 596 "SYSTEM_USER": exp.CurrentUser.from_arg_list, 597 "TIMEFROMPARTS": _build_timefromparts, 598 "DATETRUNC": _build_datetrunc, 599 } 600 601 JOIN_HINTS = {"LOOP", "HASH", "MERGE", "REMOTE"} 602 603 PROCEDURE_OPTIONS = dict.fromkeys( 604 ("ENCRYPTION", "RECOMPILE", "SCHEMABINDING", "NATIVE_COMPILATION", "EXECUTE"), tuple() 605 ) 606 607 COLUMN_DEFINITION_MODES = {"OUT", "OUTPUT", "READ_ONLY"} 608 609 RETURNS_TABLE_TOKENS = parser.Parser.ID_VAR_TOKENS - { 610 TokenType.TABLE, 611 *parser.Parser.TYPE_TOKENS, 612 } 613 614 STATEMENT_PARSERS = { 615 **parser.Parser.STATEMENT_PARSERS, 616 TokenType.DECLARE: lambda self: self._parse_declare(), 617 } 618 619 RANGE_PARSERS = { 620 **parser.Parser.RANGE_PARSERS, 621 TokenType.DCOLON: lambda self, this: self.expression( 622 exp.ScopeResolution, 623 this=this, 624 expression=self._parse_function() or self._parse_var(any_token=True), 625 ), 626 } 627 628 NO_PAREN_FUNCTION_PARSERS = { 629 **parser.Parser.NO_PAREN_FUNCTION_PARSERS, 630 "NEXT": lambda self: self._parse_next_value_for(), 631 } 632 633 # The DCOLON (::) operator serves as a scope resolution (exp.ScopeResolution) operator in T-SQL 634 COLUMN_OPERATORS = { 635 **parser.Parser.COLUMN_OPERATORS, 636 TokenType.DCOLON: lambda self, this, to: self.expression(exp.Cast, this=this, to=to) 637 if isinstance(to, exp.DataType) and to.this != exp.DataType.Type.USERDEFINED 638 else self.expression(exp.ScopeResolution, this=this, expression=to), 639 } 640 641 def _parse_wrapped_select(self, table: bool = False) -> t.Optional[exp.Expression]: 642 if self._match(TokenType.MERGE): 643 comments = self._prev_comments 644 merge = self._parse_merge() 645 merge.add_comments(comments, prepend=True) 646 return merge 647 648 return super()._parse_wrapped_select(table=table) 649 650 def _parse_dcolon(self) -> t.Optional[exp.Expression]: 651 # We want to use _parse_types() if the first token after :: is a known type, 652 # otherwise we could parse something like x::varchar(max) into a function 653 if self._match_set(self.TYPE_TOKENS, advance=False): 654 return self._parse_types() 655 656 return self._parse_function() or self._parse_types() 657 658 def _parse_options(self) -> t.Optional[t.List[exp.Expression]]: 659 if not self._match(TokenType.OPTION): 660 return None 661 662 def _parse_option() -> t.Optional[exp.Expression]: 663 option = self._parse_var_from_options(OPTIONS) 664 if not option: 665 return None 666 667 self._match(TokenType.EQ) 668 return self.expression( 669 exp.QueryOption, this=option, expression=self._parse_primary_or_var() 670 ) 671 672 return self._parse_wrapped_csv(_parse_option) 673 674 def _parse_projections(self) -> t.List[exp.Expression]: 675 """ 676 T-SQL supports the syntax alias = expression in the SELECT's projection list, 677 so we transform all parsed Selects to convert their EQ projections into Aliases. 678 679 See: https://learn.microsoft.com/en-us/sql/t-sql/queries/select-clause-transact-sql?view=sql-server-ver16#syntax 680 """ 681 return [ 682 ( 683 exp.alias_(projection.expression, projection.this.this, copy=False) 684 if isinstance(projection, exp.EQ) and isinstance(projection.this, exp.Column) 685 else projection 686 ) 687 for projection in super()._parse_projections() 688 ] 689 690 def _parse_commit_or_rollback(self) -> exp.Commit | exp.Rollback: 691 """Applies to SQL Server and Azure SQL Database 692 COMMIT [ { TRAN | TRANSACTION } 693 [ transaction_name | @tran_name_variable ] ] 694 [ WITH ( DELAYED_DURABILITY = { OFF | ON } ) ] 695 696 ROLLBACK { TRAN | TRANSACTION } 697 [ transaction_name | @tran_name_variable 698 | savepoint_name | @savepoint_variable ] 699 """ 700 rollback = self._prev.token_type == TokenType.ROLLBACK 701 702 self._match_texts(("TRAN", "TRANSACTION")) 703 this = self._parse_id_var() 704 705 if rollback: 706 return self.expression(exp.Rollback, this=this) 707 708 durability = None 709 if self._match_pair(TokenType.WITH, TokenType.L_PAREN): 710 self._match_text_seq("DELAYED_DURABILITY") 711 self._match(TokenType.EQ) 712 713 if self._match_text_seq("OFF"): 714 durability = False 715 else: 716 self._match(TokenType.ON) 717 durability = True 718 719 self._match_r_paren() 720 721 return self.expression(exp.Commit, this=this, durability=durability) 722 723 def _parse_transaction(self) -> exp.Transaction | exp.Command: 724 """Applies to SQL Server and Azure SQL Database 725 BEGIN { TRAN | TRANSACTION } 726 [ { transaction_name | @tran_name_variable } 727 [ WITH MARK [ 'description' ] ] 728 ] 729 """ 730 if self._match_texts(("TRAN", "TRANSACTION")): 731 transaction = self.expression(exp.Transaction, this=self._parse_id_var()) 732 if self._match_text_seq("WITH", "MARK"): 733 transaction.set("mark", self._parse_string()) 734 735 return transaction 736 737 return self._parse_as_command(self._prev) 738 739 def _parse_returns(self) -> exp.ReturnsProperty: 740 table = self._parse_id_var(any_token=False, tokens=self.RETURNS_TABLE_TOKENS) 741 returns = super()._parse_returns() 742 returns.set("table", table) 743 return returns 744 745 def _parse_convert( 746 self, strict: bool, safe: t.Optional[bool] = None 747 ) -> t.Optional[exp.Expression]: 748 this = self._parse_types() 749 self._match(TokenType.COMMA) 750 args = [this, *self._parse_csv(self._parse_assignment)] 751 convert = exp.Convert.from_arg_list(args) 752 convert.set("safe", safe) 753 convert.set("strict", strict) 754 return convert 755 756 def _parse_column_def( 757 self, this: t.Optional[exp.Expression], computed_column: bool = True 758 ) -> t.Optional[exp.Expression]: 759 this = super()._parse_column_def(this=this, computed_column=computed_column) 760 if not this: 761 return None 762 if self._match(TokenType.EQ): 763 this.set("default", self._parse_disjunction()) 764 if self._match_texts(self.COLUMN_DEFINITION_MODES): 765 this.set("output", self._prev.text) 766 return this 767 768 def _parse_user_defined_function( 769 self, kind: t.Optional[TokenType] = None 770 ) -> t.Optional[exp.Expression]: 771 this = super()._parse_user_defined_function(kind=kind) 772 773 if ( 774 kind == TokenType.FUNCTION 775 or isinstance(this, exp.UserDefinedFunction) 776 or self._match(TokenType.ALIAS, advance=False) 777 ): 778 return this 779 780 if not self._match(TokenType.WITH, advance=False): 781 expressions = self._parse_csv(self._parse_function_parameter) 782 else: 783 expressions = None 784 785 return self.expression(exp.UserDefinedFunction, this=this, expressions=expressions) 786 787 def _parse_into(self) -> t.Optional[exp.Into]: 788 into = super()._parse_into() 789 790 table = isinstance(into, exp.Into) and into.find(exp.Table) 791 if isinstance(table, exp.Table): 792 table_identifier = table.this 793 if table_identifier.args.get("temporary"): 794 # Promote the temporary property from the Identifier to the Into expression 795 t.cast(exp.Into, into).set("temporary", True) 796 797 return into 798 799 def _parse_id_var( 800 self, 801 any_token: bool = True, 802 tokens: t.Optional[t.Collection[TokenType]] = None, 803 ) -> t.Optional[exp.Expression]: 804 is_temporary = self._match(TokenType.HASH) 805 is_global = is_temporary and self._match(TokenType.HASH) 806 807 this = super()._parse_id_var(any_token=any_token, tokens=tokens) 808 if this: 809 if is_global: 810 this.set("global", True) 811 elif is_temporary: 812 this.set("temporary", True) 813 814 return this 815 816 def _parse_create(self) -> exp.Create | exp.Command: 817 create = super()._parse_create() 818 819 if isinstance(create, exp.Create): 820 table = create.this.this if isinstance(create.this, exp.Schema) else create.this 821 if isinstance(table, exp.Table) and table.this and table.this.args.get("temporary"): 822 if not create.args.get("properties"): 823 create.set("properties", exp.Properties(expressions=[])) 824 825 create.args["properties"].append("expressions", exp.TemporaryProperty()) 826 827 return create 828 829 def _parse_if(self) -> t.Optional[exp.Expression]: 830 index = self._index 831 832 if self._match_text_seq("OBJECT_ID"): 833 self._parse_wrapped_csv(self._parse_string) 834 if self._match_text_seq("IS", "NOT", "NULL") and self._match(TokenType.DROP): 835 return self._parse_drop(exists=True) 836 self._retreat(index) 837 838 return super()._parse_if() 839 840 def _parse_unique(self) -> exp.UniqueColumnConstraint: 841 if self._match_texts(("CLUSTERED", "NONCLUSTERED")): 842 this = self.CONSTRAINT_PARSERS[self._prev.text.upper()](self) 843 else: 844 this = self._parse_schema(self._parse_id_var(any_token=False)) 845 846 return self.expression(exp.UniqueColumnConstraint, this=this) 847 848 def _parse_partition(self) -> t.Optional[exp.Partition]: 849 if not self._match_text_seq("WITH", "(", "PARTITIONS"): 850 return None 851 852 def parse_range(): 853 low = self._parse_bitwise() 854 high = self._parse_bitwise() if self._match_text_seq("TO") else None 855 856 return ( 857 self.expression(exp.PartitionRange, this=low, expression=high) if high else low 858 ) 859 860 partition = self.expression( 861 exp.Partition, expressions=self._parse_wrapped_csv(parse_range) 862 ) 863 864 self._match_r_paren() 865 866 return partition 867 868 def _parse_declare(self) -> exp.Declare | exp.Command: 869 index = self._index 870 expressions = self._try_parse(partial(self._parse_csv, self._parse_declareitem)) 871 872 if not expressions or self._curr: 873 self._retreat(index) 874 return self._parse_as_command(self._prev) 875 876 return self.expression(exp.Declare, expressions=expressions) 877 878 def _parse_declareitem(self) -> t.Optional[exp.DeclareItem]: 879 var = self._parse_id_var() 880 if not var: 881 return None 882 883 value = None 884 self._match(TokenType.ALIAS) 885 if self._match(TokenType.TABLE): 886 data_type = self._parse_schema() 887 else: 888 data_type = self._parse_types() 889 if self._match(TokenType.EQ): 890 value = self._parse_bitwise() 891 892 return self.expression(exp.DeclareItem, this=var, kind=data_type, default=value) 893 894 def _parse_alter_table_alter(self) -> t.Optional[exp.Expression]: 895 expression = super()._parse_alter_table_alter() 896 897 if expression is not None: 898 collation = expression.args.get("collate") 899 if isinstance(collation, exp.Column) and isinstance(collation.this, exp.Identifier): 900 identifier = collation.this 901 collation.set("this", exp.Var(this=identifier.name)) 902 903 return expression 904 905 class Generator(generator.Generator): 906 LIMIT_IS_TOP = True 907 QUERY_HINTS = False 908 RETURNING_END = False 909 NVL2_SUPPORTED = False 910 ALTER_TABLE_INCLUDE_COLUMN_KEYWORD = False 911 LIMIT_FETCH = "FETCH" 912 COMPUTED_COLUMN_WITH_TYPE = False 913 CTE_RECURSIVE_KEYWORD_REQUIRED = False 914 ENSURE_BOOLS = True 915 NULL_ORDERING_SUPPORTED = None 916 SUPPORTS_SINGLE_ARG_CONCAT = False 917 TABLESAMPLE_SEED_KEYWORD = "REPEATABLE" 918 SUPPORTS_SELECT_INTO = True 919 JSON_PATH_BRACKETED_KEY_SUPPORTED = False 920 SUPPORTS_TO_NUMBER = False 921 SET_OP_MODIFIERS = False 922 COPY_PARAMS_EQ_REQUIRED = True 923 PARSE_JSON_NAME = None 924 EXCEPT_INTERSECT_SUPPORT_ALL_CLAUSE = False 925 ALTER_SET_TYPE = "" 926 927 EXPRESSIONS_WITHOUT_NESTED_CTES = { 928 exp.Create, 929 exp.Delete, 930 exp.Insert, 931 exp.Intersect, 932 exp.Except, 933 exp.Merge, 934 exp.Select, 935 exp.Subquery, 936 exp.Union, 937 exp.Update, 938 } 939 940 SUPPORTED_JSON_PATH_PARTS = { 941 exp.JSONPathKey, 942 exp.JSONPathRoot, 943 exp.JSONPathSubscript, 944 } 945 946 TYPE_MAPPING = { 947 **generator.Generator.TYPE_MAPPING, 948 exp.DataType.Type.BOOLEAN: "BIT", 949 exp.DataType.Type.DATETIME2: "DATETIME2", 950 exp.DataType.Type.DECIMAL: "NUMERIC", 951 exp.DataType.Type.DOUBLE: "FLOAT", 952 exp.DataType.Type.INT: "INTEGER", 953 exp.DataType.Type.ROWVERSION: "ROWVERSION", 954 exp.DataType.Type.TEXT: "VARCHAR(MAX)", 955 exp.DataType.Type.TIMESTAMP: "DATETIME2", 956 exp.DataType.Type.TIMESTAMPNTZ: "DATETIME2", 957 exp.DataType.Type.TIMESTAMPTZ: "DATETIMEOFFSET", 958 exp.DataType.Type.SMALLDATETIME: "SMALLDATETIME", 959 exp.DataType.Type.UTINYINT: "TINYINT", 960 exp.DataType.Type.VARIANT: "SQL_VARIANT", 961 exp.DataType.Type.UUID: "UNIQUEIDENTIFIER", 962 } 963 964 TYPE_MAPPING.pop(exp.DataType.Type.NCHAR) 965 TYPE_MAPPING.pop(exp.DataType.Type.NVARCHAR) 966 967 TRANSFORMS = { 968 **generator.Generator.TRANSFORMS, 969 exp.AnyValue: any_value_to_max_sql, 970 exp.ArrayToString: rename_func("STRING_AGG"), 971 exp.AutoIncrementColumnConstraint: lambda *_: "IDENTITY", 972 exp.Chr: rename_func("CHAR"), 973 exp.DateAdd: date_delta_sql("DATEADD"), 974 exp.DateDiff: date_delta_sql("DATEDIFF"), 975 exp.CTE: transforms.preprocess([qualify_derived_table_outputs]), 976 exp.CurrentDate: rename_func("GETDATE"), 977 exp.CurrentTimestamp: rename_func("GETDATE"), 978 exp.DateStrToDate: datestrtodate_sql, 979 exp.Extract: rename_func("DATEPART"), 980 exp.GeneratedAsIdentityColumnConstraint: generatedasidentitycolumnconstraint_sql, 981 exp.GroupConcat: _string_agg_sql, 982 exp.If: rename_func("IIF"), 983 exp.JSONExtract: _json_extract_sql, 984 exp.JSONExtractScalar: _json_extract_sql, 985 exp.LastDay: lambda self, e: self.func("EOMONTH", e.this), 986 exp.Ln: rename_func("LOG"), 987 exp.Max: max_or_greatest, 988 exp.MD5: lambda self, e: self.func("HASHBYTES", exp.Literal.string("MD5"), e.this), 989 exp.Min: min_or_least, 990 exp.NumberToStr: _format_sql, 991 exp.Repeat: rename_func("REPLICATE"), 992 exp.CurrentSchema: rename_func("SCHEMA_NAME"), 993 exp.Select: transforms.preprocess( 994 [ 995 transforms.eliminate_distinct_on, 996 transforms.eliminate_semi_and_anti_joins, 997 transforms.eliminate_qualify, 998 transforms.unnest_generate_date_array_using_recursive_cte, 999 ] 1000 ), 1001 exp.Stddev: rename_func("STDEV"), 1002 exp.StrPosition: lambda self, e: strposition_sql( 1003 self, e, func_name="CHARINDEX", supports_position=True 1004 ), 1005 exp.Subquery: transforms.preprocess([qualify_derived_table_outputs]), 1006 exp.SHA: lambda self, e: self.func("HASHBYTES", exp.Literal.string("SHA1"), e.this), 1007 exp.SHA2: lambda self, e: self.func( 1008 "HASHBYTES", exp.Literal.string(f"SHA2_{e.args.get('length', 256)}"), e.this 1009 ), 1010 exp.TemporaryProperty: lambda self, e: "", 1011 exp.TimeStrToTime: _timestrtotime_sql, 1012 exp.TimeToStr: _format_sql, 1013 exp.Trim: trim_sql, 1014 exp.TsOrDsAdd: date_delta_sql("DATEADD", cast=True), 1015 exp.TsOrDsDiff: date_delta_sql("DATEDIFF"), 1016 exp.TimestampTrunc: lambda self, e: self.func("DATETRUNC", e.unit, e.this), 1017 exp.Uuid: lambda *_: "NEWID()", 1018 exp.DateFromParts: rename_func("DATEFROMPARTS"), 1019 } 1020 1021 TRANSFORMS.pop(exp.ReturnsProperty) 1022 1023 PROPERTIES_LOCATION = { 1024 **generator.Generator.PROPERTIES_LOCATION, 1025 exp.VolatileProperty: exp.Properties.Location.UNSUPPORTED, 1026 } 1027 1028 def scope_resolution(self, rhs: str, scope_name: str) -> str: 1029 return f"{scope_name}::{rhs}" 1030 1031 def select_sql(self, expression: exp.Select) -> str: 1032 limit = expression.args.get("limit") 1033 offset = expression.args.get("offset") 1034 1035 if isinstance(limit, exp.Fetch) and not offset: 1036 # Dialects like Oracle can FETCH directly from a row set but 1037 # T-SQL requires an ORDER BY + OFFSET clause in order to FETCH 1038 offset = exp.Offset(expression=exp.Literal.number(0)) 1039 expression.set("offset", offset) 1040 1041 if offset: 1042 if not expression.args.get("order"): 1043 # ORDER BY is required in order to use OFFSET in a query, so we use 1044 # a noop order by, since we don't really care about the order. 1045 # See: https://www.microsoftpressstore.com/articles/article.aspx?p=2314819 1046 expression.order_by(exp.select(exp.null()).subquery(), copy=False) 1047 1048 if isinstance(limit, exp.Limit): 1049 # TOP and OFFSET can't be combined, we need use FETCH instead of TOP 1050 # we replace here because otherwise TOP would be generated in select_sql 1051 limit.replace(exp.Fetch(direction="FIRST", count=limit.expression)) 1052 1053 return super().select_sql(expression) 1054 1055 def convert_sql(self, expression: exp.Convert) -> str: 1056 name = "TRY_CONVERT" if expression.args.get("safe") else "CONVERT" 1057 return self.func( 1058 name, expression.this, expression.expression, expression.args.get("style") 1059 ) 1060 1061 def queryoption_sql(self, expression: exp.QueryOption) -> str: 1062 option = self.sql(expression, "this") 1063 value = self.sql(expression, "expression") 1064 if value: 1065 optional_equal_sign = "= " if option in OPTIONS_THAT_REQUIRE_EQUAL else "" 1066 return f"{option} {optional_equal_sign}{value}" 1067 return option 1068 1069 def lateral_op(self, expression: exp.Lateral) -> str: 1070 cross_apply = expression.args.get("cross_apply") 1071 if cross_apply is True: 1072 return "CROSS APPLY" 1073 if cross_apply is False: 1074 return "OUTER APPLY" 1075 1076 # TODO: perhaps we can check if the parent is a Join and transpile it appropriately 1077 self.unsupported("LATERAL clause is not supported.") 1078 return "LATERAL" 1079 1080 def splitpart_sql(self: TSQL.Generator, expression: exp.SplitPart) -> str: 1081 this = expression.this 1082 split_count = len(this.name.split(".")) 1083 delimiter = expression.args.get("delimiter") 1084 part_index = expression.args.get("part_index") 1085 1086 if ( 1087 not all(isinstance(arg, exp.Literal) for arg in (this, delimiter, part_index)) 1088 or (delimiter and delimiter.name != ".") 1089 or not part_index 1090 or split_count > 4 1091 ): 1092 self.unsupported( 1093 "SPLIT_PART can be transpiled to PARSENAME only for '.' delimiter and literal values" 1094 ) 1095 return "" 1096 1097 return self.func( 1098 "PARSENAME", this, exp.Literal.number(split_count + 1 - part_index.to_py()) 1099 ) 1100 1101 def timefromparts_sql(self, expression: exp.TimeFromParts) -> str: 1102 nano = expression.args.get("nano") 1103 if nano is not None: 1104 nano.pop() 1105 self.unsupported("Specifying nanoseconds is not supported in TIMEFROMPARTS.") 1106 1107 if expression.args.get("fractions") is None: 1108 expression.set("fractions", exp.Literal.number(0)) 1109 if expression.args.get("precision") is None: 1110 expression.set("precision", exp.Literal.number(0)) 1111 1112 return rename_func("TIMEFROMPARTS")(self, expression) 1113 1114 def timestampfromparts_sql(self, expression: exp.TimestampFromParts) -> str: 1115 zone = expression.args.get("zone") 1116 if zone is not None: 1117 zone.pop() 1118 self.unsupported("Time zone is not supported in DATETIMEFROMPARTS.") 1119 1120 nano = expression.args.get("nano") 1121 if nano is not None: 1122 nano.pop() 1123 self.unsupported("Specifying nanoseconds is not supported in DATETIMEFROMPARTS.") 1124 1125 if expression.args.get("milli") is None: 1126 expression.set("milli", exp.Literal.number(0)) 1127 1128 return rename_func("DATETIMEFROMPARTS")(self, expression) 1129 1130 def setitem_sql(self, expression: exp.SetItem) -> str: 1131 this = expression.this 1132 if isinstance(this, exp.EQ) and not isinstance(this.left, exp.Parameter): 1133 # T-SQL does not use '=' in SET command, except when the LHS is a variable. 1134 return f"{self.sql(this.left)} {self.sql(this.right)}" 1135 1136 return super().setitem_sql(expression) 1137 1138 def boolean_sql(self, expression: exp.Boolean) -> str: 1139 if type(expression.parent) in BIT_TYPES or isinstance( 1140 expression.find_ancestor(exp.Values, exp.Select), exp.Values 1141 ): 1142 return "1" if expression.this else "0" 1143 1144 return "(1 = 1)" if expression.this else "(1 = 0)" 1145 1146 def is_sql(self, expression: exp.Is) -> str: 1147 if isinstance(expression.expression, exp.Boolean): 1148 return self.binary(expression, "=") 1149 return self.binary(expression, "IS") 1150 1151 def createable_sql(self, expression: exp.Create, locations: t.DefaultDict) -> str: 1152 sql = self.sql(expression, "this") 1153 properties = expression.args.get("properties") 1154 1155 if sql[:1] != "#" and any( 1156 isinstance(prop, exp.TemporaryProperty) 1157 for prop in (properties.expressions if properties else []) 1158 ): 1159 sql = f"[#{sql[1:]}" if sql.startswith("[") else f"#{sql}" 1160 1161 return sql 1162 1163 def create_sql(self, expression: exp.Create) -> str: 1164 kind = expression.kind 1165 exists = expression.args.pop("exists", None) 1166 1167 like_property = expression.find(exp.LikeProperty) 1168 if like_property: 1169 ctas_expression = like_property.this 1170 else: 1171 ctas_expression = expression.expression 1172 1173 if kind == "VIEW": 1174 expression.this.set("catalog", None) 1175 with_ = expression.args.get("with") 1176 if ctas_expression and with_: 1177 # We've already preprocessed the Create expression to bubble up any nested CTEs, 1178 # but CREATE VIEW actually requires the WITH clause to come after it so we need 1179 # to amend the AST by moving the CTEs to the CREATE VIEW statement's query. 1180 ctas_expression.set("with", with_.pop()) 1181 1182 sql = super().create_sql(expression) 1183 1184 table = expression.find(exp.Table) 1185 1186 # Convert CTAS statement to SELECT .. INTO .. 1187 if kind == "TABLE" and ctas_expression: 1188 if isinstance(ctas_expression, exp.UNWRAPPED_QUERIES): 1189 ctas_expression = ctas_expression.subquery() 1190 1191 properties = expression.args.get("properties") or exp.Properties() 1192 is_temp = any(isinstance(p, exp.TemporaryProperty) for p in properties.expressions) 1193 1194 select_into = exp.select("*").from_(exp.alias_(ctas_expression, "temp", table=True)) 1195 select_into.set("into", exp.Into(this=table, temporary=is_temp)) 1196 1197 if like_property: 1198 select_into.limit(0, copy=False) 1199 1200 sql = self.sql(select_into) 1201 1202 if exists: 1203 identifier = self.sql(exp.Literal.string(exp.table_name(table) if table else "")) 1204 sql_with_ctes = self.prepend_ctes(expression, sql) 1205 sql_literal = self.sql(exp.Literal.string(sql_with_ctes)) 1206 if kind == "SCHEMA": 1207 return f"""IF NOT EXISTS (SELECT * FROM information_schema.schemata WHERE schema_name = {identifier}) EXEC({sql_literal})""" 1208 elif kind == "TABLE": 1209 assert table 1210 where = exp.and_( 1211 exp.column("table_name").eq(table.name), 1212 exp.column("table_schema").eq(table.db) if table.db else None, 1213 exp.column("table_catalog").eq(table.catalog) if table.catalog else None, 1214 ) 1215 return f"""IF NOT EXISTS (SELECT * FROM information_schema.tables WHERE {where}) EXEC({sql_literal})""" 1216 elif kind == "INDEX": 1217 index = self.sql(exp.Literal.string(expression.this.text("this"))) 1218 return f"""IF NOT EXISTS (SELECT * FROM sys.indexes WHERE object_id = object_id({identifier}) AND name = {index}) EXEC({sql_literal})""" 1219 elif expression.args.get("replace"): 1220 sql = sql.replace("CREATE OR REPLACE ", "CREATE OR ALTER ", 1) 1221 1222 return self.prepend_ctes(expression, sql) 1223 1224 @generator.unsupported_args("unlogged", "expressions") 1225 def into_sql(self, expression: exp.Into) -> str: 1226 if expression.args.get("temporary"): 1227 # If the Into expression has a temporary property, push this down to the Identifier 1228 table = expression.find(exp.Table) 1229 if table and isinstance(table.this, exp.Identifier): 1230 table.this.set("temporary", True) 1231 1232 return f"{self.seg('INTO')} {self.sql(expression, 'this')}" 1233 1234 def count_sql(self, expression: exp.Count) -> str: 1235 func_name = "COUNT_BIG" if expression.args.get("big_int") else "COUNT" 1236 return rename_func(func_name)(self, expression) 1237 1238 def offset_sql(self, expression: exp.Offset) -> str: 1239 return f"{super().offset_sql(expression)} ROWS" 1240 1241 def version_sql(self, expression: exp.Version) -> str: 1242 name = "SYSTEM_TIME" if expression.name == "TIMESTAMP" else expression.name 1243 this = f"FOR {name}" 1244 expr = expression.expression 1245 kind = expression.text("kind") 1246 if kind in ("FROM", "BETWEEN"): 1247 args = expr.expressions 1248 sep = "TO" if kind == "FROM" else "AND" 1249 expr_sql = f"{self.sql(seq_get(args, 0))} {sep} {self.sql(seq_get(args, 1))}" 1250 else: 1251 expr_sql = self.sql(expr) 1252 1253 expr_sql = f" {expr_sql}" if expr_sql else "" 1254 return f"{this} {kind}{expr_sql}" 1255 1256 def returnsproperty_sql(self, expression: exp.ReturnsProperty) -> str: 1257 table = expression.args.get("table") 1258 table = f"{table} " if table else "" 1259 return f"RETURNS {table}{self.sql(expression, 'this')}" 1260 1261 def returning_sql(self, expression: exp.Returning) -> str: 1262 into = self.sql(expression, "into") 1263 into = self.seg(f"INTO {into}") if into else "" 1264 return f"{self.seg('OUTPUT')} {self.expressions(expression, flat=True)}{into}" 1265 1266 def transaction_sql(self, expression: exp.Transaction) -> str: 1267 this = self.sql(expression, "this") 1268 this = f" {this}" if this else "" 1269 mark = self.sql(expression, "mark") 1270 mark = f" WITH MARK {mark}" if mark else "" 1271 return f"BEGIN TRANSACTION{this}{mark}" 1272 1273 def commit_sql(self, expression: exp.Commit) -> str: 1274 this = self.sql(expression, "this") 1275 this = f" {this}" if this else "" 1276 durability = expression.args.get("durability") 1277 durability = ( 1278 f" WITH (DELAYED_DURABILITY = {'ON' if durability else 'OFF'})" 1279 if durability is not None 1280 else "" 1281 ) 1282 return f"COMMIT TRANSACTION{this}{durability}" 1283 1284 def rollback_sql(self, expression: exp.Rollback) -> str: 1285 this = self.sql(expression, "this") 1286 this = f" {this}" if this else "" 1287 return f"ROLLBACK TRANSACTION{this}" 1288 1289 def identifier_sql(self, expression: exp.Identifier) -> str: 1290 identifier = super().identifier_sql(expression) 1291 1292 if expression.args.get("global"): 1293 identifier = f"##{identifier}" 1294 elif expression.args.get("temporary"): 1295 identifier = f"#{identifier}" 1296 1297 return identifier 1298 1299 def constraint_sql(self, expression: exp.Constraint) -> str: 1300 this = self.sql(expression, "this") 1301 expressions = self.expressions(expression, flat=True, sep=" ") 1302 return f"CONSTRAINT {this} {expressions}" 1303 1304 def length_sql(self, expression: exp.Length) -> str: 1305 return self._uncast_text(expression, "LEN") 1306 1307 def right_sql(self, expression: exp.Right) -> str: 1308 return self._uncast_text(expression, "RIGHT") 1309 1310 def left_sql(self, expression: exp.Left) -> str: 1311 return self._uncast_text(expression, "LEFT") 1312 1313 def _uncast_text(self, expression: exp.Expression, name: str) -> str: 1314 this = expression.this 1315 if isinstance(this, exp.Cast) and this.is_type(exp.DataType.Type.TEXT): 1316 this_sql = self.sql(this, "this") 1317 else: 1318 this_sql = self.sql(this) 1319 expression_sql = self.sql(expression, "expression") 1320 return self.func(name, this_sql, expression_sql if expression_sql else None) 1321 1322 def partition_sql(self, expression: exp.Partition) -> str: 1323 return f"WITH (PARTITIONS({self.expressions(expression, flat=True)}))" 1324 1325 def alter_sql(self, expression: exp.Alter) -> str: 1326 action = seq_get(expression.args.get("actions") or [], 0) 1327 if isinstance(action, exp.AlterRename): 1328 return f"EXEC sp_rename '{self.sql(expression.this)}', '{action.this.name}'" 1329 return super().alter_sql(expression) 1330 1331 def drop_sql(self, expression: exp.Drop) -> str: 1332 if expression.args["kind"] == "VIEW": 1333 expression.this.set("catalog", None) 1334 return super().drop_sql(expression) 1335 1336 def options_modifier(self, expression: exp.Expression) -> str: 1337 options = self.expressions(expression, key="options") 1338 return f" OPTION{self.wrap(options)}" if options else "" 1339 1340 def dpipe_sql(self, expression: exp.DPipe) -> str: 1341 return self.sql( 1342 reduce(lambda x, y: exp.Add(this=x, expression=y), expression.flatten()) 1343 ) 1344 1345 def isascii_sql(self, expression: exp.IsAscii) -> str: 1346 return f"(PATINDEX(CONVERT(VARCHAR(MAX), 0x255b5e002d7f5d25) COLLATE Latin1_General_BIN, {self.sql(expression.this)}) = 0)" 1347 1348 def columndef_sql(self, expression: exp.ColumnDef, sep: str = " ") -> str: 1349 this = super().columndef_sql(expression, sep) 1350 default = self.sql(expression, "default") 1351 default = f" = {default}" if default else "" 1352 output = self.sql(expression, "output") 1353 output = f" {output}" if output else "" 1354 return f"{this}{default}{output}" 1355 1356 def coalesce_sql(self, expression: exp.Coalesce) -> str: 1357 func_name = "ISNULL" if expression.args.get("is_null") else "COALESCE" 1358 return rename_func(func_name)(self, expression)
Whether the base comes first in the LOG
function.
Possible values: True
, False
, None
(two arguments are not supported by LOG
)
Whether the behavior of a / b
depends on the types of a
and b
.
False means a / b
is always float division.
True means a / b
is integer division if both a
and b
are integers.
A NULL
arg in CONCAT
yields NULL
by default, but in some dialects it yields an empty string.
Specifies the strategy according to which identifiers should be normalized.
Associates this dialect's time formats with their equivalent Python strftime
formats.
501 class Tokenizer(tokens.Tokenizer): 502 IDENTIFIERS = [("[", "]"), '"'] 503 QUOTES = ["'", '"'] 504 HEX_STRINGS = [("0x", ""), ("0X", "")] 505 VAR_SINGLE_TOKENS = {"@", "$", "#"} 506 507 KEYWORDS = { 508 **tokens.Tokenizer.KEYWORDS, 509 "CLUSTERED INDEX": TokenType.INDEX, 510 "DATETIME2": TokenType.DATETIME2, 511 "DATETIMEOFFSET": TokenType.TIMESTAMPTZ, 512 "DECLARE": TokenType.DECLARE, 513 "EXEC": TokenType.COMMAND, 514 "FOR SYSTEM_TIME": TokenType.TIMESTAMP_SNAPSHOT, 515 "GO": TokenType.COMMAND, 516 "IMAGE": TokenType.IMAGE, 517 "MONEY": TokenType.MONEY, 518 "NONCLUSTERED INDEX": TokenType.INDEX, 519 "NTEXT": TokenType.TEXT, 520 "OPTION": TokenType.OPTION, 521 "OUTPUT": TokenType.RETURNING, 522 "PRINT": TokenType.COMMAND, 523 "PROC": TokenType.PROCEDURE, 524 "REAL": TokenType.FLOAT, 525 "ROWVERSION": TokenType.ROWVERSION, 526 "SMALLDATETIME": TokenType.SMALLDATETIME, 527 "SMALLMONEY": TokenType.SMALLMONEY, 528 "SQL_VARIANT": TokenType.VARIANT, 529 "SYSTEM_USER": TokenType.CURRENT_USER, 530 "TOP": TokenType.TOP, 531 "TIMESTAMP": TokenType.ROWVERSION, 532 "TINYINT": TokenType.UTINYINT, 533 "UNIQUEIDENTIFIER": TokenType.UUID, 534 "UPDATE STATISTICS": TokenType.COMMAND, 535 "XML": TokenType.XML, 536 } 537 KEYWORDS.pop("/*+") 538 539 COMMANDS = {*tokens.Tokenizer.COMMANDS, TokenType.END}
Inherited Members
- sqlglot.tokens.Tokenizer
- Tokenizer
- SINGLE_TOKENS
- BIT_STRINGS
- BYTE_STRINGS
- RAW_STRINGS
- HEREDOC_STRINGS
- UNICODE_STRINGS
- STRING_ESCAPES
- IDENTIFIER_ESCAPES
- HEREDOC_TAG_IS_IDENTIFIER
- HEREDOC_STRING_ALTERNATIVE
- STRING_ESCAPES_ALLOWED_IN_RAW_STRINGS
- NESTED_COMMENTS
- HINT_START
- TOKENS_PRECEDING_HINT
- WHITE_SPACE
- COMMAND_PREFIX_TOKENS
- NUMERIC_LITERALS
- COMMENTS
- dialect
- use_rs_tokenizer
- reset
- tokenize
- tokenize_rs
- size
- sql
- tokens
541 class Parser(parser.Parser): 542 SET_REQUIRES_ASSIGNMENT_DELIMITER = False 543 LOG_DEFAULTS_TO_LN = True 544 ALTER_TABLE_ADD_REQUIRED_FOR_EACH_COLUMN = False 545 STRING_ALIASES = True 546 NO_PAREN_IF_COMMANDS = False 547 548 QUERY_MODIFIER_PARSERS = { 549 **parser.Parser.QUERY_MODIFIER_PARSERS, 550 TokenType.OPTION: lambda self: ("options", self._parse_options()), 551 } 552 553 # T-SQL does not allow BEGIN to be used as an identifier 554 ID_VAR_TOKENS = parser.Parser.ID_VAR_TOKENS - {TokenType.BEGIN} 555 ALIAS_TOKENS = parser.Parser.ALIAS_TOKENS - {TokenType.BEGIN} 556 TABLE_ALIAS_TOKENS = parser.Parser.TABLE_ALIAS_TOKENS - {TokenType.BEGIN} 557 COMMENT_TABLE_ALIAS_TOKENS = parser.Parser.COMMENT_TABLE_ALIAS_TOKENS - {TokenType.BEGIN} 558 UPDATE_ALIAS_TOKENS = parser.Parser.UPDATE_ALIAS_TOKENS - {TokenType.BEGIN} 559 560 FUNCTIONS = { 561 **parser.Parser.FUNCTIONS, 562 "CHARINDEX": lambda args: exp.StrPosition( 563 this=seq_get(args, 1), 564 substr=seq_get(args, 0), 565 position=seq_get(args, 2), 566 ), 567 "COUNT": lambda args: exp.Count( 568 this=seq_get(args, 0), expressions=args[1:], big_int=False 569 ), 570 "COUNT_BIG": lambda args: exp.Count( 571 this=seq_get(args, 0), expressions=args[1:], big_int=True 572 ), 573 "DATEADD": build_date_delta(exp.DateAdd, unit_mapping=DATE_DELTA_INTERVAL), 574 "DATEDIFF": _build_date_delta(exp.DateDiff, unit_mapping=DATE_DELTA_INTERVAL), 575 "DATENAME": _build_formatted_time(exp.TimeToStr, full_format_mapping=True), 576 "DATEPART": _build_formatted_time(exp.TimeToStr), 577 "DATETIMEFROMPARTS": _build_datetimefromparts, 578 "EOMONTH": _build_eomonth, 579 "FORMAT": _build_format, 580 "GETDATE": exp.CurrentTimestamp.from_arg_list, 581 "HASHBYTES": _build_hashbytes, 582 "ISNULL": lambda args: build_coalesce(args=args, is_null=True), 583 "JSON_QUERY": _build_json_query, 584 "JSON_VALUE": parser.build_extract_json_with_path(exp.JSONExtractScalar), 585 "LEN": _build_with_arg_as_text(exp.Length), 586 "LEFT": _build_with_arg_as_text(exp.Left), 587 "NEWID": exp.Uuid.from_arg_list, 588 "RIGHT": _build_with_arg_as_text(exp.Right), 589 "PARSENAME": _build_parsename, 590 "REPLICATE": exp.Repeat.from_arg_list, 591 "SCHEMA_NAME": exp.CurrentSchema.from_arg_list, 592 "SQUARE": lambda args: exp.Pow(this=seq_get(args, 0), expression=exp.Literal.number(2)), 593 "SYSDATETIME": exp.CurrentTimestamp.from_arg_list, 594 "SUSER_NAME": exp.CurrentUser.from_arg_list, 595 "SUSER_SNAME": exp.CurrentUser.from_arg_list, 596 "SYSTEM_USER": exp.CurrentUser.from_arg_list, 597 "TIMEFROMPARTS": _build_timefromparts, 598 "DATETRUNC": _build_datetrunc, 599 } 600 601 JOIN_HINTS = {"LOOP", "HASH", "MERGE", "REMOTE"} 602 603 PROCEDURE_OPTIONS = dict.fromkeys( 604 ("ENCRYPTION", "RECOMPILE", "SCHEMABINDING", "NATIVE_COMPILATION", "EXECUTE"), tuple() 605 ) 606 607 COLUMN_DEFINITION_MODES = {"OUT", "OUTPUT", "READ_ONLY"} 608 609 RETURNS_TABLE_TOKENS = parser.Parser.ID_VAR_TOKENS - { 610 TokenType.TABLE, 611 *parser.Parser.TYPE_TOKENS, 612 } 613 614 STATEMENT_PARSERS = { 615 **parser.Parser.STATEMENT_PARSERS, 616 TokenType.DECLARE: lambda self: self._parse_declare(), 617 } 618 619 RANGE_PARSERS = { 620 **parser.Parser.RANGE_PARSERS, 621 TokenType.DCOLON: lambda self, this: self.expression( 622 exp.ScopeResolution, 623 this=this, 624 expression=self._parse_function() or self._parse_var(any_token=True), 625 ), 626 } 627 628 NO_PAREN_FUNCTION_PARSERS = { 629 **parser.Parser.NO_PAREN_FUNCTION_PARSERS, 630 "NEXT": lambda self: self._parse_next_value_for(), 631 } 632 633 # The DCOLON (::) operator serves as a scope resolution (exp.ScopeResolution) operator in T-SQL 634 COLUMN_OPERATORS = { 635 **parser.Parser.COLUMN_OPERATORS, 636 TokenType.DCOLON: lambda self, this, to: self.expression(exp.Cast, this=this, to=to) 637 if isinstance(to, exp.DataType) and to.this != exp.DataType.Type.USERDEFINED 638 else self.expression(exp.ScopeResolution, this=this, expression=to), 639 } 640 641 def _parse_wrapped_select(self, table: bool = False) -> t.Optional[exp.Expression]: 642 if self._match(TokenType.MERGE): 643 comments = self._prev_comments 644 merge = self._parse_merge() 645 merge.add_comments(comments, prepend=True) 646 return merge 647 648 return super()._parse_wrapped_select(table=table) 649 650 def _parse_dcolon(self) -> t.Optional[exp.Expression]: 651 # We want to use _parse_types() if the first token after :: is a known type, 652 # otherwise we could parse something like x::varchar(max) into a function 653 if self._match_set(self.TYPE_TOKENS, advance=False): 654 return self._parse_types() 655 656 return self._parse_function() or self._parse_types() 657 658 def _parse_options(self) -> t.Optional[t.List[exp.Expression]]: 659 if not self._match(TokenType.OPTION): 660 return None 661 662 def _parse_option() -> t.Optional[exp.Expression]: 663 option = self._parse_var_from_options(OPTIONS) 664 if not option: 665 return None 666 667 self._match(TokenType.EQ) 668 return self.expression( 669 exp.QueryOption, this=option, expression=self._parse_primary_or_var() 670 ) 671 672 return self._parse_wrapped_csv(_parse_option) 673 674 def _parse_projections(self) -> t.List[exp.Expression]: 675 """ 676 T-SQL supports the syntax alias = expression in the SELECT's projection list, 677 so we transform all parsed Selects to convert their EQ projections into Aliases. 678 679 See: https://learn.microsoft.com/en-us/sql/t-sql/queries/select-clause-transact-sql?view=sql-server-ver16#syntax 680 """ 681 return [ 682 ( 683 exp.alias_(projection.expression, projection.this.this, copy=False) 684 if isinstance(projection, exp.EQ) and isinstance(projection.this, exp.Column) 685 else projection 686 ) 687 for projection in super()._parse_projections() 688 ] 689 690 def _parse_commit_or_rollback(self) -> exp.Commit | exp.Rollback: 691 """Applies to SQL Server and Azure SQL Database 692 COMMIT [ { TRAN | TRANSACTION } 693 [ transaction_name | @tran_name_variable ] ] 694 [ WITH ( DELAYED_DURABILITY = { OFF | ON } ) ] 695 696 ROLLBACK { TRAN | TRANSACTION } 697 [ transaction_name | @tran_name_variable 698 | savepoint_name | @savepoint_variable ] 699 """ 700 rollback = self._prev.token_type == TokenType.ROLLBACK 701 702 self._match_texts(("TRAN", "TRANSACTION")) 703 this = self._parse_id_var() 704 705 if rollback: 706 return self.expression(exp.Rollback, this=this) 707 708 durability = None 709 if self._match_pair(TokenType.WITH, TokenType.L_PAREN): 710 self._match_text_seq("DELAYED_DURABILITY") 711 self._match(TokenType.EQ) 712 713 if self._match_text_seq("OFF"): 714 durability = False 715 else: 716 self._match(TokenType.ON) 717 durability = True 718 719 self._match_r_paren() 720 721 return self.expression(exp.Commit, this=this, durability=durability) 722 723 def _parse_transaction(self) -> exp.Transaction | exp.Command: 724 """Applies to SQL Server and Azure SQL Database 725 BEGIN { TRAN | TRANSACTION } 726 [ { transaction_name | @tran_name_variable } 727 [ WITH MARK [ 'description' ] ] 728 ] 729 """ 730 if self._match_texts(("TRAN", "TRANSACTION")): 731 transaction = self.expression(exp.Transaction, this=self._parse_id_var()) 732 if self._match_text_seq("WITH", "MARK"): 733 transaction.set("mark", self._parse_string()) 734 735 return transaction 736 737 return self._parse_as_command(self._prev) 738 739 def _parse_returns(self) -> exp.ReturnsProperty: 740 table = self._parse_id_var(any_token=False, tokens=self.RETURNS_TABLE_TOKENS) 741 returns = super()._parse_returns() 742 returns.set("table", table) 743 return returns 744 745 def _parse_convert( 746 self, strict: bool, safe: t.Optional[bool] = None 747 ) -> t.Optional[exp.Expression]: 748 this = self._parse_types() 749 self._match(TokenType.COMMA) 750 args = [this, *self._parse_csv(self._parse_assignment)] 751 convert = exp.Convert.from_arg_list(args) 752 convert.set("safe", safe) 753 convert.set("strict", strict) 754 return convert 755 756 def _parse_column_def( 757 self, this: t.Optional[exp.Expression], computed_column: bool = True 758 ) -> t.Optional[exp.Expression]: 759 this = super()._parse_column_def(this=this, computed_column=computed_column) 760 if not this: 761 return None 762 if self._match(TokenType.EQ): 763 this.set("default", self._parse_disjunction()) 764 if self._match_texts(self.COLUMN_DEFINITION_MODES): 765 this.set("output", self._prev.text) 766 return this 767 768 def _parse_user_defined_function( 769 self, kind: t.Optional[TokenType] = None 770 ) -> t.Optional[exp.Expression]: 771 this = super()._parse_user_defined_function(kind=kind) 772 773 if ( 774 kind == TokenType.FUNCTION 775 or isinstance(this, exp.UserDefinedFunction) 776 or self._match(TokenType.ALIAS, advance=False) 777 ): 778 return this 779 780 if not self._match(TokenType.WITH, advance=False): 781 expressions = self._parse_csv(self._parse_function_parameter) 782 else: 783 expressions = None 784 785 return self.expression(exp.UserDefinedFunction, this=this, expressions=expressions) 786 787 def _parse_into(self) -> t.Optional[exp.Into]: 788 into = super()._parse_into() 789 790 table = isinstance(into, exp.Into) and into.find(exp.Table) 791 if isinstance(table, exp.Table): 792 table_identifier = table.this 793 if table_identifier.args.get("temporary"): 794 # Promote the temporary property from the Identifier to the Into expression 795 t.cast(exp.Into, into).set("temporary", True) 796 797 return into 798 799 def _parse_id_var( 800 self, 801 any_token: bool = True, 802 tokens: t.Optional[t.Collection[TokenType]] = None, 803 ) -> t.Optional[exp.Expression]: 804 is_temporary = self._match(TokenType.HASH) 805 is_global = is_temporary and self._match(TokenType.HASH) 806 807 this = super()._parse_id_var(any_token=any_token, tokens=tokens) 808 if this: 809 if is_global: 810 this.set("global", True) 811 elif is_temporary: 812 this.set("temporary", True) 813 814 return this 815 816 def _parse_create(self) -> exp.Create | exp.Command: 817 create = super()._parse_create() 818 819 if isinstance(create, exp.Create): 820 table = create.this.this if isinstance(create.this, exp.Schema) else create.this 821 if isinstance(table, exp.Table) and table.this and table.this.args.get("temporary"): 822 if not create.args.get("properties"): 823 create.set("properties", exp.Properties(expressions=[])) 824 825 create.args["properties"].append("expressions", exp.TemporaryProperty()) 826 827 return create 828 829 def _parse_if(self) -> t.Optional[exp.Expression]: 830 index = self._index 831 832 if self._match_text_seq("OBJECT_ID"): 833 self._parse_wrapped_csv(self._parse_string) 834 if self._match_text_seq("IS", "NOT", "NULL") and self._match(TokenType.DROP): 835 return self._parse_drop(exists=True) 836 self._retreat(index) 837 838 return super()._parse_if() 839 840 def _parse_unique(self) -> exp.UniqueColumnConstraint: 841 if self._match_texts(("CLUSTERED", "NONCLUSTERED")): 842 this = self.CONSTRAINT_PARSERS[self._prev.text.upper()](self) 843 else: 844 this = self._parse_schema(self._parse_id_var(any_token=False)) 845 846 return self.expression(exp.UniqueColumnConstraint, this=this) 847 848 def _parse_partition(self) -> t.Optional[exp.Partition]: 849 if not self._match_text_seq("WITH", "(", "PARTITIONS"): 850 return None 851 852 def parse_range(): 853 low = self._parse_bitwise() 854 high = self._parse_bitwise() if self._match_text_seq("TO") else None 855 856 return ( 857 self.expression(exp.PartitionRange, this=low, expression=high) if high else low 858 ) 859 860 partition = self.expression( 861 exp.Partition, expressions=self._parse_wrapped_csv(parse_range) 862 ) 863 864 self._match_r_paren() 865 866 return partition 867 868 def _parse_declare(self) -> exp.Declare | exp.Command: 869 index = self._index 870 expressions = self._try_parse(partial(self._parse_csv, self._parse_declareitem)) 871 872 if not expressions or self._curr: 873 self._retreat(index) 874 return self._parse_as_command(self._prev) 875 876 return self.expression(exp.Declare, expressions=expressions) 877 878 def _parse_declareitem(self) -> t.Optional[exp.DeclareItem]: 879 var = self._parse_id_var() 880 if not var: 881 return None 882 883 value = None 884 self._match(TokenType.ALIAS) 885 if self._match(TokenType.TABLE): 886 data_type = self._parse_schema() 887 else: 888 data_type = self._parse_types() 889 if self._match(TokenType.EQ): 890 value = self._parse_bitwise() 891 892 return self.expression(exp.DeclareItem, this=var, kind=data_type, default=value) 893 894 def _parse_alter_table_alter(self) -> t.Optional[exp.Expression]: 895 expression = super()._parse_alter_table_alter() 896 897 if expression is not None: 898 collation = expression.args.get("collate") 899 if isinstance(collation, exp.Column) and isinstance(collation.this, exp.Identifier): 900 identifier = collation.this 901 collation.set("this", exp.Var(this=identifier.name)) 902 903 return expression
Parser consumes a list of tokens produced by the Tokenizer and produces a parsed syntax tree.
Arguments:
- error_level: The desired error level. Default: ErrorLevel.IMMEDIATE
- error_message_context: The amount of context to capture from a query string when displaying the error message (in number of characters). Default: 100
- max_errors: Maximum number of error messages to include in a raised ParseError. This is only relevant if error_level is ErrorLevel.RAISE. Default: 3
Inherited Members
- sqlglot.parser.Parser
- Parser
- NO_PAREN_FUNCTIONS
- STRUCT_TYPE_TOKENS
- NESTED_TYPE_TOKENS
- ENUM_TYPE_TOKENS
- AGGREGATE_TYPE_TOKENS
- TYPE_TOKENS
- SIGNED_TO_UNSIGNED_TYPE_TOKEN
- SUBQUERY_PREDICATES
- RESERVED_TOKENS
- DB_CREATABLES
- CREATABLES
- ALTERABLES
- COLON_PLACEHOLDER_TOKENS
- ARRAY_CONSTRUCTORS
- TRIM_TYPES
- FUNC_TOKENS
- CONJUNCTION
- ASSIGNMENT
- DISJUNCTION
- EQUALITY
- COMPARISON
- BITWISE
- TERM
- FACTOR
- EXPONENT
- TIMES
- TIMESTAMPS
- SET_OPERATIONS
- JOIN_METHODS
- JOIN_SIDES
- JOIN_KINDS
- LAMBDAS
- EXPRESSION_PARSERS
- UNARY_PARSERS
- STRING_PARSERS
- NUMERIC_PARSERS
- PRIMARY_PARSERS
- PLACEHOLDER_PARSERS
- PROPERTY_PARSERS
- CONSTRAINT_PARSERS
- ALTER_PARSERS
- ALTER_ALTER_PARSERS
- SCHEMA_UNNAMED_CONSTRAINTS
- INVALID_FUNC_NAME_TOKENS
- FUNCTIONS_WITH_ALIASED_ARGS
- KEY_VALUE_DEFINITIONS
- FUNCTION_PARSERS
- SET_PARSERS
- SHOW_PARSERS
- TYPE_LITERAL_PARSERS
- TYPE_CONVERTERS
- DDL_SELECT_TOKENS
- PRE_VOLATILE_TOKENS
- TRANSACTION_KIND
- TRANSACTION_CHARACTERISTICS
- CONFLICT_ACTIONS
- CREATE_SEQUENCE
- ISOLATED_LOADING_OPTIONS
- USABLES
- CAST_ACTIONS
- SCHEMA_BINDING_OPTIONS
- EXECUTE_AS_OPTIONS
- KEY_CONSTRAINT_OPTIONS
- WINDOW_EXCLUDE_OPTIONS
- INSERT_ALTERNATIVES
- CLONE_KEYWORDS
- HISTORICAL_DATA_PREFIX
- HISTORICAL_DATA_KIND
- OPCLASS_FOLLOW_KEYWORDS
- OPTYPE_FOLLOW_TOKENS
- TABLE_INDEX_HINT_TOKENS
- VIEW_ATTRIBUTES
- WINDOW_ALIAS_TOKENS
- WINDOW_BEFORE_PAREN_TOKENS
- WINDOW_SIDES
- JSON_KEY_VALUE_SEPARATOR_TOKENS
- FETCH_TOKENS
- ADD_CONSTRAINT_TOKENS
- DISTINCT_TOKENS
- NULL_TOKENS
- UNNEST_OFFSET_ALIAS_TOKENS
- SELECT_START_TOKENS
- COPY_INTO_VARLEN_OPTIONS
- IS_JSON_PREDICATE_KIND
- ODBC_DATETIME_LITERALS
- ON_CONDITION_TOKENS
- PRIVILEGE_FOLLOW_TOKENS
- DESCRIBE_STYLES
- ANALYZE_STYLES
- ANALYZE_EXPRESSION_PARSERS
- PARTITION_KEYWORDS
- AMBIGUOUS_ALIAS_TOKENS
- OPERATION_MODIFIERS
- RECURSIVE_CTE_SEARCH_KIND
- MODIFIABLES
- STRICT_CAST
- PREFIXED_PIVOT_COLUMNS
- IDENTIFY_PIVOT_STRINGS
- TABLESAMPLE_CSV
- DEFAULT_SAMPLING_METHOD
- TRIM_PATTERN_FIRST
- MODIFIERS_ATTACHED_TO_SET_OP
- SET_OP_MODIFIERS
- JSON_ARROWS_REQUIRE_JSON_TYPE
- COLON_IS_VARIANT_EXTRACT
- VALUES_FOLLOWED_BY_PAREN
- SUPPORTS_IMPLICIT_UNNEST
- INTERVAL_SPANS
- SUPPORTS_PARTITION_SELECTION
- WRAPPED_TRANSFORM_COLUMN_CONSTRAINT
- OPTIONAL_ALIAS_TOKEN_CTE
- error_level
- error_message_context
- max_errors
- dialect
- reset
- parse
- parse_into
- check_errors
- raise_error
- expression
- validate_expression
- parse_set_operation
- errors
- sql
905 class Generator(generator.Generator): 906 LIMIT_IS_TOP = True 907 QUERY_HINTS = False 908 RETURNING_END = False 909 NVL2_SUPPORTED = False 910 ALTER_TABLE_INCLUDE_COLUMN_KEYWORD = False 911 LIMIT_FETCH = "FETCH" 912 COMPUTED_COLUMN_WITH_TYPE = False 913 CTE_RECURSIVE_KEYWORD_REQUIRED = False 914 ENSURE_BOOLS = True 915 NULL_ORDERING_SUPPORTED = None 916 SUPPORTS_SINGLE_ARG_CONCAT = False 917 TABLESAMPLE_SEED_KEYWORD = "REPEATABLE" 918 SUPPORTS_SELECT_INTO = True 919 JSON_PATH_BRACKETED_KEY_SUPPORTED = False 920 SUPPORTS_TO_NUMBER = False 921 SET_OP_MODIFIERS = False 922 COPY_PARAMS_EQ_REQUIRED = True 923 PARSE_JSON_NAME = None 924 EXCEPT_INTERSECT_SUPPORT_ALL_CLAUSE = False 925 ALTER_SET_TYPE = "" 926 927 EXPRESSIONS_WITHOUT_NESTED_CTES = { 928 exp.Create, 929 exp.Delete, 930 exp.Insert, 931 exp.Intersect, 932 exp.Except, 933 exp.Merge, 934 exp.Select, 935 exp.Subquery, 936 exp.Union, 937 exp.Update, 938 } 939 940 SUPPORTED_JSON_PATH_PARTS = { 941 exp.JSONPathKey, 942 exp.JSONPathRoot, 943 exp.JSONPathSubscript, 944 } 945 946 TYPE_MAPPING = { 947 **generator.Generator.TYPE_MAPPING, 948 exp.DataType.Type.BOOLEAN: "BIT", 949 exp.DataType.Type.DATETIME2: "DATETIME2", 950 exp.DataType.Type.DECIMAL: "NUMERIC", 951 exp.DataType.Type.DOUBLE: "FLOAT", 952 exp.DataType.Type.INT: "INTEGER", 953 exp.DataType.Type.ROWVERSION: "ROWVERSION", 954 exp.DataType.Type.TEXT: "VARCHAR(MAX)", 955 exp.DataType.Type.TIMESTAMP: "DATETIME2", 956 exp.DataType.Type.TIMESTAMPNTZ: "DATETIME2", 957 exp.DataType.Type.TIMESTAMPTZ: "DATETIMEOFFSET", 958 exp.DataType.Type.SMALLDATETIME: "SMALLDATETIME", 959 exp.DataType.Type.UTINYINT: "TINYINT", 960 exp.DataType.Type.VARIANT: "SQL_VARIANT", 961 exp.DataType.Type.UUID: "UNIQUEIDENTIFIER", 962 } 963 964 TYPE_MAPPING.pop(exp.DataType.Type.NCHAR) 965 TYPE_MAPPING.pop(exp.DataType.Type.NVARCHAR) 966 967 TRANSFORMS = { 968 **generator.Generator.TRANSFORMS, 969 exp.AnyValue: any_value_to_max_sql, 970 exp.ArrayToString: rename_func("STRING_AGG"), 971 exp.AutoIncrementColumnConstraint: lambda *_: "IDENTITY", 972 exp.Chr: rename_func("CHAR"), 973 exp.DateAdd: date_delta_sql("DATEADD"), 974 exp.DateDiff: date_delta_sql("DATEDIFF"), 975 exp.CTE: transforms.preprocess([qualify_derived_table_outputs]), 976 exp.CurrentDate: rename_func("GETDATE"), 977 exp.CurrentTimestamp: rename_func("GETDATE"), 978 exp.DateStrToDate: datestrtodate_sql, 979 exp.Extract: rename_func("DATEPART"), 980 exp.GeneratedAsIdentityColumnConstraint: generatedasidentitycolumnconstraint_sql, 981 exp.GroupConcat: _string_agg_sql, 982 exp.If: rename_func("IIF"), 983 exp.JSONExtract: _json_extract_sql, 984 exp.JSONExtractScalar: _json_extract_sql, 985 exp.LastDay: lambda self, e: self.func("EOMONTH", e.this), 986 exp.Ln: rename_func("LOG"), 987 exp.Max: max_or_greatest, 988 exp.MD5: lambda self, e: self.func("HASHBYTES", exp.Literal.string("MD5"), e.this), 989 exp.Min: min_or_least, 990 exp.NumberToStr: _format_sql, 991 exp.Repeat: rename_func("REPLICATE"), 992 exp.CurrentSchema: rename_func("SCHEMA_NAME"), 993 exp.Select: transforms.preprocess( 994 [ 995 transforms.eliminate_distinct_on, 996 transforms.eliminate_semi_and_anti_joins, 997 transforms.eliminate_qualify, 998 transforms.unnest_generate_date_array_using_recursive_cte, 999 ] 1000 ), 1001 exp.Stddev: rename_func("STDEV"), 1002 exp.StrPosition: lambda self, e: strposition_sql( 1003 self, e, func_name="CHARINDEX", supports_position=True 1004 ), 1005 exp.Subquery: transforms.preprocess([qualify_derived_table_outputs]), 1006 exp.SHA: lambda self, e: self.func("HASHBYTES", exp.Literal.string("SHA1"), e.this), 1007 exp.SHA2: lambda self, e: self.func( 1008 "HASHBYTES", exp.Literal.string(f"SHA2_{e.args.get('length', 256)}"), e.this 1009 ), 1010 exp.TemporaryProperty: lambda self, e: "", 1011 exp.TimeStrToTime: _timestrtotime_sql, 1012 exp.TimeToStr: _format_sql, 1013 exp.Trim: trim_sql, 1014 exp.TsOrDsAdd: date_delta_sql("DATEADD", cast=True), 1015 exp.TsOrDsDiff: date_delta_sql("DATEDIFF"), 1016 exp.TimestampTrunc: lambda self, e: self.func("DATETRUNC", e.unit, e.this), 1017 exp.Uuid: lambda *_: "NEWID()", 1018 exp.DateFromParts: rename_func("DATEFROMPARTS"), 1019 } 1020 1021 TRANSFORMS.pop(exp.ReturnsProperty) 1022 1023 PROPERTIES_LOCATION = { 1024 **generator.Generator.PROPERTIES_LOCATION, 1025 exp.VolatileProperty: exp.Properties.Location.UNSUPPORTED, 1026 } 1027 1028 def scope_resolution(self, rhs: str, scope_name: str) -> str: 1029 return f"{scope_name}::{rhs}" 1030 1031 def select_sql(self, expression: exp.Select) -> str: 1032 limit = expression.args.get("limit") 1033 offset = expression.args.get("offset") 1034 1035 if isinstance(limit, exp.Fetch) and not offset: 1036 # Dialects like Oracle can FETCH directly from a row set but 1037 # T-SQL requires an ORDER BY + OFFSET clause in order to FETCH 1038 offset = exp.Offset(expression=exp.Literal.number(0)) 1039 expression.set("offset", offset) 1040 1041 if offset: 1042 if not expression.args.get("order"): 1043 # ORDER BY is required in order to use OFFSET in a query, so we use 1044 # a noop order by, since we don't really care about the order. 1045 # See: https://www.microsoftpressstore.com/articles/article.aspx?p=2314819 1046 expression.order_by(exp.select(exp.null()).subquery(), copy=False) 1047 1048 if isinstance(limit, exp.Limit): 1049 # TOP and OFFSET can't be combined, we need use FETCH instead of TOP 1050 # we replace here because otherwise TOP would be generated in select_sql 1051 limit.replace(exp.Fetch(direction="FIRST", count=limit.expression)) 1052 1053 return super().select_sql(expression) 1054 1055 def convert_sql(self, expression: exp.Convert) -> str: 1056 name = "TRY_CONVERT" if expression.args.get("safe") else "CONVERT" 1057 return self.func( 1058 name, expression.this, expression.expression, expression.args.get("style") 1059 ) 1060 1061 def queryoption_sql(self, expression: exp.QueryOption) -> str: 1062 option = self.sql(expression, "this") 1063 value = self.sql(expression, "expression") 1064 if value: 1065 optional_equal_sign = "= " if option in OPTIONS_THAT_REQUIRE_EQUAL else "" 1066 return f"{option} {optional_equal_sign}{value}" 1067 return option 1068 1069 def lateral_op(self, expression: exp.Lateral) -> str: 1070 cross_apply = expression.args.get("cross_apply") 1071 if cross_apply is True: 1072 return "CROSS APPLY" 1073 if cross_apply is False: 1074 return "OUTER APPLY" 1075 1076 # TODO: perhaps we can check if the parent is a Join and transpile it appropriately 1077 self.unsupported("LATERAL clause is not supported.") 1078 return "LATERAL" 1079 1080 def splitpart_sql(self: TSQL.Generator, expression: exp.SplitPart) -> str: 1081 this = expression.this 1082 split_count = len(this.name.split(".")) 1083 delimiter = expression.args.get("delimiter") 1084 part_index = expression.args.get("part_index") 1085 1086 if ( 1087 not all(isinstance(arg, exp.Literal) for arg in (this, delimiter, part_index)) 1088 or (delimiter and delimiter.name != ".") 1089 or not part_index 1090 or split_count > 4 1091 ): 1092 self.unsupported( 1093 "SPLIT_PART can be transpiled to PARSENAME only for '.' delimiter and literal values" 1094 ) 1095 return "" 1096 1097 return self.func( 1098 "PARSENAME", this, exp.Literal.number(split_count + 1 - part_index.to_py()) 1099 ) 1100 1101 def timefromparts_sql(self, expression: exp.TimeFromParts) -> str: 1102 nano = expression.args.get("nano") 1103 if nano is not None: 1104 nano.pop() 1105 self.unsupported("Specifying nanoseconds is not supported in TIMEFROMPARTS.") 1106 1107 if expression.args.get("fractions") is None: 1108 expression.set("fractions", exp.Literal.number(0)) 1109 if expression.args.get("precision") is None: 1110 expression.set("precision", exp.Literal.number(0)) 1111 1112 return rename_func("TIMEFROMPARTS")(self, expression) 1113 1114 def timestampfromparts_sql(self, expression: exp.TimestampFromParts) -> str: 1115 zone = expression.args.get("zone") 1116 if zone is not None: 1117 zone.pop() 1118 self.unsupported("Time zone is not supported in DATETIMEFROMPARTS.") 1119 1120 nano = expression.args.get("nano") 1121 if nano is not None: 1122 nano.pop() 1123 self.unsupported("Specifying nanoseconds is not supported in DATETIMEFROMPARTS.") 1124 1125 if expression.args.get("milli") is None: 1126 expression.set("milli", exp.Literal.number(0)) 1127 1128 return rename_func("DATETIMEFROMPARTS")(self, expression) 1129 1130 def setitem_sql(self, expression: exp.SetItem) -> str: 1131 this = expression.this 1132 if isinstance(this, exp.EQ) and not isinstance(this.left, exp.Parameter): 1133 # T-SQL does not use '=' in SET command, except when the LHS is a variable. 1134 return f"{self.sql(this.left)} {self.sql(this.right)}" 1135 1136 return super().setitem_sql(expression) 1137 1138 def boolean_sql(self, expression: exp.Boolean) -> str: 1139 if type(expression.parent) in BIT_TYPES or isinstance( 1140 expression.find_ancestor(exp.Values, exp.Select), exp.Values 1141 ): 1142 return "1" if expression.this else "0" 1143 1144 return "(1 = 1)" if expression.this else "(1 = 0)" 1145 1146 def is_sql(self, expression: exp.Is) -> str: 1147 if isinstance(expression.expression, exp.Boolean): 1148 return self.binary(expression, "=") 1149 return self.binary(expression, "IS") 1150 1151 def createable_sql(self, expression: exp.Create, locations: t.DefaultDict) -> str: 1152 sql = self.sql(expression, "this") 1153 properties = expression.args.get("properties") 1154 1155 if sql[:1] != "#" and any( 1156 isinstance(prop, exp.TemporaryProperty) 1157 for prop in (properties.expressions if properties else []) 1158 ): 1159 sql = f"[#{sql[1:]}" if sql.startswith("[") else f"#{sql}" 1160 1161 return sql 1162 1163 def create_sql(self, expression: exp.Create) -> str: 1164 kind = expression.kind 1165 exists = expression.args.pop("exists", None) 1166 1167 like_property = expression.find(exp.LikeProperty) 1168 if like_property: 1169 ctas_expression = like_property.this 1170 else: 1171 ctas_expression = expression.expression 1172 1173 if kind == "VIEW": 1174 expression.this.set("catalog", None) 1175 with_ = expression.args.get("with") 1176 if ctas_expression and with_: 1177 # We've already preprocessed the Create expression to bubble up any nested CTEs, 1178 # but CREATE VIEW actually requires the WITH clause to come after it so we need 1179 # to amend the AST by moving the CTEs to the CREATE VIEW statement's query. 1180 ctas_expression.set("with", with_.pop()) 1181 1182 sql = super().create_sql(expression) 1183 1184 table = expression.find(exp.Table) 1185 1186 # Convert CTAS statement to SELECT .. INTO .. 1187 if kind == "TABLE" and ctas_expression: 1188 if isinstance(ctas_expression, exp.UNWRAPPED_QUERIES): 1189 ctas_expression = ctas_expression.subquery() 1190 1191 properties = expression.args.get("properties") or exp.Properties() 1192 is_temp = any(isinstance(p, exp.TemporaryProperty) for p in properties.expressions) 1193 1194 select_into = exp.select("*").from_(exp.alias_(ctas_expression, "temp", table=True)) 1195 select_into.set("into", exp.Into(this=table, temporary=is_temp)) 1196 1197 if like_property: 1198 select_into.limit(0, copy=False) 1199 1200 sql = self.sql(select_into) 1201 1202 if exists: 1203 identifier = self.sql(exp.Literal.string(exp.table_name(table) if table else "")) 1204 sql_with_ctes = self.prepend_ctes(expression, sql) 1205 sql_literal = self.sql(exp.Literal.string(sql_with_ctes)) 1206 if kind == "SCHEMA": 1207 return f"""IF NOT EXISTS (SELECT * FROM information_schema.schemata WHERE schema_name = {identifier}) EXEC({sql_literal})""" 1208 elif kind == "TABLE": 1209 assert table 1210 where = exp.and_( 1211 exp.column("table_name").eq(table.name), 1212 exp.column("table_schema").eq(table.db) if table.db else None, 1213 exp.column("table_catalog").eq(table.catalog) if table.catalog else None, 1214 ) 1215 return f"""IF NOT EXISTS (SELECT * FROM information_schema.tables WHERE {where}) EXEC({sql_literal})""" 1216 elif kind == "INDEX": 1217 index = self.sql(exp.Literal.string(expression.this.text("this"))) 1218 return f"""IF NOT EXISTS (SELECT * FROM sys.indexes WHERE object_id = object_id({identifier}) AND name = {index}) EXEC({sql_literal})""" 1219 elif expression.args.get("replace"): 1220 sql = sql.replace("CREATE OR REPLACE ", "CREATE OR ALTER ", 1) 1221 1222 return self.prepend_ctes(expression, sql) 1223 1224 @generator.unsupported_args("unlogged", "expressions") 1225 def into_sql(self, expression: exp.Into) -> str: 1226 if expression.args.get("temporary"): 1227 # If the Into expression has a temporary property, push this down to the Identifier 1228 table = expression.find(exp.Table) 1229 if table and isinstance(table.this, exp.Identifier): 1230 table.this.set("temporary", True) 1231 1232 return f"{self.seg('INTO')} {self.sql(expression, 'this')}" 1233 1234 def count_sql(self, expression: exp.Count) -> str: 1235 func_name = "COUNT_BIG" if expression.args.get("big_int") else "COUNT" 1236 return rename_func(func_name)(self, expression) 1237 1238 def offset_sql(self, expression: exp.Offset) -> str: 1239 return f"{super().offset_sql(expression)} ROWS" 1240 1241 def version_sql(self, expression: exp.Version) -> str: 1242 name = "SYSTEM_TIME" if expression.name == "TIMESTAMP" else expression.name 1243 this = f"FOR {name}" 1244 expr = expression.expression 1245 kind = expression.text("kind") 1246 if kind in ("FROM", "BETWEEN"): 1247 args = expr.expressions 1248 sep = "TO" if kind == "FROM" else "AND" 1249 expr_sql = f"{self.sql(seq_get(args, 0))} {sep} {self.sql(seq_get(args, 1))}" 1250 else: 1251 expr_sql = self.sql(expr) 1252 1253 expr_sql = f" {expr_sql}" if expr_sql else "" 1254 return f"{this} {kind}{expr_sql}" 1255 1256 def returnsproperty_sql(self, expression: exp.ReturnsProperty) -> str: 1257 table = expression.args.get("table") 1258 table = f"{table} " if table else "" 1259 return f"RETURNS {table}{self.sql(expression, 'this')}" 1260 1261 def returning_sql(self, expression: exp.Returning) -> str: 1262 into = self.sql(expression, "into") 1263 into = self.seg(f"INTO {into}") if into else "" 1264 return f"{self.seg('OUTPUT')} {self.expressions(expression, flat=True)}{into}" 1265 1266 def transaction_sql(self, expression: exp.Transaction) -> str: 1267 this = self.sql(expression, "this") 1268 this = f" {this}" if this else "" 1269 mark = self.sql(expression, "mark") 1270 mark = f" WITH MARK {mark}" if mark else "" 1271 return f"BEGIN TRANSACTION{this}{mark}" 1272 1273 def commit_sql(self, expression: exp.Commit) -> str: 1274 this = self.sql(expression, "this") 1275 this = f" {this}" if this else "" 1276 durability = expression.args.get("durability") 1277 durability = ( 1278 f" WITH (DELAYED_DURABILITY = {'ON' if durability else 'OFF'})" 1279 if durability is not None 1280 else "" 1281 ) 1282 return f"COMMIT TRANSACTION{this}{durability}" 1283 1284 def rollback_sql(self, expression: exp.Rollback) -> str: 1285 this = self.sql(expression, "this") 1286 this = f" {this}" if this else "" 1287 return f"ROLLBACK TRANSACTION{this}" 1288 1289 def identifier_sql(self, expression: exp.Identifier) -> str: 1290 identifier = super().identifier_sql(expression) 1291 1292 if expression.args.get("global"): 1293 identifier = f"##{identifier}" 1294 elif expression.args.get("temporary"): 1295 identifier = f"#{identifier}" 1296 1297 return identifier 1298 1299 def constraint_sql(self, expression: exp.Constraint) -> str: 1300 this = self.sql(expression, "this") 1301 expressions = self.expressions(expression, flat=True, sep=" ") 1302 return f"CONSTRAINT {this} {expressions}" 1303 1304 def length_sql(self, expression: exp.Length) -> str: 1305 return self._uncast_text(expression, "LEN") 1306 1307 def right_sql(self, expression: exp.Right) -> str: 1308 return self._uncast_text(expression, "RIGHT") 1309 1310 def left_sql(self, expression: exp.Left) -> str: 1311 return self._uncast_text(expression, "LEFT") 1312 1313 def _uncast_text(self, expression: exp.Expression, name: str) -> str: 1314 this = expression.this 1315 if isinstance(this, exp.Cast) and this.is_type(exp.DataType.Type.TEXT): 1316 this_sql = self.sql(this, "this") 1317 else: 1318 this_sql = self.sql(this) 1319 expression_sql = self.sql(expression, "expression") 1320 return self.func(name, this_sql, expression_sql if expression_sql else None) 1321 1322 def partition_sql(self, expression: exp.Partition) -> str: 1323 return f"WITH (PARTITIONS({self.expressions(expression, flat=True)}))" 1324 1325 def alter_sql(self, expression: exp.Alter) -> str: 1326 action = seq_get(expression.args.get("actions") or [], 0) 1327 if isinstance(action, exp.AlterRename): 1328 return f"EXEC sp_rename '{self.sql(expression.this)}', '{action.this.name}'" 1329 return super().alter_sql(expression) 1330 1331 def drop_sql(self, expression: exp.Drop) -> str: 1332 if expression.args["kind"] == "VIEW": 1333 expression.this.set("catalog", None) 1334 return super().drop_sql(expression) 1335 1336 def options_modifier(self, expression: exp.Expression) -> str: 1337 options = self.expressions(expression, key="options") 1338 return f" OPTION{self.wrap(options)}" if options else "" 1339 1340 def dpipe_sql(self, expression: exp.DPipe) -> str: 1341 return self.sql( 1342 reduce(lambda x, y: exp.Add(this=x, expression=y), expression.flatten()) 1343 ) 1344 1345 def isascii_sql(self, expression: exp.IsAscii) -> str: 1346 return f"(PATINDEX(CONVERT(VARCHAR(MAX), 0x255b5e002d7f5d25) COLLATE Latin1_General_BIN, {self.sql(expression.this)}) = 0)" 1347 1348 def columndef_sql(self, expression: exp.ColumnDef, sep: str = " ") -> str: 1349 this = super().columndef_sql(expression, sep) 1350 default = self.sql(expression, "default") 1351 default = f" = {default}" if default else "" 1352 output = self.sql(expression, "output") 1353 output = f" {output}" if output else "" 1354 return f"{this}{default}{output}" 1355 1356 def coalesce_sql(self, expression: exp.Coalesce) -> str: 1357 func_name = "ISNULL" if expression.args.get("is_null") else "COALESCE" 1358 return rename_func(func_name)(self, expression)
Generator converts a given syntax tree to the corresponding SQL string.
Arguments:
- pretty: Whether to format the produced SQL string. Default: False.
- identify: Determines when an identifier should be quoted. Possible values are: False (default): Never quote, except in cases where it's mandatory by the dialect. True or 'always': Always quote. 'safe': Only quote identifiers that are case insensitive.
- normalize: Whether to normalize identifiers to lowercase. Default: False.
- pad: The pad size in a formatted string. For example, this affects the indentation of a projection in a query, relative to its nesting level. Default: 2.
- indent: The indentation size in a formatted string. For example, this affects the
indentation of subqueries and filters under a
WHERE
clause. Default: 2. - normalize_functions: How to normalize function names. Possible values are: "upper" or True (default): Convert names to uppercase. "lower": Convert names to lowercase. False: Disables function name normalization.
- unsupported_level: Determines the generator's behavior when it encounters unsupported expressions. Default ErrorLevel.WARN.
- max_unsupported: Maximum number of unsupported messages to include in a raised UnsupportedError. This is only relevant if unsupported_level is ErrorLevel.RAISE. Default: 3
- leading_comma: Whether the comma is leading or trailing in select expressions. This is only relevant when generating in pretty mode. Default: False
- max_text_width: The max number of characters in a segment before creating new lines in pretty mode. The default is on the smaller end because the length only represents a segment and not the true line length. Default: 80
- comments: Whether to preserve comments in the output SQL code. Default: True
1031 def select_sql(self, expression: exp.Select) -> str: 1032 limit = expression.args.get("limit") 1033 offset = expression.args.get("offset") 1034 1035 if isinstance(limit, exp.Fetch) and not offset: 1036 # Dialects like Oracle can FETCH directly from a row set but 1037 # T-SQL requires an ORDER BY + OFFSET clause in order to FETCH 1038 offset = exp.Offset(expression=exp.Literal.number(0)) 1039 expression.set("offset", offset) 1040 1041 if offset: 1042 if not expression.args.get("order"): 1043 # ORDER BY is required in order to use OFFSET in a query, so we use 1044 # a noop order by, since we don't really care about the order. 1045 # See: https://www.microsoftpressstore.com/articles/article.aspx?p=2314819 1046 expression.order_by(exp.select(exp.null()).subquery(), copy=False) 1047 1048 if isinstance(limit, exp.Limit): 1049 # TOP and OFFSET can't be combined, we need use FETCH instead of TOP 1050 # we replace here because otherwise TOP would be generated in select_sql 1051 limit.replace(exp.Fetch(direction="FIRST", count=limit.expression)) 1052 1053 return super().select_sql(expression)
1061 def queryoption_sql(self, expression: exp.QueryOption) -> str: 1062 option = self.sql(expression, "this") 1063 value = self.sql(expression, "expression") 1064 if value: 1065 optional_equal_sign = "= " if option in OPTIONS_THAT_REQUIRE_EQUAL else "" 1066 return f"{option} {optional_equal_sign}{value}" 1067 return option
1069 def lateral_op(self, expression: exp.Lateral) -> str: 1070 cross_apply = expression.args.get("cross_apply") 1071 if cross_apply is True: 1072 return "CROSS APPLY" 1073 if cross_apply is False: 1074 return "OUTER APPLY" 1075 1076 # TODO: perhaps we can check if the parent is a Join and transpile it appropriately 1077 self.unsupported("LATERAL clause is not supported.") 1078 return "LATERAL"
1080 def splitpart_sql(self: TSQL.Generator, expression: exp.SplitPart) -> str: 1081 this = expression.this 1082 split_count = len(this.name.split(".")) 1083 delimiter = expression.args.get("delimiter") 1084 part_index = expression.args.get("part_index") 1085 1086 if ( 1087 not all(isinstance(arg, exp.Literal) for arg in (this, delimiter, part_index)) 1088 or (delimiter and delimiter.name != ".") 1089 or not part_index 1090 or split_count > 4 1091 ): 1092 self.unsupported( 1093 "SPLIT_PART can be transpiled to PARSENAME only for '.' delimiter and literal values" 1094 ) 1095 return "" 1096 1097 return self.func( 1098 "PARSENAME", this, exp.Literal.number(split_count + 1 - part_index.to_py()) 1099 )
1101 def timefromparts_sql(self, expression: exp.TimeFromParts) -> str: 1102 nano = expression.args.get("nano") 1103 if nano is not None: 1104 nano.pop() 1105 self.unsupported("Specifying nanoseconds is not supported in TIMEFROMPARTS.") 1106 1107 if expression.args.get("fractions") is None: 1108 expression.set("fractions", exp.Literal.number(0)) 1109 if expression.args.get("precision") is None: 1110 expression.set("precision", exp.Literal.number(0)) 1111 1112 return rename_func("TIMEFROMPARTS")(self, expression)
1114 def timestampfromparts_sql(self, expression: exp.TimestampFromParts) -> str: 1115 zone = expression.args.get("zone") 1116 if zone is not None: 1117 zone.pop() 1118 self.unsupported("Time zone is not supported in DATETIMEFROMPARTS.") 1119 1120 nano = expression.args.get("nano") 1121 if nano is not None: 1122 nano.pop() 1123 self.unsupported("Specifying nanoseconds is not supported in DATETIMEFROMPARTS.") 1124 1125 if expression.args.get("milli") is None: 1126 expression.set("milli", exp.Literal.number(0)) 1127 1128 return rename_func("DATETIMEFROMPARTS")(self, expression)
1130 def setitem_sql(self, expression: exp.SetItem) -> str: 1131 this = expression.this 1132 if isinstance(this, exp.EQ) and not isinstance(this.left, exp.Parameter): 1133 # T-SQL does not use '=' in SET command, except when the LHS is a variable. 1134 return f"{self.sql(this.left)} {self.sql(this.right)}" 1135 1136 return super().setitem_sql(expression)
1151 def createable_sql(self, expression: exp.Create, locations: t.DefaultDict) -> str: 1152 sql = self.sql(expression, "this") 1153 properties = expression.args.get("properties") 1154 1155 if sql[:1] != "#" and any( 1156 isinstance(prop, exp.TemporaryProperty) 1157 for prop in (properties.expressions if properties else []) 1158 ): 1159 sql = f"[#{sql[1:]}" if sql.startswith("[") else f"#{sql}" 1160 1161 return sql
1163 def create_sql(self, expression: exp.Create) -> str: 1164 kind = expression.kind 1165 exists = expression.args.pop("exists", None) 1166 1167 like_property = expression.find(exp.LikeProperty) 1168 if like_property: 1169 ctas_expression = like_property.this 1170 else: 1171 ctas_expression = expression.expression 1172 1173 if kind == "VIEW": 1174 expression.this.set("catalog", None) 1175 with_ = expression.args.get("with") 1176 if ctas_expression and with_: 1177 # We've already preprocessed the Create expression to bubble up any nested CTEs, 1178 # but CREATE VIEW actually requires the WITH clause to come after it so we need 1179 # to amend the AST by moving the CTEs to the CREATE VIEW statement's query. 1180 ctas_expression.set("with", with_.pop()) 1181 1182 sql = super().create_sql(expression) 1183 1184 table = expression.find(exp.Table) 1185 1186 # Convert CTAS statement to SELECT .. INTO .. 1187 if kind == "TABLE" and ctas_expression: 1188 if isinstance(ctas_expression, exp.UNWRAPPED_QUERIES): 1189 ctas_expression = ctas_expression.subquery() 1190 1191 properties = expression.args.get("properties") or exp.Properties() 1192 is_temp = any(isinstance(p, exp.TemporaryProperty) for p in properties.expressions) 1193 1194 select_into = exp.select("*").from_(exp.alias_(ctas_expression, "temp", table=True)) 1195 select_into.set("into", exp.Into(this=table, temporary=is_temp)) 1196 1197 if like_property: 1198 select_into.limit(0, copy=False) 1199 1200 sql = self.sql(select_into) 1201 1202 if exists: 1203 identifier = self.sql(exp.Literal.string(exp.table_name(table) if table else "")) 1204 sql_with_ctes = self.prepend_ctes(expression, sql) 1205 sql_literal = self.sql(exp.Literal.string(sql_with_ctes)) 1206 if kind == "SCHEMA": 1207 return f"""IF NOT EXISTS (SELECT * FROM information_schema.schemata WHERE schema_name = {identifier}) EXEC({sql_literal})""" 1208 elif kind == "TABLE": 1209 assert table 1210 where = exp.and_( 1211 exp.column("table_name").eq(table.name), 1212 exp.column("table_schema").eq(table.db) if table.db else None, 1213 exp.column("table_catalog").eq(table.catalog) if table.catalog else None, 1214 ) 1215 return f"""IF NOT EXISTS (SELECT * FROM information_schema.tables WHERE {where}) EXEC({sql_literal})""" 1216 elif kind == "INDEX": 1217 index = self.sql(exp.Literal.string(expression.this.text("this"))) 1218 return f"""IF NOT EXISTS (SELECT * FROM sys.indexes WHERE object_id = object_id({identifier}) AND name = {index}) EXEC({sql_literal})""" 1219 elif expression.args.get("replace"): 1220 sql = sql.replace("CREATE OR REPLACE ", "CREATE OR ALTER ", 1) 1221 1222 return self.prepend_ctes(expression, sql)
1224 @generator.unsupported_args("unlogged", "expressions") 1225 def into_sql(self, expression: exp.Into) -> str: 1226 if expression.args.get("temporary"): 1227 # If the Into expression has a temporary property, push this down to the Identifier 1228 table = expression.find(exp.Table) 1229 if table and isinstance(table.this, exp.Identifier): 1230 table.this.set("temporary", True) 1231 1232 return f"{self.seg('INTO')} {self.sql(expression, 'this')}"
1241 def version_sql(self, expression: exp.Version) -> str: 1242 name = "SYSTEM_TIME" if expression.name == "TIMESTAMP" else expression.name 1243 this = f"FOR {name}" 1244 expr = expression.expression 1245 kind = expression.text("kind") 1246 if kind in ("FROM", "BETWEEN"): 1247 args = expr.expressions 1248 sep = "TO" if kind == "FROM" else "AND" 1249 expr_sql = f"{self.sql(seq_get(args, 0))} {sep} {self.sql(seq_get(args, 1))}" 1250 else: 1251 expr_sql = self.sql(expr) 1252 1253 expr_sql = f" {expr_sql}" if expr_sql else "" 1254 return f"{this} {kind}{expr_sql}"
1273 def commit_sql(self, expression: exp.Commit) -> str: 1274 this = self.sql(expression, "this") 1275 this = f" {this}" if this else "" 1276 durability = expression.args.get("durability") 1277 durability = ( 1278 f" WITH (DELAYED_DURABILITY = {'ON' if durability else 'OFF'})" 1279 if durability is not None 1280 else "" 1281 ) 1282 return f"COMMIT TRANSACTION{this}{durability}"
1289 def identifier_sql(self, expression: exp.Identifier) -> str: 1290 identifier = super().identifier_sql(expression) 1291 1292 if expression.args.get("global"): 1293 identifier = f"##{identifier}" 1294 elif expression.args.get("temporary"): 1295 identifier = f"#{identifier}" 1296 1297 return identifier
1348 def columndef_sql(self, expression: exp.ColumnDef, sep: str = " ") -> str: 1349 this = super().columndef_sql(expression, sep) 1350 default = self.sql(expression, "default") 1351 default = f" = {default}" if default else "" 1352 output = self.sql(expression, "output") 1353 output = f" {output}" if output else "" 1354 return f"{this}{default}{output}"
Inherited Members
- sqlglot.generator.Generator
- Generator
- IGNORE_NULLS_IN_FUNC
- LOCKING_READS_SUPPORTED
- WRAP_DERIVED_VALUES
- CREATE_FUNCTION_RETURN_AS
- MATCHED_BY_SOURCE
- SINGLE_STRING_INTERVAL
- INTERVAL_ALLOWS_PLURAL_FORM
- LIMIT_ONLY_LITERALS
- RENAME_TABLE_WITH_DB
- GROUPINGS_SEP
- INDEX_ON
- JOIN_HINTS
- TABLE_HINTS
- QUERY_HINT_SEP
- IS_BOOL_ALLOWED
- DUPLICATE_KEY_UPDATE_WITH_SET
- EXTRACT_ALLOWS_QUOTES
- TZ_TO_WITH_TIME_ZONE
- VALUES_AS_TABLE
- UNNEST_WITH_ORDINALITY
- AGGREGATE_FILTER_SUPPORTED
- SEMI_ANTI_JOIN_WITH_SIDE
- SUPPORTS_TABLE_COPY
- TABLESAMPLE_REQUIRES_PARENS
- TABLESAMPLE_SIZE_IS_ROWS
- TABLESAMPLE_KEYWORDS
- TABLESAMPLE_WITH_METHOD
- COLLATE_IS_FUNC
- DATA_TYPE_SPECIFIERS_ALLOWED
- LAST_DAY_SUPPORTS_DATE_PART
- SUPPORTS_TABLE_ALIAS_COLUMNS
- UNPIVOT_ALIASES_ARE_IDENTIFIERS
- JSON_KEY_VALUE_PAIR_SEP
- INSERT_OVERWRITE
- SUPPORTS_UNLOGGED_TABLES
- SUPPORTS_CREATE_TABLE_LIKE
- LIKE_PROPERTY_INSIDE_SCHEMA
- MULTI_ARG_DISTINCT
- JSON_TYPE_REQUIRED_FOR_EXTRACTION
- JSON_PATH_SINGLE_QUOTE_ESCAPE
- CAN_IMPLEMENT_ARRAY_ANY
- SUPPORTS_WINDOW_EXCLUDE
- COPY_PARAMS_ARE_WRAPPED
- COPY_HAS_INTO_KEYWORD
- STAR_EXCEPT
- HEX_FUNC
- WITH_PROPERTIES_PREFIX
- QUOTE_JSON_PATH
- PAD_FILL_PATTERN_IS_REQUIRED
- SUPPORTS_EXPLODING_PROJECTIONS
- ARRAY_CONCAT_IS_VAR_LEN
- SUPPORTS_CONVERT_TIMEZONE
- SUPPORTS_MEDIAN
- SUPPORTS_UNIX_SECONDS
- ARRAY_SIZE_NAME
- ARRAY_SIZE_DIM_REQUIRED
- TIME_PART_SINGULARS
- TOKEN_MAPPING
- STRUCT_DELIMITER
- PARAMETER_TOKEN
- NAMED_PLACEHOLDER_TOKEN
- EXPRESSION_PRECEDES_PROPERTIES_CREATABLES
- RESERVED_KEYWORDS
- WITH_SEPARATED_COMMENTS
- EXCLUDE_COMMENTS
- UNWRAPPED_INTERVAL_VALUES
- PARAMETERIZABLE_TEXT_TYPES
- RESPECT_IGNORE_NULLS_UNSUPPORTED_EXPRESSIONS
- SENTINEL_LINE_BREAK
- pretty
- identify
- normalize
- pad
- unsupported_level
- max_unsupported
- leading_comma
- max_text_width
- comments
- dialect
- normalize_functions
- unsupported_messages
- generate
- preprocess
- unsupported
- sep
- seg
- pad_comment
- maybe_comment
- wrap
- no_identify
- normalize_func
- indent
- sql
- uncache_sql
- cache_sql
- characterset_sql
- column_parts
- column_sql
- columnposition_sql
- columnconstraint_sql
- computedcolumnconstraint_sql
- autoincrementcolumnconstraint_sql
- compresscolumnconstraint_sql
- generatedasidentitycolumnconstraint_sql
- generatedasrowcolumnconstraint_sql
- periodforsystemtimeconstraint_sql
- notnullcolumnconstraint_sql
- transformcolumnconstraint_sql
- primarykeycolumnconstraint_sql
- uniquecolumnconstraint_sql
- sequenceproperties_sql
- clone_sql
- describe_sql
- heredoc_sql
- prepend_ctes
- with_sql
- cte_sql
- tablealias_sql
- bitstring_sql
- hexstring_sql
- bytestring_sql
- unicodestring_sql
- rawstring_sql
- datatypeparam_sql
- datatype_sql
- directory_sql
- delete_sql
- set_operation
- set_operations
- fetch_sql
- limitoptions_sql
- filter_sql
- hint_sql
- indexparameters_sql
- index_sql
- hex_sql
- lowerhex_sql
- inputoutputformat_sql
- national_sql
- properties_sql
- root_properties
- properties
- with_properties
- locate_properties
- property_name
- property_sql
- likeproperty_sql
- fallbackproperty_sql
- journalproperty_sql
- freespaceproperty_sql
- checksumproperty_sql
- mergeblockratioproperty_sql
- datablocksizeproperty_sql
- blockcompressionproperty_sql
- isolatedloadingproperty_sql
- partitionboundspec_sql
- partitionedofproperty_sql
- lockingproperty_sql
- withdataproperty_sql
- withsystemversioningproperty_sql
- insert_sql
- introducer_sql
- kill_sql
- pseudotype_sql
- objectidentifier_sql
- onconflict_sql
- rowformatdelimitedproperty_sql
- withtablehint_sql
- indextablehint_sql
- historicaldata_sql
- table_parts
- table_sql
- tablefromrows_sql
- tablesample_sql
- pivot_sql
- tuple_sql
- update_sql
- values_sql
- var_sql
- from_sql
- groupingsets_sql
- rollup_sql
- cube_sql
- group_sql
- having_sql
- connect_sql
- prior_sql
- join_sql
- lambda_sql
- lateral_sql
- limit_sql
- set_sql
- pragma_sql
- lock_sql
- literal_sql
- escape_str
- loaddata_sql
- null_sql
- order_sql
- withfill_sql
- cluster_sql
- distribute_sql
- sort_sql
- ordered_sql
- matchrecognizemeasure_sql
- matchrecognize_sql
- query_modifiers
- offset_limit_modifiers
- after_limit_modifiers
- schema_sql
- schema_columns_sql
- star_sql
- parameter_sql
- sessionparameter_sql
- placeholder_sql
- subquery_sql
- qualify_sql
- unnest_sql
- prewhere_sql
- where_sql
- window_sql
- partition_by_sql
- windowspec_sql
- withingroup_sql
- between_sql
- bracket_offset_expressions
- bracket_sql
- all_sql
- any_sql
- exists_sql
- case_sql
- nextvaluefor_sql
- extract_sql
- trim_sql
- convert_concat_args
- concat_sql
- concatws_sql
- check_sql
- foreignkey_sql
- primarykey_sql
- if_sql
- matchagainst_sql
- jsonkeyvalue_sql
- jsonpath_sql
- json_path_part
- formatjson_sql
- jsonobject_sql
- jsonobjectagg_sql
- jsonarray_sql
- jsonarrayagg_sql
- jsoncolumndef_sql
- jsonschema_sql
- jsontable_sql
- openjsoncolumndef_sql
- openjson_sql
- in_sql
- in_unnest_op
- interval_sql
- return_sql
- reference_sql
- anonymous_sql
- paren_sql
- neg_sql
- not_sql
- alias_sql
- pivotalias_sql
- aliases_sql
- atindex_sql
- attimezone_sql
- fromtimezone_sql
- add_sql
- and_sql
- or_sql
- xor_sql
- connector_sql
- bitwiseand_sql
- bitwiseleftshift_sql
- bitwisenot_sql
- bitwiseor_sql
- bitwiserightshift_sql
- bitwisexor_sql
- cast_sql
- currentdate_sql
- collate_sql
- command_sql
- comment_sql
- mergetreettlaction_sql
- mergetreettl_sql
- altercolumn_sql
- alterindex_sql
- alterdiststyle_sql
- altersortkey_sql
- alterrename_sql
- renamecolumn_sql
- alterset_sql
- add_column_sql
- droppartition_sql
- addconstraint_sql
- distinct_sql
- ignorenulls_sql
- respectnulls_sql
- havingmax_sql
- intdiv_sql
- div_sql
- safedivide_sql
- overlaps_sql
- distance_sql
- dot_sql
- eq_sql
- propertyeq_sql
- escape_sql
- glob_sql
- gt_sql
- gte_sql
- ilike_sql
- ilikeany_sql
- like_sql
- likeany_sql
- similarto_sql
- lt_sql
- lte_sql
- mod_sql
- mul_sql
- neq_sql
- nullsafeeq_sql
- nullsafeneq_sql
- slice_sql
- sub_sql
- trycast_sql
- jsoncast_sql
- try_sql
- log_sql
- use_sql
- binary
- ceil_floor
- function_fallback_sql
- func
- format_args
- too_wide
- format_time
- expressions
- op_expressions
- naked_property
- tag_sql
- token_sql
- userdefinedfunction_sql
- joinhint_sql
- kwarg_sql
- when_sql
- whens_sql
- merge_sql
- tochar_sql
- tonumber_sql
- dictproperty_sql
- dictrange_sql
- dictsubproperty_sql
- duplicatekeyproperty_sql
- uniquekeyproperty_sql
- distributedbyproperty_sql
- oncluster_sql
- clusteredbyproperty_sql
- anyvalue_sql
- querytransform_sql
- indexconstraintoption_sql
- checkcolumnconstraint_sql
- indexcolumnconstraint_sql
- nvl2_sql
- comprehension_sql
- columnprefix_sql
- opclass_sql
- predict_sql
- forin_sql
- refresh_sql
- toarray_sql
- tsordstotime_sql
- tsordstotimestamp_sql
- tsordstodatetime_sql
- tsordstodate_sql
- unixdate_sql
- lastday_sql
- dateadd_sql
- arrayany_sql
- struct_sql
- partitionrange_sql
- truncatetable_sql
- copyparameter_sql
- credentials_sql
- copy_sql
- semicolon_sql
- datadeletionproperty_sql
- maskingpolicycolumnconstraint_sql
- gapfill_sql
- scoperesolution_sql
- parsejson_sql
- rand_sql
- changes_sql
- pad_sql
- summarize_sql
- explodinggenerateseries_sql
- arrayconcat_sql
- converttimezone_sql
- json_sql
- jsonvalue_sql
- conditionalinsert_sql
- multitableinserts_sql
- oncondition_sql
- jsonextractquote_sql
- jsonexists_sql
- arrayagg_sql
- apply_sql
- grant_sql
- grantprivilege_sql
- grantprincipal_sql
- columns_sql
- overlay_sql
- todouble_sql
- string_sql
- median_sql
- overflowtruncatebehavior_sql
- unixseconds_sql
- arraysize_sql
- attach_sql
- detach_sql
- attachoption_sql
- featuresattime_sql
- watermarkcolumnconstraint_sql
- encodeproperty_sql
- includeproperty_sql
- xmlelement_sql
- partitionbyrangeproperty_sql
- partitionbyrangepropertydynamic_sql
- unpivotcolumns_sql
- analyzesample_sql
- analyzestatistics_sql
- analyzehistogram_sql
- analyzedelete_sql
- analyzelistchainedrows_sql
- analyzevalidate_sql
- analyze_sql
- xmltable_sql
- xmlnamespace_sql
- export_sql
- declare_sql
- declareitem_sql
- recursivewithsearch_sql
- parameterizedagg_sql
- anonymousaggfunc_sql
- combinedaggfunc_sql
- combinedparameterizedagg_sql
- show_sql
- get_put_sql
- translatecharacters_sql