From 8e66a0d1da49e78564db281d6b7f20c3a28e90c7 Mon Sep 17 00:00:00 2001 From: Yoav Cohen Date: Fri, 11 Jul 2025 10:29:02 +0200 Subject: [PATCH 1/3] Support for T-SQL --- src/parser/mod.rs | 10 ++++++++++ tests/sqlparser_mssql.rs | 29 ++++++++++++++++++++++++++++- tests/sqlparser_mysql.rs | 1 + 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a2d01f136..f8cdbf4c5 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -219,9 +219,14 @@ impl From for MatchedTrailingBracket { #[derive(Debug, Clone, PartialEq, Eq)] pub struct ParserOptions { pub trailing_commas: bool, + /// Controls how literal values are unescaped. See /// [`Tokenizer::with_unescape`] for more details. pub unescape: bool, + + /// Controls if the parser expects a semi-colon token + /// between statements. + pub require_semicolon_stmt_delimiter: bool, } impl Default for ParserOptions { @@ -229,6 +234,7 @@ impl Default for ParserOptions { Self { trailing_commas: false, unescape: true, + require_semicolon_stmt_delimiter: true, } } } @@ -467,6 +473,10 @@ impl<'a> Parser<'a> { expecting_statement_delimiter = false; } + if !self.options.require_semicolon_stmt_delimiter { + expecting_statement_delimiter = false; + } + match self.peek_token().token { Token::EOF => break, diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index b4a650cee..7a4dec2e9 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -32,7 +32,7 @@ use sqlparser::ast::DeclareAssignment::MsSqlAssignment; use sqlparser::ast::Value::SingleQuotedString; use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, MsSqlDialect}; -use sqlparser::parser::{Parser, ParserError}; +use sqlparser::parser::{Parser, ParserError, ParserOptions}; #[test] fn parse_mssql_identifiers() { @@ -2327,6 +2327,18 @@ fn ms() -> TestedDialects { TestedDialects::new(vec![Box::new(MsSqlDialect {})]) } +// MS SQL dialect with support for optional semi-colon statement delimiters +fn tsql() -> TestedDialects { + TestedDialects::new_with_options( + vec![Box::new(MsSqlDialect {})], + ParserOptions { + trailing_commas: false, + unescape: true, + require_semicolon_stmt_delimiter: false, + }, + ) +} + fn ms_and_generic() -> TestedDialects { TestedDialects::new(vec![Box::new(MsSqlDialect {}), Box::new(GenericDialect {})]) } @@ -2483,3 +2495,18 @@ fn parse_mssql_grant() { fn parse_mssql_deny() { ms().verified_stmt("DENY SELECT ON my_table TO public, db_admin"); } + +#[test] +fn test_tsql_no_semicolon_delimiter() { + let sql = r#" +DECLARE @X AS NVARCHAR(MAX)='x' +DECLARE @Y AS NVARCHAR(MAX)='y' + "#; + + let stmts = tsql().parse_sql_statements(sql).unwrap(); + assert_eq!(stmts.len(), 2); + assert_eq!( + stmts.iter().all(|s| matches!(s, Statement::Declare { .. })), + true + ); +} diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index e8c6719e0..9068ed9c5 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1442,6 +1442,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() { ParserOptions { trailing_commas: false, unescape: false, + require_semicolon_stmt_delimiter: true, } ) .verified_stmt(sql), From 4d0a83216c817ba038431a13d347cb78f7af6644 Mon Sep 17 00:00:00 2001 From: Yoav Cohen Date: Fri, 11 Jul 2025 10:45:25 +0200 Subject: [PATCH 2/3] Support for T-SQL --- src/parser/mod.rs | 2 -- tests/sqlparser_mssql.rs | 5 +---- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f8cdbf4c5..22e0bf351 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -219,11 +219,9 @@ impl From for MatchedTrailingBracket { #[derive(Debug, Clone, PartialEq, Eq)] pub struct ParserOptions { pub trailing_commas: bool, - /// Controls how literal values are unescaped. See /// [`Tokenizer::with_unescape`] for more details. pub unescape: bool, - /// Controls if the parser expects a semi-colon token /// between statements. pub require_semicolon_stmt_delimiter: bool, diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 7a4dec2e9..50c6448d9 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -2505,8 +2505,5 @@ DECLARE @Y AS NVARCHAR(MAX)='y' let stmts = tsql().parse_sql_statements(sql).unwrap(); assert_eq!(stmts.len(), 2); - assert_eq!( - stmts.iter().all(|s| matches!(s, Statement::Declare { .. })), - true - ); + assert!(stmts.iter().all(|s| matches!(s, Statement::Declare { .. }))); } From 9169e572758dd8610431dc31fa02b6712b4ac35e Mon Sep 17 00:00:00 2001 From: Yoav Cohen Date: Fri, 11 Jul 2025 11:54:21 +0200 Subject: [PATCH 3/3] Code review feedback --- src/parser/mod.rs | 2 +- src/test_utils.rs | 5 +++++ tests/sqlparser_common.rs | 22 ++++++++++++++++++++-- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 22e0bf351..47b63da87 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -223,7 +223,7 @@ pub struct ParserOptions { /// [`Tokenizer::with_unescape`] for more details. pub unescape: bool, /// Controls if the parser expects a semi-colon token - /// between statements. + /// between statements. Default is `true`. pub require_semicolon_stmt_delimiter: bool, } diff --git a/src/test_utils.rs b/src/test_utils.rs index 544ceaef8..654f2723e 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -294,6 +294,11 @@ pub fn all_dialects() -> TestedDialects { ]) } +// Returns all available dialects with the specified parser options +pub fn all_dialects_with_options(options: ParserOptions) -> TestedDialects { + TestedDialects::new_with_options(all_dialects().dialects, options) +} + /// Returns all dialects matching the given predicate. pub fn all_dialects_where(predicate: F) -> TestedDialects where diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index b7b5b630b..15144479c 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -40,8 +40,9 @@ use sqlparser::parser::{Parser, ParserError, ParserOptions}; use sqlparser::tokenizer::Tokenizer; use sqlparser::tokenizer::{Location, Span}; use test_utils::{ - all_dialects, all_dialects_where, alter_table_op, assert_eq_vec, call, expr_from_projection, - join, number, only, table, table_alias, table_from_name, TestedDialects, + all_dialects, all_dialects_where, all_dialects_with_options, alter_table_op, assert_eq_vec, + call, expr_from_projection, join, number, only, table, table_alias, table_from_name, + TestedDialects, }; #[macro_use] @@ -16115,3 +16116,20 @@ fn test_select_exclude() { ParserError::ParserError("Expected: end of statement, found: EXCLUDE".to_string()) ); } + +#[test] +fn test_no_semicolon_required_between_statements() { + let sql = r#" +SELECT * FROM tbl1 +SELECT * FROM tbl2 + "#; + + let dialects = all_dialects_with_options(ParserOptions { + trailing_commas: false, + unescape: true, + require_semicolon_stmt_delimiter: false, + }); + let stmts = dialects.parse_sql_statements(sql).unwrap(); + assert_eq!(stmts.len(), 2); + assert!(stmts.iter().all(|s| matches!(s, Statement::Query { .. }))); +}