From 3b30793044961ec67e969bdafbd07fb9a1e8ab4d 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 eb5bc6cc75..8eff9d8a92 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4937,6 +4937,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 @@ -6519,6 +6530,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 8267c71798..92822cc522 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -498,6 +498,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 a2920e61eb..1e468d22f9 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1347,6 +1347,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 c060f2a8c1..b0bec172a7 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -398,6 +398,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 276b252ed2..87d9a709e3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -736,6 +736,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 => { @@ -1525,6 +1526,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 8f665b25c2..a03b14c753 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -21084,3 +21084,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 161a0181f859a29649e92d27686d8d7c53bdeeaa 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 d988421d5ad38b526054030a7b92aab5f60ade0b 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 | 45 ++++++++++++++++++++++++++---------- src/parser/mod.rs | 2 +- tests/sqlparser_common.rs | 4 ++-- 4 files changed, 71 insertions(+), 28 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8eff9d8a92..12fe2a0a6e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4941,12 +4941,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 ] /// ``` @@ -6530,13 +6525,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 { @@ -11604,6 +11593,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) @@ -12456,6 +12472,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 92822cc522..661d7fe616 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -33,17 +33,17 @@ use super::{ AttachedToken, BeginEndStatements, CaseStatement, CloseCursor, ClusteredIndex, ColumnDef, ColumnOption, ColumnOptionDef, ConditionalStatementBlock, ConditionalStatements, ConflictTarget, ConnectByKind, ConstraintCharacteristics, CopySource, CreateIndex, 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, MatchRecognizePattern, Measure, Merge, MergeAction, MergeClause, MergeInsertExpr, - MergeInsertKind, MergeUpdateExpr, MergeUpdateKind, NamedParenthesizedList, - NamedWindowDefinition, ObjectName, ObjectNamePart, Offset, OnConflict, OnConflictAction, - OnInsert, OpenStatement, OrderBy, OrderByExpr, OrderByKind, OutputClause, Parens, Partition, - PartitionBoundValue, PivotValueSource, ProjectionSelect, Query, RaiseStatement, - RaiseStatementValue, ReferentialAction, RenameSelectItem, ReplaceSelectElement, + CreateTableOptions, Cte, Delete, 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, OnInsert, OpenStatement, OrderBy, OrderByExpr, OrderByKind, OutputClause, + Parens, Partition, PartitionBoundValue, PivotValueSource, ProjectionSelect, Query, + RaiseStatement, RaiseStatementValue, ReferentialAction, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, Setting, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint, TableFactor, TableObject, TableOptionsClustered, TableWithJoins, Update, UpdateTableFromKind, Use, Values, @@ -498,7 +498,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(), @@ -2650,6 +2650,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; @@ -3259,4 +3271,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 87d9a709e3..6fa52f334a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1539,7 +1539,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 a03b14c753..bfdf47201e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -21090,7 +21090,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, @@ -21104,7 +21104,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 343a389d1f8e4dcbb6a18287a5651ba960b88c04 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 6fa52f334a..b5f5ae2ee9 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1532,7 +1532,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 bfdf47201e..62371369a2 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -21111,6 +21111,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 a59c7ed60cc366e78d90c54b8f10f9bf11e3d89a 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 661d7fe616..765f63efee 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1106,9 +1106,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 62371369a2..516eee0054 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6090,9 +6090,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 460b6ebf1c8e066a51917a766635183e335e628e 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 | 19 ++++++++++-------- src/parser/mod.rs | 41 ++++++++++++++++++++------------------- tests/sqlparser_common.rs | 33 ++++++++++++++----------------- 4 files changed, 74 insertions(+), 56 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 12fe2a0a6e..31c4a0298a 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4937,7 +4937,7 @@ pub enum Statement { channel: Ident, }, /// ```sql - /// DO [ LANGUAGE lang_name ] code + /// DO { code | LANGUAGE lang_name } [ ... ] /// ``` /// /// See Postgres @@ -11596,7 +11596,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 @@ -11604,19 +11604,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 765f63efee..c516c979d4 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -33,7 +33,7 @@ use super::{ AttachedToken, BeginEndStatements, CaseStatement, CloseCursor, ClusteredIndex, ColumnDef, ColumnOption, ColumnOptionDef, ConditionalStatementBlock, ConditionalStatements, ConflictTarget, ConnectByKind, ConstraintCharacteristics, CopySource, CreateIndex, CreateTable, - CreateTableOptions, Cte, Delete, DoStatement, DoUpdate, ExceptSelectItem, + CreateTableOptions, Cte, Delete, DoOption, DoStatement, DoUpdate, ExceptSelectItem, ExcludeConstraintElement, ExcludeSelectItem, Expr, ExprWithAlias, Fetch, ForValues, FromTable, Function, FunctionArg, FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList, FunctionArguments, GroupByExpr, HavingBound, IfStatement, IlikeSelectItem, InTarget, @@ -2650,13 +2650,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 b5f5ae2ee9..1ea681511c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1528,27 +1528,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 516eee0054..ffb9034749 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -21090,38 +21090,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 ec63cb723b21d9f711863b7e59b72eb092f3f5c9 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 1ea681511c..3a955c8720 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1540,6 +1540,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 0a3866d29f..275c1de47e 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4176,6 +4176,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 84477aafcef38e8115eb04705aa73bd919971a38 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 3a955c8720..1a8a30b140 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1549,7 +1549,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))