From 4fbedc6d409966e8898eb6b713280c180982d5d5 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 8e798524c9..d3c6bc639f 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4941,6 +4941,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 @@ -6507,6 +6518,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 7638d7d6c0..99aba463ee 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 c867b76f61..63c3a431d1 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1404,6 +1404,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 9e5257b30f..fd153500d3 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 3efb22a349..a057de61a8 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 => { @@ -1526,6 +1527,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 f71869e820..b212b8cc97 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -21529,3 +21529,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 4d5d658bba74f7442dbd2b9c8d3ac307436ce0ae 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 cd1c09a8e2990e66869b17ba53fc5251961aec81 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 | 54 +++++++++++++++++++++++++++------------ src/parser/mod.rs | 2 +- tests/sqlparser_common.rs | 4 +-- 4 files changed, 76 insertions(+), 32 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d3c6bc639f..d2ad07ee6b 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4945,12 +4945,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 ] /// ``` @@ -6518,13 +6513,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 { @@ -11647,6 +11636,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) @@ -12499,6 +12515,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 99aba463ee..ee31a66635 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -33,21 +33,22 @@ 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, OperateFunctionArg, 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, - ViewColumnDef, WhileStatement, WildcardAdditionalOptions, With, WithFill, + 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, OperateFunctionArg, 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, ViewColumnDef, WhileStatement, + WildcardAdditionalOptions, With, WithFill, }; /// Given an iterator of spans, return the [Span::union] of all spans. @@ -498,7 +499,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(), @@ -2718,6 +2719,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; @@ -3352,4 +3365,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 a057de61a8..2750370796 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1540,7 +1540,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 b212b8cc97..3ca0bcd662 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -21535,7 +21535,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, @@ -21549,7 +21549,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 df5b41a1fc634725867e78cbaee9ce125bc29e85 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 2750370796..0091a53339 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1533,7 +1533,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 3ca0bcd662..f7204ae9f7 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -21556,6 +21556,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 dd0eed77df8ec9d0c4f72370cc072f7b6198d159 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 ee31a66635..12cee0c00d 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1108,9 +1108,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 f7204ae9f7..be997a7bf0 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6331,9 +6331,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 2006e9bfd699c74e5f9d05e8a678b4040317550a 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 d2ad07ee6b..33653d7ae9 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4941,7 +4941,7 @@ pub enum Statement { channel: Ident, }, /// ```sql - /// DO [ LANGUAGE lang_name ] code + /// DO { code | LANGUAGE lang_name } [ ... ] /// ``` /// /// See Postgres @@ -11639,7 +11639,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 @@ -11647,19 +11647,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 12cee0c00d..8b22fedc96 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, @@ -2719,13 +2719,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 0091a53339..87a8e99447 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1529,27 +1529,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 be997a7bf0..caeed3e007 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -21535,38 +21535,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 e2096418db0fcbb4a3873b16e531a72ff312c902 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 87a8e99447..95c5f27958 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1541,6 +1541,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 aa9252125b..5421ebaf9a 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 36210f50966a08725f72802882eec5f11515d510 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 95c5f27958..5aee7a684d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1550,7 +1550,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))