From 484d9ac101355e78c4c7eddbafe521dd4c9a3c6b Mon Sep 17 00:00:00 2001 From: Devayan Mandal Date: Tue, 13 Oct 2026 14:48:09 +0530 Subject: [PATCH 1/8] PostgreSQL: Add support for DO statement --- src/ast/mod.rs | 18 +++++++++++++ src/ast/spans.rs | 1 + src/dialect/mod.rs | 5 ++++ src/dialect/postgresql.rs | 4 +++ src/parser/mod.rs | 25 ++++++++++++++++++ tests/sqlparser_common.rs | 55 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 108 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 87618bbc5f..e8d74b5c3a 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4974,6 +4974,17 @@ pub enum Statement { channel: Ident, }, /// ```sql + /// DO [ LANGUAGE lang_name ] code + /// ``` + /// + /// See Postgres + Do { + /// Optional procedural language name. + language: Option, + /// The anonymous code block, a string literal. + code: ValueWithSpan, + }, + /// ```sql /// NOTIFY channel [ , payload ] /// ``` /// send a notification event together with an optional "payload" string to channel @@ -6543,6 +6554,13 @@ impl fmt::Display for Statement { write!(f, "UNLISTEN {channel}")?; Ok(()) } + Statement::Do { language, code } => { + write!(f, "DO ")?; + if let Some(language) = language { + write!(f, "LANGUAGE {language} ")?; + } + write!(f, "{code}") + } Statement::NOTIFY { channel, payload } => { write!(f, "NOTIFY {channel}")?; if let Some(payload) = payload { diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 74e0844d56..36488a3764 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -502,6 +502,7 @@ impl Spanned for Statement { Statement::ShowObjects { .. } => Span::empty(), Statement::ShowViews { .. } => Span::empty(), Statement::LISTEN { .. } => Span::empty(), + Statement::Do { .. } => Span::empty(), Statement::NOTIFY { .. } => Span::empty(), Statement::LoadData { .. } => Span::empty(), Statement::UNLISTEN { .. } => Span::empty(), diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index db3f5c0661..c2080c1a28 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1428,6 +1428,11 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports the `DO` statement + fn supports_do_statement(&self) -> bool { + false + } + /// Returns true if the dialect supports the `LISTEN`, `UNLISTEN` and `NOTIFY` statements fn supports_listen_notify(&self) -> bool { false diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index e8202cfdac..e400a10696 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -402,6 +402,10 @@ impl Dialect for PostgreSqlDialect { true } + fn supports_do_statement(&self) -> bool { + true + } + /// see fn supports_exclude_constraint(&self) -> bool { true diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f5a6ff8c71..f6b3851ca8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -737,6 +737,7 @@ impl<'a> Parser<'a> { Keyword::LISTEN if self.dialect.supports_listen_notify() => self.parse_listen(), Keyword::UNLISTEN if self.dialect.supports_listen_notify() => self.parse_unlisten(), Keyword::NOTIFY if self.dialect.supports_listen_notify() => self.parse_notify(), + Keyword::DO if self.dialect.supports_do_statement() => self.parse_do(), // `PRAGMA` is sqlite specific https://www.sqlite.org/pragma.html Keyword::PRAGMA => self.parse_pragma(), Keyword::UNLOAD => { @@ -1521,6 +1522,30 @@ impl<'a> Parser<'a> { Ok(Statement::UNLISTEN { channel }) } + /// Parse `DO` statement. + pub fn parse_do(&mut self) -> Result { + let mut language = self.parse_do_language()?; + match &self.peek_token_ref().token { + Token::SingleQuotedString(_) + | Token::DollarQuotedString(_) + | Token::EscapedStringLiteral(_) => {} + _ => return self.expected_ref("string literal", self.peek_token_ref()), + } + let code = self.parse_value()?; + if language.is_none() { + language = self.parse_do_language()?; + } + Ok(Statement::Do { language, code }) + } + + fn parse_do_language(&mut self) -> Result, ParserError> { + if self.parse_keyword(Keyword::LANGUAGE) { + Ok(Some(self.parse_identifier()?)) + } else { + Ok(None) + } + } + /// Parse `NOTIFY` statement. pub fn parse_notify(&mut self) -> Result { let channel = self.parse_identifier()?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 396e898bca..a6f96fb352 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -22099,3 +22099,58 @@ fn parse_qualified_literal_prefix_requires_adjacent_period() { dialects.tokenizes_to(sql, vec![token]); } } + +#[test] +fn parse_do_statement() { + let dialects = all_dialects_where(|d| d.supports_do_statement()); + + match dialects.verified_stmt("DO $$BEGIN RAISE NOTICE 'hi'; END$$") { + Statement::Do { language, code } => { + assert_eq!(language, None); + assert_eq!( + code.value, + Value::DollarQuotedString(DollarQuotedString { + value: "BEGIN RAISE NOTICE 'hi'; END".to_string(), + tag: None, + }) + ); + } + _ => unreachable!(), + } + + match dialects.verified_stmt("DO LANGUAGE plpgsql $body$BEGIN NULL; END$body$") { + Statement::Do { language, .. } => { + assert_eq!(language, Some(Ident::new("plpgsql"))); + } + _ => unreachable!(), + } + + dialects.verified_stmt("DO 'BEGIN NULL; END'"); + dialects.one_statement_parses_to( + "DO $$BEGIN NULL; END$$ LANGUAGE plpgsql", + "DO LANGUAGE plpgsql $$BEGIN NULL; END$$", + ); + assert_eq!( + dialects + .parse_sql_statements("DO LANGUAGE plpgsql $$BEGIN NULL; END$$ LANGUAGE sql") + .unwrap_err(), + ParserError::ParserError("Expected: end of statement, found: LANGUAGE".to_string()) + ); + + assert_eq!( + dialects.parse_sql_statements("DO 1").unwrap_err(), + ParserError::ParserError("Expected: string literal, found: 1".to_string()) + ); + assert_eq!( + dialects.parse_sql_statements("DO LANGUAGE").unwrap_err(), + ParserError::ParserError("Expected: identifier, found: EOF".to_string()) + ); + + let dialects = all_dialects_where(|d| !d.supports_do_statement()); + assert_eq!( + dialects + .parse_sql_statements("DO $$BEGIN NULL; END$$") + .unwrap_err(), + ParserError::ParserError("Expected: an SQL statement, found: DO".to_string()) + ); +} From 5c80c99e781a68ea58240547a86cc1c84b472a7d Mon Sep 17 00:00:00 2001 From: Devayan Mandal Date: Tue, 13 Oct 2026 14:48:09 +0530 Subject: [PATCH 2/8] PostgreSQL: Add DO statement fuzz seeds --- fuzz/fuzz_seeds/postgres_do.sql | 1 + fuzz/fuzz_seeds/postgres_do_language_after.sql | 1 + fuzz/fuzz_seeds/postgres_do_language_before.sql | 1 + fuzz/fuzz_seeds/postgres_do_single_quoted.sql | 1 + fuzz/fuzz_seeds/postgres_do_tagged_dollar_quote.sql | 1 + 5 files changed, 5 insertions(+) create mode 100644 fuzz/fuzz_seeds/postgres_do.sql create mode 100644 fuzz/fuzz_seeds/postgres_do_language_after.sql create mode 100644 fuzz/fuzz_seeds/postgres_do_language_before.sql create mode 100644 fuzz/fuzz_seeds/postgres_do_single_quoted.sql create mode 100644 fuzz/fuzz_seeds/postgres_do_tagged_dollar_quote.sql diff --git a/fuzz/fuzz_seeds/postgres_do.sql b/fuzz/fuzz_seeds/postgres_do.sql new file mode 100644 index 0000000000..a27d1d18f5 --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_do.sql @@ -0,0 +1 @@ +DO $$BEGIN RAISE NOTICE 'hello'; END$$ \ No newline at end of file diff --git a/fuzz/fuzz_seeds/postgres_do_language_after.sql b/fuzz/fuzz_seeds/postgres_do_language_after.sql new file mode 100644 index 0000000000..30f6e3aeac --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_do_language_after.sql @@ -0,0 +1 @@ +DO $$BEGIN UPDATE t SET a = a + 1 WHERE b > 0; END$$ LANGUAGE plpgsql \ No newline at end of file diff --git a/fuzz/fuzz_seeds/postgres_do_language_before.sql b/fuzz/fuzz_seeds/postgres_do_language_before.sql new file mode 100644 index 0000000000..259368c967 --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_do_language_before.sql @@ -0,0 +1 @@ +DO LANGUAGE plpgsql $$DECLARE n int; BEGIN SELECT count(*) INTO n FROM t; END$$ \ No newline at end of file diff --git a/fuzz/fuzz_seeds/postgres_do_single_quoted.sql b/fuzz/fuzz_seeds/postgres_do_single_quoted.sql new file mode 100644 index 0000000000..8165b2478b --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_do_single_quoted.sql @@ -0,0 +1 @@ +DO 'BEGIN RAISE NOTICE ''hi''; END' \ No newline at end of file diff --git a/fuzz/fuzz_seeds/postgres_do_tagged_dollar_quote.sql b/fuzz/fuzz_seeds/postgres_do_tagged_dollar_quote.sql new file mode 100644 index 0000000000..32728aa87e --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_do_tagged_dollar_quote.sql @@ -0,0 +1 @@ +DO $body$BEGIN PERFORM $$nested$$; END$body$ \ No newline at end of file From dc861e13cf136b392db443821de615502e754ca7 Mon Sep 17 00:00:00 2001 From: Devayan Mandal Date: Tue, 13 Oct 2026 14:48:09 +0530 Subject: [PATCH 3/8] PostgreSQL: Make DO statement a named struct --- src/ast/mod.rs | 48 ++++++++++++++++++++++++++++----------- src/ast/spans.rs | 33 ++++++++++++++++++++++----- src/parser/mod.rs | 2 +- tests/sqlparser_common.rs | 4 ++-- 4 files changed, 65 insertions(+), 22 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index e8d74b5c3a..cc1acdc673 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4978,12 +4978,7 @@ pub enum Statement { /// ``` /// /// See Postgres - Do { - /// Optional procedural language name. - language: Option, - /// The anonymous code block, a string literal. - code: ValueWithSpan, - }, + Do(DoStatement), /// ```sql /// NOTIFY channel [ , payload ] /// ``` @@ -6554,13 +6549,7 @@ impl fmt::Display for Statement { write!(f, "UNLISTEN {channel}")?; Ok(()) } - Statement::Do { language, code } => { - write!(f, "DO ")?; - if let Some(language) = language { - write!(f, "LANGUAGE {language} ")?; - } - write!(f, "{code}") - } + Statement::Do(s) => write!(f, "{s}"), Statement::NOTIFY { channel, payload } => { write!(f, "NOTIFY {channel}")?; if let Some(payload) = payload { @@ -11683,6 +11672,33 @@ impl fmt::Display for WaitForStatement { } } +/// PostgreSQL `DO` statement, executing an anonymous code block. +/// +/// ```sql +/// DO [ LANGUAGE lang_name ] code +/// ``` +/// +/// See +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct DoStatement { + /// Optional procedural language name. + pub language: Option, + /// The anonymous code block, a string literal. + pub code: ValueWithSpan, +} + +impl fmt::Display for DoStatement { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "DO ")?; + if let Some(language) = &self.language { + write!(f, "LANGUAGE {language} ")?; + } + write!(f, "{}", self.code) + } +} + /// Represents a `Return` statement. /// /// [MsSql triggers](https://learn.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql) @@ -12535,6 +12551,12 @@ impl From for Statement { } } +impl From for Statement { + fn from(d: DoStatement) -> Self { + Self::Do(d) + } +} + impl From for Statement { fn from(f: Function) -> Self { Self::Call(f) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 36488a3764..6fa9e6540f 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -34,11 +34,11 @@ use super::{ ClusteredIndex, ColumnDef, ColumnOption, ColumnOptionDef, ConditionalStatementBlock, ConditionalStatements, ConflictTarget, ConnectByKind, ConstraintCharacteristics, CopySource, CreateIndex, CreateRule, CreateRuleAction, CreateTable, CreateTableOptions, Cte, Delete, - DoUpdate, ExceptSelectItem, ExcludeConstraintElement, ExcludeSelectItem, Expr, ExprWithAlias, - Fetch, ForValues, FromTable, Function, FunctionArg, FunctionArgExpr, FunctionArgumentClause, - FunctionArgumentList, FunctionArguments, GroupByExpr, HavingBound, IfStatement, - IlikeSelectItem, InTarget, IndexColumn, Insert, Interpolate, InterpolateExpr, Join, - JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, LimitClause, + DoStatement, DoUpdate, ExceptSelectItem, ExcludeConstraintElement, ExcludeSelectItem, Expr, + ExprWithAlias, Fetch, ForValues, FromTable, Function, FunctionArg, FunctionArgExpr, + FunctionArgumentClause, FunctionArgumentList, FunctionArguments, GroupByExpr, HavingBound, + IfStatement, IlikeSelectItem, InTarget, IndexColumn, Insert, Interpolate, InterpolateExpr, + Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, LimitClause, MatchRecognizePattern, Measure, Merge, MergeAction, MergeClause, MergeInsertExpr, MergeInsertKind, MergeUpdateExpr, MergeUpdateKind, NamedParenthesizedList, NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction, @@ -502,7 +502,7 @@ impl Spanned for Statement { Statement::ShowObjects { .. } => Span::empty(), Statement::ShowViews { .. } => Span::empty(), Statement::LISTEN { .. } => Span::empty(), - Statement::Do { .. } => Span::empty(), + Statement::Do(s) => s.span(), Statement::NOTIFY { .. } => Span::empty(), Statement::LoadData { .. } => Span::empty(), Statement::UNLISTEN { .. } => Span::empty(), @@ -2766,6 +2766,18 @@ impl Spanned for comments::CommentWithSpan { } } +impl Spanned for DoStatement { + fn span(&self) -> Span { + let DoStatement { language, code } = self; + union_spans( + language + .iter() + .map(|l| l.span) + .chain(core::iter::once(code.span)), + ) + } +} + #[cfg(test)] pub mod tests { use crate::ast::Value; @@ -3416,4 +3428,13 @@ SETTINGS update_parallel_mode = 'auto'"#; assert_eq!(test.get_source(expr.span()), expected, "{sql}"); } } + + #[test] + fn test_do_statement_span() { + let dialect = &crate::dialect::PostgreSqlDialect {}; + let sql = "DO LANGUAGE plpgsql $$BEGIN NULL; END$$"; + let test = SpanTest::new(dialect, sql); + let r = Parser::parse_sql(dialect, sql).unwrap(); + assert_eq!(test.get_source(r[0].span()), "plpgsql $$BEGIN NULL; END$$"); + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f6b3851ca8..f70f12aa3e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1535,7 +1535,7 @@ impl<'a> Parser<'a> { if language.is_none() { language = self.parse_do_language()?; } - Ok(Statement::Do { language, code }) + Ok(Statement::Do(DoStatement { language, code })) } fn parse_do_language(&mut self) -> Result, ParserError> { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a6f96fb352..fa5d2de45d 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -22105,7 +22105,7 @@ fn parse_do_statement() { let dialects = all_dialects_where(|d| d.supports_do_statement()); match dialects.verified_stmt("DO $$BEGIN RAISE NOTICE 'hi'; END$$") { - Statement::Do { language, code } => { + Statement::Do(DoStatement { language, code }) => { assert_eq!(language, None); assert_eq!( code.value, @@ -22119,7 +22119,7 @@ fn parse_do_statement() { } match dialects.verified_stmt("DO LANGUAGE plpgsql $body$BEGIN NULL; END$body$") { - Statement::Do { language, .. } => { + Statement::Do(DoStatement { language, .. }) => { assert_eq!(language, Some(Ident::new("plpgsql"))); } _ => unreachable!(), From dca2bb7992afcf2ffcee7fb4a6da2157f061e1e5 Mon Sep 17 00:00:00 2001 From: devtechedge Date: Thu, 15 Oct 2026 00:14:17 +0530 Subject: [PATCH 4/8] Accept Unicode escape string as DO code block --- src/parser/mod.rs | 3 ++- tests/sqlparser_common.rs | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f70f12aa3e..3cae71ff52 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1528,7 +1528,8 @@ impl<'a> Parser<'a> { match &self.peek_token_ref().token { Token::SingleQuotedString(_) | Token::DollarQuotedString(_) - | Token::EscapedStringLiteral(_) => {} + | Token::EscapedStringLiteral(_) + | Token::UnicodeStringLiteral(_) => {} _ => return self.expected_ref("string literal", self.peek_token_ref()), } let code = self.parse_value()?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index fa5d2de45d..661bd671e1 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -22126,6 +22126,7 @@ fn parse_do_statement() { } dialects.verified_stmt("DO 'BEGIN NULL; END'"); + dialects.verified_stmt("DO U&'BEGIN NULL; END'"); dialects.one_statement_parses_to( "DO $$BEGIN NULL; END$$ LANGUAGE plpgsql", "DO LANGUAGE plpgsql $$BEGIN NULL; END$$", From 70705a37c62723178dd61a0c64eb8b5ec00d6fca Mon Sep 17 00:00:00 2001 From: devtechedge Date: Thu, 15 Oct 2026 00:20:20 +0530 Subject: [PATCH 5/8] Fix clippy lints (unnecessary_as_slice, needless_range_loop) --- src/ast/spans.rs | 4 +--- tests/sqlparser_common.rs | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 6fa9e6540f..28320bbbec 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1150,9 +1150,7 @@ impl Spanned for CreateTableOptions { match self { CreateTableOptions::None => Span::empty(), CreateTableOptions::With(vec) => union_spans(vec.iter().map(|i| i.span())), - CreateTableOptions::Options(vec) => { - union_spans(vec.as_slice().iter().map(|i| i.span())) - } + CreateTableOptions::Options(vec) => union_spans(vec.iter().map(|i| i.span())), CreateTableOptions::Plain(vec) => union_spans(vec.iter().map(|i| i.span())), CreateTableOptions::TableProperties(vec) => union_spans(vec.iter().map(|i| i.span())), } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 661bd671e1..ce11c577db 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6517,9 +6517,9 @@ fn parse_window_functions() { expr_from_projection(&select.projection[0]) ); - for i in 0..EXPECTED_PROJ_QTY { + for proj in select.projection.iter().take(EXPECTED_PROJ_QTY) { assert!(matches!( - expr_from_projection(&select.projection[i]), + expr_from_projection(proj), Expr::Function(Function { over: Some(WindowType::WindowSpec(WindowSpec { window_name: None, From 6e3e3f39204f2b7da0649091155d73ec5e50f539 Mon Sep 17 00:00:00 2001 From: devtechedge Date: Thu, 15 Oct 2026 22:09:09 +0530 Subject: [PATCH 6/8] PostgreSQL: Parse DO options like dostmt_opt_list Accept any run of code blocks and LANGUAGE items, with the language name as an identifier or any string literal, and keep them in source order so every accepted form round-trips. Missing-code and duplicate checks are left to the server, matching PostgreSQL's grammar. --- src/ast/mod.rs | 37 +++++++++++++++++++++++++---------- src/ast/spans.rs | 21 +++++++++++--------- src/parser/mod.rs | 41 ++++++++++++++++++++------------------- tests/sqlparser_common.rs | 33 ++++++++++++++----------------- 4 files changed, 75 insertions(+), 57 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index cc1acdc673..a66b7f8892 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4974,7 +4974,7 @@ pub enum Statement { channel: Ident, }, /// ```sql - /// DO [ LANGUAGE lang_name ] code + /// DO { code | LANGUAGE lang_name } [ ... ] /// ``` /// /// See Postgres @@ -11675,7 +11675,7 @@ impl fmt::Display for WaitForStatement { /// PostgreSQL `DO` statement, executing an anonymous code block. /// /// ```sql -/// DO [ LANGUAGE lang_name ] code +/// DO { code | LANGUAGE lang_name } [ ... ] /// ``` /// /// See @@ -11683,19 +11683,36 @@ impl fmt::Display for WaitForStatement { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct DoStatement { - /// Optional procedural language name. - pub language: Option, - /// The anonymous code block, a string literal. - pub code: ValueWithSpan, + /// The code blocks and `LANGUAGE` options, in source order. + pub options: Vec, } impl fmt::Display for DoStatement { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "DO ")?; - if let Some(language) = &self.language { - write!(f, "LANGUAGE {language} ")?; + write!(f, "DO {}", display_separated(&self.options, " ")) + } +} + +/// An item of a [`DoStatement`]. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum DoOption { + /// `LANGUAGE lang_name` + Language(Ident), + /// `LANGUAGE` followed by the name as a string literal. + LanguageLiteral(ValueWithSpan), + /// The anonymous code block, a string literal. + Code(ValueWithSpan), +} + +impl fmt::Display for DoOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + DoOption::Language(language) => write!(f, "LANGUAGE {language}"), + DoOption::LanguageLiteral(language) => write!(f, "LANGUAGE {language}"), + DoOption::Code(code) => write!(f, "{code}"), } - write!(f, "{}", self.code) } } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 28320bbbec..17eee3d461 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -34,8 +34,8 @@ use super::{ ClusteredIndex, ColumnDef, ColumnOption, ColumnOptionDef, ConditionalStatementBlock, ConditionalStatements, ConflictTarget, ConnectByKind, ConstraintCharacteristics, CopySource, CreateIndex, CreateRule, CreateRuleAction, CreateTable, CreateTableOptions, Cte, Delete, - DoStatement, DoUpdate, ExceptSelectItem, ExcludeConstraintElement, ExcludeSelectItem, Expr, - ExprWithAlias, Fetch, ForValues, FromTable, Function, FunctionArg, FunctionArgExpr, + DoOption, DoStatement, DoUpdate, ExceptSelectItem, ExcludeConstraintElement, ExcludeSelectItem, + Expr, ExprWithAlias, Fetch, ForValues, FromTable, Function, FunctionArg, FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, FunctionArguments, GroupByExpr, HavingBound, IfStatement, IlikeSelectItem, InTarget, IndexColumn, Insert, Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView, LimitClause, @@ -2766,13 +2766,16 @@ impl Spanned for comments::CommentWithSpan { impl Spanned for DoStatement { fn span(&self) -> Span { - let DoStatement { language, code } = self; - union_spans( - language - .iter() - .map(|l| l.span) - .chain(core::iter::once(code.span)), - ) + union_spans(self.options.iter().map(|option| option.span())) + } +} + +impl Spanned for DoOption { + fn span(&self) -> Span { + match self { + DoOption::Language(language) => language.span, + DoOption::LanguageLiteral(value) | DoOption::Code(value) => value.span, + } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3cae71ff52..396b3dac1c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1524,27 +1524,28 @@ impl<'a> Parser<'a> { /// Parse `DO` statement. pub fn parse_do(&mut self) -> Result { - let mut language = self.parse_do_language()?; - match &self.peek_token_ref().token { - Token::SingleQuotedString(_) - | Token::DollarQuotedString(_) - | Token::EscapedStringLiteral(_) - | Token::UnicodeStringLiteral(_) => {} - _ => return self.expected_ref("string literal", self.peek_token_ref()), - } - let code = self.parse_value()?; - if language.is_none() { - language = self.parse_do_language()?; - } - Ok(Statement::Do(DoStatement { language, code })) - } - - fn parse_do_language(&mut self) -> Result, ParserError> { - if self.parse_keyword(Keyword::LANGUAGE) { - Ok(Some(self.parse_identifier()?)) - } else { - Ok(None) + let mut options = Vec::new(); + loop { + let language = self.parse_keyword(Keyword::LANGUAGE); + let string_literal = matches!( + self.peek_token_ref().token, + Token::SingleQuotedString(_) + | Token::DollarQuotedString(_) + | Token::EscapedStringLiteral(_) + | Token::UnicodeStringLiteral(_) + ); + let option = match (language, string_literal) { + (true, true) => DoOption::LanguageLiteral(self.parse_value()?), + (true, false) => DoOption::Language(self.parse_identifier()?), + (false, true) => DoOption::Code(self.parse_value()?), + (false, false) if options.is_empty() => { + return self.expected_ref("string literal", self.peek_token_ref()); + } + (false, false) => break, + }; + options.push(option); } + Ok(Statement::Do(DoStatement { options })) } /// Parse `NOTIFY` statement. diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ce11c577db..d102e270d5 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -22105,38 +22105,35 @@ fn parse_do_statement() { let dialects = all_dialects_where(|d| d.supports_do_statement()); match dialects.verified_stmt("DO $$BEGIN RAISE NOTICE 'hi'; END$$") { - Statement::Do(DoStatement { language, code }) => { - assert_eq!(language, None); - assert_eq!( - code.value, + Statement::Do(DoStatement { options }) => assert_eq!( + options, + vec![DoOption::Code( Value::DollarQuotedString(DollarQuotedString { value: "BEGIN RAISE NOTICE 'hi'; END".to_string(), tag: None, }) - ); - } + .with_empty_span() + )] + ), _ => unreachable!(), } match dialects.verified_stmt("DO LANGUAGE plpgsql $body$BEGIN NULL; END$body$") { - Statement::Do(DoStatement { language, .. }) => { - assert_eq!(language, Some(Ident::new("plpgsql"))); + Statement::Do(DoStatement { options }) => { + assert_eq!(options[0], DoOption::Language(Ident::new("plpgsql"))); } _ => unreachable!(), } dialects.verified_stmt("DO 'BEGIN NULL; END'"); dialects.verified_stmt("DO U&'BEGIN NULL; END'"); - dialects.one_statement_parses_to( - "DO $$BEGIN NULL; END$$ LANGUAGE plpgsql", - "DO LANGUAGE plpgsql $$BEGIN NULL; END$$", - ); - assert_eq!( - dialects - .parse_sql_statements("DO LANGUAGE plpgsql $$BEGIN NULL; END$$ LANGUAGE sql") - .unwrap_err(), - ParserError::ParserError("Expected: end of statement, found: LANGUAGE".to_string()) - ); + dialects.verified_stmt("DO $$BEGIN NULL; END$$ LANGUAGE plpgsql"); + dialects.verified_stmt("DO LANGUAGE 'plpgsql' $$BEGIN NULL; END$$"); + dialects.verified_stmt("DO LANGUAGE $$plpgsql$$ $$BEGIN NULL; END$$"); + dialects.verified_stmt("DO $$BEGIN NULL; END$$ LANGUAGE E'plpgsql'"); + dialects.verified_stmt("DO LANGUAGE plpgsql"); + dialects.verified_stmt("DO $$BEGIN NULL; END$$ $$BEGIN NULL; END$$"); + dialects.verified_stmt("DO LANGUAGE plpgsql LANGUAGE sql $$BEGIN NULL; END$$"); assert_eq!( dialects.parse_sql_statements("DO 1").unwrap_err(), From d059a678390c34381ccc47568671a66d294c1b1a Mon Sep 17 00:00:00 2001 From: devtechedge Date: Sat, 17 Oct 2026 16:12:49 +0000 Subject: [PATCH 7/8] PostgreSQL: Parse LANGUAGE N string in DO statement --- src/parser/mod.rs | 14 ++++++++++++++ tests/sqlparser_postgres.rs | 10 ++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 396b3dac1c..b4f46506be 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1536,6 +1536,20 @@ impl<'a> Parser<'a> { ); let option = match (language, string_literal) { (true, true) => DoOption::LanguageLiteral(self.parse_value()?), + (true, false) + if matches!(self.peek_token_ref().token, Token::NationalStringLiteral(_)) => + { + // PostgreSQL lexes `N'...'` as the `NCHAR` keyword followed + // by a string literal, so `LANGUAGE N'...'` is the language + // name `nchar` followed by the code block. + let next_token = self.next_token(); + let span = next_token.span; + let Token::NationalStringLiteral(code) = next_token.token else { + unreachable!() + }; + options.push(DoOption::Language(Ident::with_span(span, "nchar"))); + DoOption::Code(Value::SingleQuotedString(code).with_span(span)) + } (true, false) => DoOption::Language(self.parse_identifier()?), (false, true) => DoOption::Code(self.parse_value()?), (false, false) if options.is_empty() => { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index da83d4f71a..373a17155d 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4464,6 +4464,16 @@ fn parse_xml_typed_string() { } } +#[test] +fn parse_do_statement() { + let dialects = pg(); + dialects.verified_stmt("DO LANGUAGE plpgsql LANGUAGE sql $$BEGIN NULL; END$$"); + dialects.one_statement_parses_to( + "DO LANGUAGE N'BEGIN NULL; END'", + "DO LANGUAGE nchar 'BEGIN NULL; END'", + ); +} + fn pg() -> TestedDialects { TestedDialects::new(vec![Box::new(PostgreSqlDialect {})]) } From bbf088f9d09353af45e9c9b7b4c9beb469bf9d7e Mon Sep 17 00:00:00 2001 From: devtechedge Date: Sun, 18 Oct 2026 09:01:06 +0530 Subject: [PATCH 8/8] PostgreSQL: Avoid unreachable in DO LANGUAGE parsing --- src/parser/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b4f46506be..bfaaeaa6cb 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1545,7 +1545,7 @@ impl<'a> Parser<'a> { let next_token = self.next_token(); let span = next_token.span; let Token::NationalStringLiteral(code) = next_token.token else { - unreachable!() + return self.expected("national string literal", next_token); }; options.push(DoOption::Language(Ident::with_span(span, "nchar"))); DoOption::Code(Value::SingleQuotedString(code).with_span(span))