From 7d6c0365aafffc8c614f3896a167d65e2520238a Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Wed, 28 Oct 2026 08:45:01 +0100 Subject: [PATCH 1/2] PostgreSQL: Support ALTER SERVER --- fuzz/fuzz_seeds/postgres_alter_server.sql | 2 + src/ast/ddl.rs | 73 +++++++++++++++++++++++ src/ast/mod.rs | 38 ++++++++---- src/ast/spans.rs | 62 ++++++++++++------- src/parser/mod.rs | 37 +++++++++++- tests/sqlparser_common.rs | 2 +- tests/sqlparser_postgres.rs | 70 ++++++++++++++++++++++ 7 files changed, 247 insertions(+), 37 deletions(-) create mode 100644 fuzz/fuzz_seeds/postgres_alter_server.sql diff --git a/fuzz/fuzz_seeds/postgres_alter_server.sql b/fuzz/fuzz_seeds/postgres_alter_server.sql new file mode 100644 index 0000000000..47054726c6 --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_alter_server.sql @@ -0,0 +1,2 @@ +ALTER SERVER s VERSION NULL OPTIONS (ADD host 'db', SET port '5432', DROP dbname); +ALTER SERVER s OWNER TO postgres \ No newline at end of file diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 51ac7e8bae..107c389ffb 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -5039,6 +5039,73 @@ impl fmt::Display for AlterViewOperation { } } +/// `ALTER SERVER` statement. +/// +/// [PostgreSQL](https://www.postgresql.org/docs/current/sql-alterserver.html) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct AlterServer { + /// Name of the server being altered. + pub name: Ident, + /// The operation to perform on the server. + pub operation: AlterServerOperation, +} + +/// Operations supported by `ALTER SERVER`. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterServerOperation { + /// Change the server version, its options, or both. + /// + /// ```sql + /// ALTER SERVER name [ VERSION 'new_version' ] [ OPTIONS ( [ ADD | SET | DROP ] option ['value'] [, ...] ) ] + /// ``` + Update { + /// The new version, `NULL` included. + version: Option, + /// The option changes, in source order. + options: Option>, + }, + /// Change the server owner. + /// + /// ```sql + /// ALTER SERVER name OWNER TO role_name + /// ``` + OwnerTo(Owner), + /// Rename the server. + /// + /// ```sql + /// ALTER SERVER name RENAME TO new_name + /// ``` + RenameTo { + /// New server name. + new_name: Ident, + }, +} + +impl fmt::Display for AlterServerOperation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AlterServerOperation::Update { version, options } => { + if let Some(version) = version { + write!(f, "VERSION {version}")?; + if options.is_some() { + f.write_str(" ")?; + } + } + if let Some(options) = options { + write!(f, "OPTIONS ({})", display_comma_separated(options))?; + } + Ok(()) + } + AlterServerOperation::OwnerTo(owner) => write!(f, "OWNER TO {owner}"), + AlterServerOperation::RenameTo { new_name } => write!(f, "RENAME TO {new_name}"), + } + } +} + impl fmt::Display for AlterView { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "ALTER VIEW ")?; @@ -5049,6 +5116,12 @@ impl fmt::Display for AlterView { } } +impl fmt::Display for AlterServer { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "ALTER SERVER {} {}", self.name, self.operation) + } +} + /// Table type for ALTER TABLE statements. /// Used to distinguish between regular tables, Iceberg tables, and Dynamic tables. #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 6640bf7bfb..b639c4441d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -69,19 +69,19 @@ pub use self::ddl::{ AlterFunctionOperation, AlterGenericOption, AlterIndexOperation, AlterOperator, AlterOperatorClass, AlterOperatorClassOperation, AlterOperatorFamily, AlterOperatorFamilyOperation, AlterOperatorOperation, AlterPolicy, AlterPolicyOperation, - AlterSchema, AlterSchemaOperation, AlterTable, AlterTableAlgorithm, AlterTableLock, - AlterTableOperation, AlterTableType, AlterTextSearch, AlterTextSearchOperation, - AlterTextSearchOption, AlterTrigger, AlterTriggerOperation, AlterType, AlterTypeAddValue, - AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue, - AlterView, AlterViewOperation, ClusteredBy, ColumnDef, ColumnOption, ColumnOptionDef, - ColumnOptions, ColumnPolicy, ColumnPolicyProperty, ConstraintCharacteristics, CreateAggregate, - CreateAggregateArgs, CreateAggregateOption, CreateCollation, CreateCollationDefinition, - CreateConnector, CreateDomain, CreateExtension, CreateForeignDataWrapper, CreateForeignTable, - CreateFunction, CreateIndex, CreateOperator, CreateOperatorClass, CreateOperatorFamily, - CreatePolicy, CreatePolicyCommand, CreatePolicyType, CreateRule, CreateRuleAction, - CreateRuleDoKind, CreateRuleEvent, CreateTable, CreateTextSearch, CreateTrigger, - CreateUserMapping, CreateView, Deduplicate, DeferrableInitial, DistStyle, DropBehavior, - DropExtension, DropFunction, DropOperator, DropOperatorClass, DropOperatorFamily, + AlterSchema, AlterSchemaOperation, AlterServer, AlterServerOperation, AlterTable, + AlterTableAlgorithm, AlterTableLock, AlterTableOperation, AlterTableType, AlterTextSearch, + AlterTextSearchOperation, AlterTextSearchOption, AlterTrigger, AlterTriggerOperation, + AlterType, AlterTypeAddValue, AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, + AlterTypeRenameValue, AlterView, AlterViewOperation, ClusteredBy, ColumnDef, ColumnOption, + ColumnOptionDef, ColumnOptions, ColumnPolicy, ColumnPolicyProperty, ConstraintCharacteristics, + CreateAggregate, CreateAggregateArgs, CreateAggregateOption, CreateCollation, + CreateCollationDefinition, CreateConnector, CreateDomain, CreateExtension, + CreateForeignDataWrapper, CreateForeignTable, CreateFunction, CreateIndex, CreateOperator, + CreateOperatorClass, CreateOperatorFamily, CreatePolicy, CreatePolicyCommand, CreatePolicyType, + CreateRule, CreateRuleAction, CreateRuleDoKind, CreateRuleEvent, CreateTable, CreateTextSearch, + CreateTrigger, CreateUserMapping, CreateView, Deduplicate, DeferrableInitial, DistStyle, + DropBehavior, DropExtension, DropFunction, DropOperator, DropOperatorClass, DropOperatorFamily, DropOperatorSignature, DropPolicy, DropRule, DropTrigger, ForValues, ForeignDataWrapperRoutineClause, FunctionReturnType, GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, @@ -3943,6 +3943,11 @@ pub enum Statement { /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-altercollation.html) AlterCollation(AlterCollation), /// ```sql + /// ALTER SERVER + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-alterserver.html) + AlterServer(AlterServer), + /// ```sql /// ALTER OPERATOR /// ``` /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-alteroperator.html) @@ -5779,6 +5784,7 @@ impl fmt::Display for Statement { write!(f, "ALTER TYPE {name} {operation}") } Statement::AlterCollation(alter_collation) => write!(f, "{alter_collation}"), + Statement::AlterServer(alter_server) => write!(f, "{alter_server}"), Statement::AlterOperator(alter_operator) => write!(f, "{alter_operator}"), Statement::AlterOperatorFamily(alter_operator_family) => { write!(f, "{alter_operator_family}") @@ -12713,6 +12719,12 @@ impl From for Statement { } } +impl From for Statement { + fn from(a: AlterServer) -> Self { + Self::AlterServer(a) + } +} + impl From for Statement { fn from(a: AlterOperator) -> Self { Self::AlterOperator(a) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index e8d47b1e7e..c689c91ed2 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -29,28 +29,28 @@ use crate::tokenizer::Span; use super::{ comments, dcl::SecondaryRoles, value::ValueWithSpan, AccessExpr, AlterColumnOperation, - AlterGenericOption, AlterIndexOperation, AlterTableOperation, AlterTrigger, - AlterTriggerOperation, AlterView, AlterViewOperation, Analyze, Array, Assignment, - AssignmentTarget, AttachedToken, BeginEndStatements, CaseStatement, CloseCursor, - 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, - 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, + AlterGenericOption, AlterIndexOperation, AlterServer, AlterServerOperation, + AlterTableOperation, AlterTrigger, AlterTriggerOperation, AlterView, AlterViewOperation, + Analyze, Array, Assignment, AssignmentTarget, AttachedToken, BeginEndStatements, CaseStatement, + CloseCursor, 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, 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. @@ -416,6 +416,7 @@ impl Spanned for Statement { Statement::AlterFunction { .. } => Span::empty(), Statement::AlterType { .. } => Span::empty(), Statement::AlterCollation { .. } => Span::empty(), + Statement::AlterServer(alter_server) => alter_server.span(), Statement::AlterOperator { .. } => Span::empty(), Statement::AlterOperatorFamily { .. } => Span::empty(), Statement::AlterOperatorClass { .. } => Span::empty(), @@ -2868,6 +2869,23 @@ impl Spanned for CreateUserMapping { } } +impl Spanned for AlterServer { + fn span(&self) -> Span { + let operation = match &self.operation { + AlterServerOperation::Update { version, options } => Span::union_iter( + version + .iter() + .map(|version| version.span) + .chain(options.iter().flatten().map(|option| option.span())), + ), + AlterServerOperation::OwnerTo(Owner::Ident(owner)) => owner.span, + AlterServerOperation::OwnerTo(_) => Span::empty(), + AlterServerOperation::RenameTo { new_name } => new_name.span, + }; + self.name.span.union(&operation) + } +} + #[cfg(test)] pub mod tests { use crate::ast::Value; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 536e753087..067f3d47d2 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11941,6 +11941,7 @@ impl<'a> Parser<'a> { Keyword::POLICY => self.parse_alter_policy().map(Into::into), Keyword::TRIGGER => self.parse_alter_trigger().map(Into::into), Keyword::CONNECTOR => self.parse_alter_connector(), + Keyword::SERVER => self.parse_alter_server().map(Into::into), Keyword::USER if self.dialect.supports_alter_user_as_alter_role() => { self.parse_alter_role() } @@ -11948,7 +11949,7 @@ impl<'a> Parser<'a> { _ => { self.index = index; self.expected_ref( - "one of VIEW or TYPE or COLLATION or TABLE or INDEX or FUNCTION or AGGREGATE or ROLE or POLICY or TRIGGER or CONNECTOR or ICEBERG or FOREIGN TABLE or SCHEMA or USER or OPERATOR", + "one of VIEW or TYPE or COLLATION or TABLE or INDEX or FUNCTION or AGGREGATE or ROLE or POLICY or TRIGGER or CONNECTOR or ICEBERG or FOREIGN TABLE or SCHEMA or SERVER or USER or OPERATOR", self.peek_token_ref(), ) } @@ -12380,6 +12381,40 @@ impl<'a> Parser<'a> { Ok(AlterCollation { name, operation }) } + /// Parse a [Statement::AlterServer]. + /// + /// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-alterserver.html) + pub fn parse_alter_server(&mut self) -> Result { + let name = self.parse_identifier()?; + let operation = if self.parse_keywords(&[Keyword::RENAME, Keyword::TO]) { + AlterServerOperation::RenameTo { + new_name: self.parse_identifier()?, + } + } else if self.parse_keywords(&[Keyword::OWNER, Keyword::TO]) { + AlterServerOperation::OwnerTo(self.parse_owner()?) + } else { + let version = if self.parse_keyword(Keyword::VERSION) { + Some(self.parse_value()?) + } else { + None + }; + let options = if self.parse_keyword(Keyword::OPTIONS) { + Some(self.parse_alter_generic_options()?) + } else { + None + }; + if version.is_none() && options.is_none() { + return self.expected_ref( + "VERSION, OPTIONS, OWNER TO, or RENAME TO after ALTER SERVER", + self.peek_token_ref(), + ); + } + AlterServerOperation::Update { version, options } + }; + + Ok(AlterServer { name, operation }) + } + /// Parse a [Statement::AlterOperator] /// /// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-alteroperator.html) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 16d1dcc002..b937329e57 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -16193,7 +16193,7 @@ fn parse_alter_trigger_spans() { #[test] fn parse_alter_rejects_missing_or_unknown_object_type() { - let objects = "one of VIEW or TYPE or COLLATION or TABLE or INDEX or FUNCTION or AGGREGATE or ROLE or POLICY or TRIGGER or CONNECTOR or ICEBERG or FOREIGN TABLE or SCHEMA or USER or OPERATOR"; + let objects = "one of VIEW or TYPE or COLLATION or TABLE or INDEX or FUNCTION or AGGREGATE or ROLE or POLICY or TRIGGER or CONNECTOR or ICEBERG or FOREIGN TABLE or SCHEMA or SERVER or USER or OPERATOR"; for (sql, found) in [ ("ALTER", "EOF"), ("ALTER 1", "1"), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 26edced406..92ab166802 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -12773,3 +12773,73 @@ fn parse_alter_foreign_table_rejects_clauses() { ); } } + +#[test] +fn parse_alter_server() { + let sql = "ALTER SERVER s VERSION NULL OPTIONS (ADD host 'db', SET port '5432', DROP dbname)"; + let Statement::AlterServer(alter) = pg_and_generic().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(alter.name, Ident::new("s")); + let AlterServerOperation::Update { version, options } = alter.operation else { + unreachable!() + }; + assert_eq!(version.map(|version| version.value), Some(Value::Null)); + assert_eq!( + options, + Some(vec![ + AlterGenericOption::Add { + option: CreateServerOption { + key: Ident::new("host"), + value: Ident::with_quote('\'', "db"), + }, + add_keyword: true, + }, + AlterGenericOption::Set(CreateServerOption { + key: Ident::new("port"), + value: Ident::with_quote('\'', "5432"), + }), + AlterGenericOption::Drop(Ident::new("dbname")), + ]) + ); + + assert_eq!( + pg_and_generic().verified_stmt("ALTER SERVER s OWNER TO postgres"), + Statement::AlterServer(AlterServer { + name: Ident::new("s"), + operation: AlterServerOperation::OwnerTo(Owner::Ident(Ident::new("postgres"))), + }) + ); + for sql in [ + "ALTER SERVER s VERSION '2.0'", + "ALTER SERVER s OPTIONS (host 'db')", + "ALTER SERVER s OWNER TO CURRENT_USER", + "ALTER SERVER s RENAME TO s2", + ] { + pg_and_generic().verified_stmt(sql); + } + + for (sql, expected) in [ + ( + "ALTER SERVER s", + "Expected: VERSION, OPTIONS, OWNER TO, or RENAME TO after ALTER SERVER, found: EOF", + ), + ( + "ALTER SERVER s OPTIONS (port '1') VERSION '4'", + "Expected: end of statement, found: VERSION", + ), + ( + "ALTER SERVER public.s OWNER TO app", + "Expected: VERSION, OPTIONS, OWNER TO, or RENAME TO after ALTER SERVER, found: .", + ), + ] { + assert_eq!( + pg_and_generic() + .parse_sql_statements(sql) + .unwrap_err() + .to_string(), + format!("sql parser error: {expected}"), + "{sql}" + ); + } +} From 695a10bcfef20c3aabb7b9855cf5bdab3cad1415 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Wed, 28 Oct 2026 09:54:22 +0100 Subject: [PATCH 2/2] Test error paths and formatter errors --- tests/sqlparser_postgres.rs | 42 +++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 92ab166802..63928cc4e8 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -12832,6 +12832,17 @@ fn parse_alter_server() { "ALTER SERVER public.s OWNER TO app", "Expected: VERSION, OPTIONS, OWNER TO, or RENAME TO after ALTER SERVER, found: .", ), + ("ALTER SERVER", "Expected: identifier, found: EOF"), + ("ALTER SERVER s RENAME TO", "Expected: identifier, found: EOF"), + ( + "ALTER SERVER s OWNER TO", + "Expected: CURRENT_USER, CURRENT_ROLE, SESSION_USER or identifier after OWNER TO. sql parser error: Expected: identifier, found: EOF", + ), + ("ALTER SERVER s VERSION", "Expected: a value, found: EOF"), + ( + "ALTER SERVER s OPTIONS (ADD host)", + "Expected: identifier, found: )", + ), ] { assert_eq!( pg_and_generic() @@ -12843,3 +12854,34 @@ fn parse_alter_server() { ); } } + +#[test] +fn display_alter_server_propagates_formatter_errors() { + use core::fmt::{self, Write}; + + struct RejectingWriter(&'static str); + + impl Write for RejectingWriter { + fn write_str(&mut self, value: &str) -> fmt::Result { + if value == self.0 { + Err(fmt::Error) + } else { + Ok(()) + } + } + } + + let Statement::AlterServer(alter) = + pg_and_generic().verified_stmt("ALTER SERVER s VERSION '1' OPTIONS (ADD host 'db')") + else { + unreachable!() + }; + for rejected in ["VERSION ", " ", "OPTIONS ("] { + let mut writer = RejectingWriter(rejected); + assert_eq!( + write!(writer, "{}", alter.operation), + Err(fmt::Error), + "{rejected:?}" + ); + } +}