From 2a94926998fdc4ad36a997bf303d7512a20b0473 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 765ea7d5f2..a89c106ac3 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -5036,6 +5036,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 ")?; @@ -5046,6 +5113,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 0218ea7366..af70fe3d99 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -68,19 +68,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, @@ -3932,6 +3932,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) @@ -5759,6 +5764,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}") @@ -12679,6 +12685,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 b9ae993cbb..b2b329730e 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(), @@ -2862,6 +2863,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 a323a40b82..61dc45e4e8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11923,6 +11923,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() } @@ -11930,7 +11931,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(), ) } @@ -12362,6 +12363,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 54a3b994e4..bf0b04a8fc 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 64aaad4d41..2c3d7b0be0 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -12542,3 +12542,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 bf8219cbcf7d42452dff41f9aa13347ba484305b 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 2c3d7b0be0..b924f5b126 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -12601,6 +12601,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() @@ -12612,3 +12623,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:?}" + ); + } +}