| // Licensed to the Apache Software Foundation (ASF) under one | |
| // or more contributor license agreements. See the NOTICE file | |
| // distributed with this work for additional information | |
| // regarding copyright ownership. The ASF licenses this file | |
| // to you under the Apache License, Version 2.0 (the | |
| // "License"); you may not use this file except in compliance | |
| // with the License. You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, | |
| // software distributed under the License is distributed on an | |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| // KIND, either express or implied. See the License for the | |
| // specific language governing permissions and limitations | |
| // under the License. | |
| use crate::ast::helpers::attached_token::AttachedToken; | |
| use crate::ast::{ | |
| BeginEndStatements, ConditionalStatementBlock, ConditionalStatements, CreateTrigger, | |
| GranteesType, IfStatement, Statement, | |
| }; | |
| use crate::dialect::Dialect; | |
| use crate::keywords::Keyword; | |
| use crate::parser::{Parser, ParserError}; | |
| use crate::tokenizer::Token; | |
| #[cfg(not(feature = "std"))] | |
| use alloc::{vec, vec::Vec}; | |
| /// A [`Dialect`] for [Microsoft SQL Server](https://www.microsoft.com/en-us/sql-server/) | |
| #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] | |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] | |
| pub struct MsSqlDialect {} | |
| impl Dialect for MsSqlDialect { | |
| fn is_delimited_identifier_start(&self, ch: char) -> bool { | |
| ch == '"' || ch == '[' | |
| } | |
| fn is_identifier_start(&self, ch: char) -> bool { | |
| // See https://docs.microsoft.com/en-us/sql/relational-databases/databases/database-identifiers?view=sql-server-2017#rules-for-regular-identifiers | |
| ch.is_alphabetic() || ch == '_' || ch == '#' || ch == '@' | |
| } | |
| fn is_identifier_part(&self, ch: char) -> bool { | |
| ch.is_alphabetic() | |
| || ch.is_ascii_digit() | |
| || ch == '@' | |
| || ch == '$' | |
| || ch == '#' | |
| || ch == '_' | |
| } | |
| fn identifier_quote_style(&self, _identifier: &str) -> Option<char> { | |
| Some('[') | |
| } | |
| /// SQL Server has `CONVERT(type, value)` instead of `CONVERT(value, type)` | |
| /// <https://learn.microsoft.com/en-us/sql/t-sql/functions/cast-and-convert-transact-sql?view=sql-server-ver16> | |
| fn convert_type_before_value(&self) -> bool { | |
| true | |
| } | |
| fn supports_outer_join_operator(&self) -> bool { | |
| true | |
| } | |
| /// SQL Server supports `$` as a prefix for money literals | |
| /// <https://learn.microsoft.com/en-us/sql/t-sql/data-types/constants-transact-sql?view=sql-server-ver17#money-constants> | |
| fn supports_dollar_as_money_prefix(&self) -> bool { | |
| true | |
| } | |
| fn supports_create_proc_syntax(&self) -> bool { | |
| true | |
| } | |
| fn supports_connect_by(&self) -> bool { | |
| true | |
| } | |
| fn supports_eq_alias_assignment(&self) -> bool { | |
| true | |
| } | |
| fn supports_try_convert(&self) -> bool { | |
| true | |
| } | |
| /// In MSSQL, there is no boolean type, and `true` and `false` are valid column names | |
| fn supports_boolean_literals(&self) -> bool { | |
| false | |
| } | |
| fn supports_named_fn_args_with_colon_operator(&self) -> bool { | |
| true | |
| } | |
| fn supports_named_fn_args_with_expr_name(&self) -> bool { | |
| true | |
| } | |
| fn supports_named_fn_args_with_rarrow_operator(&self) -> bool { | |
| false | |
| } | |
| fn supports_start_transaction_modifier(&self) -> bool { | |
| true | |
| } | |
| fn supports_end_transaction_modifier(&self) -> bool { | |
| true | |
| } | |
| /// See: <https://learn.microsoft.com/en-us/sql/t-sql/statements/set-statements-transact-sql> | |
| fn supports_set_stmt_without_operator(&self) -> bool { | |
| true | |
| } | |
| /// See: <https://learn.microsoft.com/en-us/sql/relational-databases/tables/querying-data-in-a-system-versioned-temporal-table> | |
| fn supports_table_versioning(&self) -> bool { | |
| true | |
| } | |
| /// See <https://learn.microsoft.com/en-us/sql/t-sql/language-elements/slash-star-comment-transact-sql?view=sql-server-ver16> | |
| fn supports_nested_comments(&self) -> bool { | |
| true | |
| } | |
| /// See <https://learn.microsoft.com/en-us/sql/t-sql/queries/from-transact-sql> | |
| fn supports_object_name_double_dot_notation(&self) -> bool { | |
| true | |
| } | |
| /// See <https://learn.microsoft.com/en-us/sql/relational-databases/security/authentication-access/server-level-roles> | |
| fn get_reserved_grantees_types(&self) -> &[GranteesType] { | |
| &[GranteesType::Public] | |
| } | |
| fn is_select_item_alias(&self, explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool { | |
| match kw { | |
| // List of keywords that cannot be used as select item (column) aliases in MSSQL | |
| // regardless of whether the alias is explicit or implicit. | |
| // | |
| // These are T-SQL statement-starting keywords; allowing them as implicit aliases | |
| // causes the parser to consume the keyword as an alias for the previous expression, | |
| // then fail on the token that follows (e.g. `TABLE`, `@var`, `sp_name`, …). | |
| Keyword::IF | |
| | Keyword::ELSE | |
| | Keyword::DECLARE | |
| | Keyword::EXEC | |
| | Keyword::EXECUTE | |
| | Keyword::INSERT | |
| | Keyword::UPDATE | |
| | Keyword::DELETE | |
| | Keyword::DROP | |
| | Keyword::CREATE | |
| | Keyword::ALTER | |
| | Keyword::TRUNCATE | |
| | Keyword::PRINT | |
| | Keyword::WHILE | |
| | Keyword::RETURN | |
| | Keyword::THROW | |
| | Keyword::RAISERROR | |
| | Keyword::MERGE => false, | |
| _ => explicit || self.is_column_alias(kw, parser), | |
| } | |
| } | |
| fn is_table_factor_alias(&self, explicit: bool, kw: &Keyword, parser: &mut Parser) -> bool { | |
| match kw { | |
| // List of keywords that cannot be used as table aliases in MSSQL | |
| // regardless of whether the alias is explicit or implicit. | |
| // | |
| // These are T-SQL statement-starting keywords. Without blocking them here, | |
| // a bare `SELECT * FROM t` followed by a newline and one of these keywords | |
| // would cause the parser to consume the keyword as a table alias for `t`, | |
| // then fail on the token that follows (e.g. `@var`, `sp_name`, `TABLE`, …). | |
| // | |
| // `SET` is already covered by the global `RESERVED_FOR_TABLE_ALIAS` list; | |
| // the keywords below are MSSQL-specific additions. | |
| Keyword::IF | |
| | Keyword::ELSE | |
| | Keyword::DECLARE | |
| | Keyword::EXEC | |
| | Keyword::EXECUTE | |
| | Keyword::INSERT | |
| | Keyword::UPDATE | |
| | Keyword::DELETE | |
| | Keyword::DROP | |
| | Keyword::CREATE | |
| | Keyword::ALTER | |
| | Keyword::TRUNCATE | |
| | Keyword::PRINT | |
| | Keyword::WHILE | |
| | Keyword::RETURN | |
| | Keyword::THROW | |
| | Keyword::RAISERROR | |
| | Keyword::MERGE => false, | |
| _ => explicit || self.is_table_alias(kw, parser), | |
| } | |
| } | |
| fn parse_statement(&self, parser: &mut Parser) -> Option<Result<Statement, ParserError>> { | |
| if parser.parse_keyword(Keyword::BEGIN) { | |
| // Check if this is a BEGIN...END block rather than BEGIN TRANSACTION | |
| let is_block = parser | |
| .maybe_parse(|p| { | |
| if p.parse_transaction_modifier().is_some() | |
| || p.parse_one_of_keywords(&[ | |
| Keyword::TRANSACTION, | |
| Keyword::WORK, | |
| Keyword::TRAN, | |
| ]) | |
| .is_some() | |
| || matches!(p.peek_token_ref().token, Token::SemiColon | Token::EOF) | |
| { | |
| p.expected_ref("statement", p.peek_token_ref()) | |
| } else { | |
| Ok(()) | |
| } | |
| }) | |
| .unwrap_or(None) | |
| .is_some(); | |
| if is_block { | |
| Some(parser.parse_begin_exception_end()) | |
| } else { | |
| parser.prev_token(); | |
| None | |
| } | |
| } else if parser.peek_keyword(Keyword::IF) { | |
| Some(self.parse_if_stmt(parser)) | |
| } else if parser.parse_keywords(&[Keyword::CREATE, Keyword::TRIGGER]) { | |
| Some(self.parse_create_trigger(parser, false)) | |
| } else if parser.parse_keywords(&[ | |
| Keyword::CREATE, | |
| Keyword::OR, | |
| Keyword::ALTER, | |
| Keyword::TRIGGER, | |
| ]) { | |
| Some(self.parse_create_trigger(parser, true)) | |
| } else { | |
| None | |
| } | |
| } | |
| fn get_next_precedence(&self, parser: &Parser) -> Option<Result<u8, ParserError>> { | |
| let token = parser.peek_token_ref(); | |
| match &token.token { | |
| // lowest prec to prevent it from turning into a binary op | |
| Token::Colon => Some(Ok(self.prec_unknown())), | |
| _ => None, | |
| } | |
| } | |
| } | |
| impl MsSqlDialect { | |
| /// ```sql | |
| /// IF boolean_expression | |
| /// { sql_statement | statement_block } | |
| /// [ ELSE | |
| /// { sql_statement | statement_block } ] | |
| /// ``` | |
| fn parse_if_stmt(&self, parser: &mut Parser) -> Result<Statement, ParserError> { | |
| let if_token = parser.expect_keyword(Keyword::IF)?; | |
| let condition = parser.parse_expr()?; | |
| let if_block = if parser.peek_keyword(Keyword::BEGIN) { | |
| let begin_token = parser.expect_keyword(Keyword::BEGIN)?; | |
| let statements = self.parse_statement_list(parser, Some(Keyword::END))?; | |
| let end_token = parser.expect_keyword(Keyword::END)?; | |
| ConditionalStatementBlock { | |
| start_token: AttachedToken(if_token), | |
| condition: Some(condition), | |
| then_token: None, | |
| conditional_statements: ConditionalStatements::BeginEnd(BeginEndStatements { | |
| begin_token: AttachedToken(begin_token), | |
| statements, | |
| end_token: AttachedToken(end_token), | |
| }), | |
| } | |
| } else { | |
| let stmt = parser.parse_statement()?; | |
| ConditionalStatementBlock { | |
| start_token: AttachedToken(if_token), | |
| condition: Some(condition), | |
| then_token: None, | |
| conditional_statements: ConditionalStatements::Sequence { | |
| statements: vec![stmt], | |
| }, | |
| } | |
| }; | |
| let mut prior_statement_ended_with_semi_colon = false; | |
| while let Token::SemiColon = parser.peek_token_ref().token { | |
| parser.advance_token(); | |
| prior_statement_ended_with_semi_colon = true; | |
| } | |
| let mut else_block = None; | |
| if parser.peek_keyword(Keyword::ELSE) { | |
| let else_token = parser.expect_keyword(Keyword::ELSE)?; | |
| if parser.peek_keyword(Keyword::BEGIN) { | |
| let begin_token = parser.expect_keyword(Keyword::BEGIN)?; | |
| let statements = self.parse_statement_list(parser, Some(Keyword::END))?; | |
| let end_token = parser.expect_keyword(Keyword::END)?; | |
| else_block = Some(ConditionalStatementBlock { | |
| start_token: AttachedToken(else_token), | |
| condition: None, | |
| then_token: None, | |
| conditional_statements: ConditionalStatements::BeginEnd(BeginEndStatements { | |
| begin_token: AttachedToken(begin_token), | |
| statements, | |
| end_token: AttachedToken(end_token), | |
| }), | |
| }); | |
| } else { | |
| let stmt = parser.parse_statement()?; | |
| else_block = Some(ConditionalStatementBlock { | |
| start_token: AttachedToken(else_token), | |
| condition: None, | |
| then_token: None, | |
| conditional_statements: ConditionalStatements::Sequence { | |
| statements: vec![stmt], | |
| }, | |
| }); | |
| } | |
| } else if prior_statement_ended_with_semi_colon { | |
| parser.prev_token(); | |
| } | |
| Ok(IfStatement { | |
| if_block, | |
| else_block, | |
| elseif_blocks: Vec::new(), | |
| end_token: None, | |
| } | |
| .into()) | |
| } | |
| /// Parse `CREATE TRIGGER` for [MsSql] | |
| /// | |
| /// [MsSql]: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql | |
| fn parse_create_trigger( | |
| &self, | |
| parser: &mut Parser, | |
| or_alter: bool, | |
| ) -> Result<Statement, ParserError> { | |
| let name = parser.parse_object_name(false)?; | |
| parser.expect_keyword_is(Keyword::ON)?; | |
| let table_name = parser.parse_object_name(false)?; | |
| let period = parser.parse_trigger_period()?; | |
| let events = parser.parse_comma_separated(Parser::parse_trigger_event)?; | |
| parser.expect_keyword_is(Keyword::AS)?; | |
| let statements = Some(parser.parse_conditional_statements(&[Keyword::END])?); | |
| Ok(CreateTrigger { | |
| or_alter, | |
| temporary: false, | |
| or_replace: false, | |
| is_constraint: false, | |
| name, | |
| period: Some(period), | |
| period_before_table: false, | |
| events, | |
| table_name, | |
| referenced_table_name: None, | |
| referencing: Vec::new(), | |
| trigger_object: None, | |
| condition: None, | |
| exec_body: None, | |
| statements_as: true, | |
| statements, | |
| characteristics: None, | |
| } | |
| .into()) | |
| } | |
| /// Parse a sequence of statements, optionally separated by semicolon. | |
| /// | |
| /// Stops parsing when reaching EOF or the given keyword. | |
| fn parse_statement_list( | |
| &self, | |
| parser: &mut Parser, | |
| terminal_keyword: Option<Keyword>, | |
| ) -> Result<Vec<Statement>, ParserError> { | |
| let mut stmts = Vec::new(); | |
| loop { | |
| if let Token::EOF = parser.peek_token_ref().token { | |
| break; | |
| } | |
| if let Some(term) = terminal_keyword { | |
| if parser.peek_keyword(term) { | |
| break; | |
| } | |
| } | |
| stmts.push(parser.parse_statement()?); | |
| while let Token::SemiColon = parser.peek_token_ref().token { | |
| parser.advance_token(); | |
| } | |
| } | |
| Ok(stmts) | |
| } | |
| } |