From 77d927714d588e9e43b57dd819cbe6aacfefb5fa Mon Sep 17 00:00:00 2001 From: Devayan Mandal Date: Tue, 13 Oct 2026 14:48:09 +0530 Subject: [PATCH 1/9] 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 ecfee10939..e1d7871c85 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -5003,6 +5003,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 @@ -6573,6 +6584,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 d8e2b6e149..b9af378467 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -501,6 +501,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 dce6fa306b..b5e65446ff 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1505,6 +1505,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 35e82f8a87..f890bad180 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -418,6 +418,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 066adb3b70..aec0fb55a3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -757,6 +757,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 => { @@ -1546,6 +1547,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 0882b95c8a..811d1ec0bb 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -23414,3 +23414,58 @@ fn display_create_procedure_begin_atomic_propagates_write_errors() { assert_eq!(write!(writer, "{stmt}"), Err(fmt::Error), "{rejected:?}"); } } + +#[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 635cdefd2c27a14eafb3cc1babd13240eba6944d Mon Sep 17 00:00:00 2001 From: Devayan Mandal Date: Tue, 13 Oct 2026 14:48:09 +0530 Subject: [PATCH 2/9] 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 eb06302953ac7db8b538cfcde5f1334c294807a5 Mon Sep 17 00:00:00 2001 From: Devayan Mandal Date: Tue, 13 Oct 2026 14:48:09 +0530 Subject: [PATCH 3/9] PostgreSQL: Make DO statement a named struct --- src/ast/mod.rs | 48 ++++++++++++++++++++++++++++----------- src/ast/spans.rs | 28 ++++++++++++++++++++--- src/parser/mod.rs | 2 +- tests/sqlparser_common.rs | 4 ++-- 4 files changed, 63 insertions(+), 19 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index e1d7871c85..b2413fa0de 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -5007,12 +5007,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 ] /// ``` @@ -6584,13 +6579,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 { @@ -11752,6 +11741,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) @@ -12635,6 +12651,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 b9af378467..1995508df9 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -35,8 +35,9 @@ use super::{ ClusteredIndex, ColumnDef, ColumnOption, ColumnOptionDef, ConditionalStatementBlock, ConditionalStatements, ConflictTarget, ConnectByKind, ConstraintCharacteristics, CopySource, CreateIndex, CreateRule, CreateRuleAction, CreateTable, CreateTableOptions, CreateUserMapping, - Cte, Delete, DoUpdate, DropRule, ExceptSelectItem, ExcludeConstraintElement, ExcludeSelectItem, - Expr, ExprWithAlias, Fetch, ForValues, FromTable, Function, FunctionArg, FunctionArgExpr, + Cte, Delete, DoStatement, DoUpdate, DropRule, 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, @@ -501,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(), @@ -2869,6 +2870,18 @@ impl Spanned for CreateUserMapping { } } +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; @@ -3595,4 +3608,13 @@ SETTINGS update_parallel_mode = 'auto'"#; assert_eq!(test.get_source(stmt.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 aec0fb55a3..1e251c3796 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1560,7 +1560,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 811d1ec0bb..0b662a1728 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -23420,7 +23420,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, @@ -23434,7 +23434,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 34d4014463f968c7b2892cad9ac394baa5db1a04 Mon Sep 17 00:00:00 2001 From: devtechedge Date: Thu, 15 Oct 2026 00:14:17 +0530 Subject: [PATCH 4/9] 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 1e251c3796..3903befb90 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1553,7 +1553,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 0b662a1728..87a416a975 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -23441,6 +23441,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 ada06b4f89836bb82304748f5130555d9e4d36e3 Mon Sep 17 00:00:00 2001 From: devtechedge Date: Thu, 15 Oct 2026 22:09:09 +0530 Subject: [PATCH 5/9] 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 | 50 ++++++++++++++++++++------------------- src/parser/mod.rs | 41 ++++++++++++++++---------------- tests/sqlparser_common.rs | 33 ++++++++++++-------------- 4 files changed, 89 insertions(+), 72 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index b2413fa0de..438bfac34e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -5003,7 +5003,7 @@ pub enum Statement { channel: Ident, }, /// ```sql - /// DO [ LANGUAGE lang_name ] code + /// DO { code | LANGUAGE lang_name } [ ... ] /// ``` /// /// See Postgres @@ -11744,7 +11744,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 @@ -11752,19 +11752,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 1995508df9..c01a2aace1 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -35,23 +35,22 @@ use super::{ ClusteredIndex, ColumnDef, ColumnOption, ColumnOptionDef, ConditionalStatementBlock, ConditionalStatements, ConflictTarget, ConnectByKind, ConstraintCharacteristics, CopySource, CreateIndex, CreateRule, CreateRuleAction, CreateTable, CreateTableOptions, CreateUserMapping, - Cte, Delete, DoStatement, DoUpdate, DropRule, 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, RefreshMaterializedView, - RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, - SetExpr, Setting, SqlOption, Statement, Subscript, SymbolDefinition, TableAlias, - TableAliasColumnDef, TableConstraint, TableFactor, TableObject, TableOptionsClustered, - TableWithJoins, Update, UpdateTableFromKind, Use, UserMappingUser, Values, ViewColumnDef, - WhileStatement, WildcardAdditionalOptions, With, WithFill, + Cte, Delete, DoOption, DoStatement, DoUpdate, DropRule, 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, + RefreshMaterializedView, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, + SelectInto, SelectItem, SetExpr, Setting, SqlOption, Statement, Subscript, SymbolDefinition, + TableAlias, TableAliasColumnDef, TableConstraint, TableFactor, TableObject, + TableOptionsClustered, TableWithJoins, Update, UpdateTableFromKind, Use, UserMappingUser, + Values, ViewColumnDef, WhileStatement, WildcardAdditionalOptions, With, WithFill, }; /// Given an iterator of spans, return the [Span::union] of all spans. @@ -2872,13 +2871,16 @@ impl Spanned for CreateUserMapping { 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 3903befb90..f04c659ac0 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1549,27 +1549,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 87a416a975..b40fe5ca35 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -23420,38 +23420,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 c51190db95e3380851c389d0b8df8df67ea7dd60 Mon Sep 17 00:00:00 2001 From: devtechedge Date: Sat, 17 Oct 2026 16:12:49 +0000 Subject: [PATCH 6/9] 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 f04c659ac0..9ba7e1e9fd 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1561,6 +1561,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 68c5096c56..6f640e030d 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -4553,6 +4553,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 bb6e1aa27ed1ef0b35e18458b43f2d27118f3f0a Mon Sep 17 00:00:00 2001 From: devtechedge Date: Sun, 18 Oct 2026 09:01:06 +0530 Subject: [PATCH 7/9] PostgreSQL: Avoid unreachable in DO LANGUAGE parsing --- src/parser/mod.rs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9ba7e1e9fd..31a6b50bcf 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1561,21 +1561,22 @@ 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(_)) => - { + (true, false) => match self.next_token() { // 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()?), + TokenWithSpan { + token: Token::NationalStringLiteral(code), + span, + } => { + options.push(DoOption::Language(Ident::with_span(span, "nchar"))); + DoOption::Code(Value::SingleQuotedString(code).with_span(span)) + } + _ => { + self.prev_token(); + 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()); From b4518cfb0be5ef4a4c0b24a120617245f7062965 Mon Sep 17 00:00:00 2001 From: devtechedge Date: Tue, 3 Nov 2026 11:38:13 +0530 Subject: [PATCH 8/9] PostgreSQL: Cover more DO statement forms Add the forms that PostgreSQL also accepts for a Unicode escape string around a LANGUAGE clause and for a LANGUAGE name given as a string literal. --- tests/sqlparser_common.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index b40fe5ca35..8e3b6f8c63 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -23442,6 +23442,9 @@ fn parse_do_statement() { dialects.verified_stmt("DO 'BEGIN NULL; END'"); dialects.verified_stmt("DO U&'BEGIN NULL; END'"); + dialects.verified_stmt("DO U&'BEGIN NULL; END' LANGUAGE plpgsql"); + dialects.verified_stmt("DO LANGUAGE 'plpgsql'"); + dialects.verified_stmt("DO LANGUAGE plpgsql U&'BEGIN NULL; END'"); 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$$"); From 006a559270091e32fbbfcf82aa38f2664abceab5 Mon Sep 17 00:00:00 2001 From: devtechedge Date: Wed, 4 Nov 2026 07:41:50 +0530 Subject: [PATCH 9/9] PostgreSQL: Split the N string span in DO statement Share the token span between the nchar language name and the code block it produces, so get_source returns N and the literal separately. Cover the split in the DO span test. --- src/ast/spans.rs | 9 +++++++++ src/parser/mod.rs | 7 +++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index c01a2aace1..4b625f92b7 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -3618,5 +3618,14 @@ SETTINGS update_parallel_mode = 'auto'"#; 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$$"); + + let sql = "DO LANGUAGE N'BEGIN NULL; END'"; + let test = SpanTest::new(dialect, sql); + let options = match Parser::parse_sql(dialect, sql).unwrap().remove(0) { + Statement::Do(DoStatement { options }) => options, + stmt => panic!("expected DO, got {stmt:?}"), + }; + assert_eq!(test.get_source(options[0].span()), "N"); + assert_eq!(test.get_source(options[1].span()), "'BEGIN NULL; END'"); } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 31a6b50bcf..2b11a77a6e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1569,8 +1569,11 @@ impl<'a> Parser<'a> { token: Token::NationalStringLiteral(code), span, } => { - options.push(DoOption::Language(Ident::with_span(span, "nchar"))); - DoOption::Code(Value::SingleQuotedString(code).with_span(span)) + let quote = Location::new(span.start.line, span.start.column + 1); + let language = Ident::with_span(Span::new(span.start, quote), "nchar"); + options.push(DoOption::Language(language)); + let code = Value::SingleQuotedString(code); + DoOption::Code(code.with_span(Span::new(quote, span.end))) } _ => { self.prev_token();