Skip to content

Add support for MSSQL IF/ELSE statements. #1791

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Apr 6, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 140 additions & 63 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ use serde::{Deserialize, Serialize};
#[cfg(feature = "visitor")]
use sqlparser_derive::{Visit, VisitMut};

use crate::tokenizer::Span;
use crate::keywords::Keyword;
use crate::tokenizer::{Span, Token};

pub use self::data_type::{
ArrayElemTypeDef, BinaryLength, CharLengthUnits, CharacterLength, DataType, EnumMember,
Expand Down Expand Up @@ -2118,20 +2119,23 @@ pub enum Password {
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct CaseStatement {
/// The `CASE` token that starts the statement.
pub case_token: AttachedToken,
pub match_expr: Option<Expr>,
pub when_blocks: Vec<ConditionalStatements>,
pub else_block: Option<Vec<Statement>>,
/// TRUE if the statement ends with `END CASE` (vs `END`).
pub has_end_case: bool,
pub else_block: Option<ConditionalStatements>,
/// The last token of the statement (`END` or `CASE`).
pub end_case_token: AttachedToken,
}

impl fmt::Display for CaseStatement {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let CaseStatement {
case_token: _,
match_expr,
when_blocks,
else_block,
has_end_case,
end_case_token: AttachedToken(end),
} = self;

write!(f, "CASE")?;
Expand All @@ -2145,116 +2149,189 @@ impl fmt::Display for CaseStatement {
}

if let Some(else_block) = else_block {
write!(f, " ELSE ")?;
format_statement_list(f, else_block)?;
write!(f, " {else_block}")?;
}

write!(f, " END")?;
if *has_end_case {
write!(f, " CASE")?;

if let Token::Word(w) = &end.token {
if w.keyword == Keyword::CASE {
write!(f, " CASE")?;
}
}

Ok(())
}
}

/// An `IF` statement.
///
/// Examples:
/// ```sql
/// IF TRUE THEN
/// SELECT 1;
/// SELECT 2;
/// ELSEIF TRUE THEN
/// SELECT 3;
/// ELSE
/// SELECT 4;
/// END IF
/// ```
///
/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#if)
/// [Snowflake](https://docs.snowflake.com/en/sql-reference/snowflake-scripting/if)
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct IfStatement {
pub if_block: ConditionalStatements,
pub elseif_blocks: Vec<ConditionalStatements>,
pub else_block: Option<Vec<Statement>>,
pub enum IfStatement {
/// An `IF ... THEN [ELSE[IF] ...] END IF` statement.
///
/// Example:
/// ```sql
/// IF TRUE THEN
/// SELECT 1;
/// SELECT 2;
/// ELSEIF TRUE THEN
/// SELECT 3;
/// ELSE
/// SELECT 4;
/// END IF
/// ```
///
/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/procedural-language#if)
/// [Snowflake](https://docs.snowflake.com/en/sql-reference/snowflake-scripting/if)
IfThenElseEnd {
/// The `IF` token that starts the statement.
if_token: AttachedToken,
if_block: ConditionalStatements,
elseif_blocks: Vec<ConditionalStatements>,
else_block: Option<ConditionalStatements>,
/// The `IF` token that ends the statement.
end_if_token: AttachedToken,
},
/// An MSSQL `IF ... ELSE ...` statement.
///
/// Example:
/// ```sql
/// IF 1=1 SELECT 1 ELSE SELECT 2
/// ```
///
/// [MSSQL](https://learn.microsoft.com/en-us/sql/t-sql/language-elements/if-else-transact-sql?view=sql-server-ver16)
MsSqlIfElse {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to merge the representation to be shared by both? Something like this roughly I'm thinking

enum StatementBlock {
  Expr(Expr),
  Statements(Vec<Statements>),
  Begin(Vec<Statements>)
}

pub struct IfStatement {
    pub condition: Expr,
    pub if_block: StatementBlock,
    pub elseif_blocks: Vec<StatementBlock>,
    pub else_block: Option<StatementBlock>,
}

Ideally we avoid introducing dialect specific nodes in the AST

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your feedback. I pushed further changes to avoid the dialect-specific AST nodes.

if_token: AttachedToken,
condition: Expr,
if_statements: MsSqlIfStatements,
else_statements: Option<MsSqlIfStatements>,
},
}

impl fmt::Display for IfStatement {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let IfStatement {
if_block,
elseif_blocks,
else_block,
} = self;
match self {
IfStatement::IfThenElseEnd {
if_token: _,
if_block,
elseif_blocks,
else_block,
end_if_token: _,
} => {
write!(f, "{if_block}")?;

write!(f, "{if_block}")?;
if !elseif_blocks.is_empty() {
write!(f, " {}", display_separated(elseif_blocks, " "))?;
}

if !elseif_blocks.is_empty() {
write!(f, " {}", display_separated(elseif_blocks, " "))?;
}
if let Some(else_block) = else_block {
write!(f, " {else_block}")?;
}

if let Some(else_block) = else_block {
write!(f, " ELSE ")?;
format_statement_list(f, else_block)?;
}
write!(f, " END IF")?;

Ok(())
}
IfStatement::MsSqlIfElse {
if_token: _,
condition,
if_statements,
else_statements,
} => {
write!(f, "IF {condition} {if_statements}")?;

write!(f, " END IF")?;
if let Some(els) = else_statements {
write!(f, " ELSE {els}")?;
}

Ok(())
Ok(())
}
}
}
}

/// Represents a type of [ConditionalStatements]
/// (MSSQL) Either a single [Statement] or a block of statements
/// enclosed in `BEGIN` and `END`.
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub enum ConditionalStatementKind {
/// `WHEN <condition> THEN <statements>`
When,
/// `IF <condition> THEN <statements>`
If,
/// `ELSEIF <condition> THEN <statements>`
ElseIf,
pub enum MsSqlIfStatements {
/// A single statement.
Single(Box<Statement>),
/// ```sql
/// A logical block of statements.
///
/// BEGIN
/// <statement>;
/// <statement>;
/// ...
/// END
/// ```
Block {
begin_token: AttachedToken,
statements: Vec<Statement>,
end_token: AttachedToken,
},
}

impl fmt::Display for MsSqlIfStatements {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MsSqlIfStatements::Single(stmt) => stmt.fmt(f),
MsSqlIfStatements::Block { statements, .. } => {
write!(f, "BEGIN ")?;
format_statement_list(f, statements)?;
write!(f, " END")
}
}
}
}

/// A block within a [Statement::Case] or [Statement::If]-like statement
///
/// Examples:
/// Example 1:
/// ```sql
/// WHEN EXISTS(SELECT 1) THEN SELECT 1;
/// ```
///
/// Example 2:
/// ```sql
/// IF TRUE THEN SELECT 1; SELECT 2;
/// ```
///
/// Example 3:
/// ```sql
/// ELSE SELECT 1; SELECT 2;
/// ```
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct ConditionalStatements {
/// The condition expression.
pub condition: Expr,
/// The start token of the conditional (`WHEN`, `IF`, `ELSEIF` or `ELSE`).
pub start_token: AttachedToken,
/// The condition expression. `None` for `ELSE` statements.
pub condition: Option<Expr>,
/// Statement list of the `THEN` clause.
pub statements: Vec<Statement>,
pub kind: ConditionalStatementKind,
}

impl fmt::Display for ConditionalStatements {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let ConditionalStatements {
condition: expr,
start_token: AttachedToken(start),
condition,
statements,
kind,
} = self;

let kind = match kind {
ConditionalStatementKind::When => "WHEN",
ConditionalStatementKind::If => "IF",
ConditionalStatementKind::ElseIf => "ELSEIF",
};
let keyword = &start.token;

write!(f, "{kind} {expr} THEN")?;
if let Some(expr) = condition {
write!(f, "{keyword} {expr} THEN")?;
} else {
write!(f, "{keyword}")?;
}

if !statements.is_empty() {
write!(f, " ")?;
Expand Down
Loading