diff --git a/fuzz/fuzz_seeds/postgres_create_user_mapping.sql b/fuzz/fuzz_seeds/postgres_create_user_mapping.sql new file mode 100644 index 0000000000..92d3746c72 --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_create_user_mapping.sql @@ -0,0 +1,2 @@ +CREATE USER MAPPING IF NOT EXISTS FOR app SERVER s OPTIONS ("user" 'remote', password 'secret'); +CREATE USER MAPPING FOR PUBLIC SERVER s \ No newline at end of file diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 6d651e39a0..019414730f 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -6649,3 +6649,78 @@ impl Spanned for CreateForeignDataWrapper { ) } } + +/// The user of a `CREATE USER MAPPING` statement. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum UserMappingUser { + /// A role name. + Name(Ident), + /// `USER`, the current user. + User, + /// `CURRENT_ROLE` + CurrentRole, + /// `CURRENT_USER` + CurrentUser, + /// `SESSION_USER` + SessionUser, + /// `PUBLIC`, every role without a mapping of its own. + Public, +} + +impl fmt::Display for UserMappingUser { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + UserMappingUser::Name(name) => write!(f, "{name}"), + UserMappingUser::User => f.write_str("USER"), + UserMappingUser::CurrentRole => f.write_str("CURRENT_ROLE"), + UserMappingUser::CurrentUser => f.write_str("CURRENT_USER"), + UserMappingUser::SessionUser => f.write_str("SESSION_USER"), + UserMappingUser::Public => f.write_str("PUBLIC"), + } + } +} + +/// A `CREATE USER MAPPING` statement. +/// +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createusermapping.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 CreateUserMapping { + /// Whether `IF NOT EXISTS` was specified. + pub if_not_exists: bool, + /// The user the mapping is for. + pub user: UserMappingUser, + /// The `SERVER server_name` clause. + pub server_name: Ident, + /// Optional `OPTIONS (key 'value', ...)` clause. + pub options: Option>, +} + +impl fmt::Display for CreateUserMapping { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "CREATE USER MAPPING {if_not_exists}FOR {user} SERVER {server_name}", + if_not_exists = if self.if_not_exists { + "IF NOT EXISTS " + } else { + "" + }, + user = self.user, + server_name = self.server_name, + )?; + if let Some(options) = &self.options { + write!(f, " OPTIONS ({})", display_comma_separated(options))?; + } + Ok(()) + } +} + +impl From for crate::ast::Statement { + fn from(v: CreateUserMapping) -> Self { + crate::ast::Statement::CreateUserMapping(v) + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 653a3581e5..2fc79ed26b 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -78,8 +78,8 @@ pub use self::ddl::{ CreateForeignDataWrapper, CreateForeignTable, CreateFunction, CreateIndex, CreateOperator, CreateOperatorClass, CreateOperatorFamily, CreatePolicy, CreatePolicyCommand, CreatePolicyType, CreateRule, CreateRuleAction, CreateRuleDoKind, CreateRuleEvent, CreateTable, CreateTextSearch, - CreateTrigger, CreateView, Deduplicate, DeferrableInitial, DistStyle, DropBehavior, - DropExtension, DropFunction, DropOperator, DropOperatorClass, DropOperatorFamily, + CreateTrigger, CreateUserMapping, CreateView, Deduplicate, DeferrableInitial, DistStyle, + DropBehavior, DropExtension, DropFunction, DropOperator, DropOperatorClass, DropOperatorFamily, DropOperatorSignature, DropPolicy, DropTrigger, ForValues, ForeignDataWrapperRoutineClause, FunctionReturnType, GeneratedAs, GeneratedExpressionMode, IdentityParameters, IdentityProperty, IdentityPropertyFormatKind, IdentityPropertyKind, IdentityPropertyOrder, IndexColumn, @@ -89,7 +89,8 @@ pub use self::ddl::{ ReplicaIdentity, SqliteTableOption, SqliteTableOptions, TagsColumnOption, TextSearchObjectType, TriggerObjectKind, Truncate, UserDefinedTypeCompositeAttributeDef, UserDefinedTypeInternalLength, UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, - UserDefinedTypeSqlDefinitionOption, UserDefinedTypeStorage, ViewColumnDef, WithData, + UserDefinedTypeSqlDefinitionOption, UserDefinedTypeStorage, UserMappingUser, ViewColumnDef, + WithData, }; pub use self::dml::{ Delete, Insert, InsertOverriding, Merge, MergeAction, MergeClause, MergeClauseKind, @@ -3847,6 +3848,11 @@ pub enum Statement { /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createforeigndatawrapper.html) CreateForeignDataWrapper(CreateForeignDataWrapper), /// ```sql + /// CREATE USER MAPPING + /// ``` + /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createusermapping.html) + CreateUserMapping(CreateUserMapping), + /// ```sql /// CREATE POLICY /// ``` /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createpolicy.html) @@ -5729,6 +5735,7 @@ impl fmt::Display for Statement { } Statement::CreateForeignTable(stmt) => write!(f, "{stmt}"), Statement::CreateForeignDataWrapper(stmt) => write!(f, "{stmt}"), + Statement::CreateUserMapping(stmt) => write!(f, "{stmt}"), Statement::CreatePolicy(policy) => write!(f, "{policy}"), Statement::CreateRule(rule) => write!(f, "{rule}"), Statement::CreateConnector(create_connector) => create_connector.fmt(f), diff --git a/src/ast/spans.rs b/src/ast/spans.rs index e5eca80edb..b9104447f0 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -33,12 +33,12 @@ use super::{ Assignment, AssignmentTarget, AttachedToken, BeginEndStatements, CaseStatement, CloseCursor, ClusteredIndex, ColumnDef, ColumnOption, ColumnOptionDef, ConditionalStatementBlock, ConditionalStatements, ConflictTarget, ConnectByKind, ConstraintCharacteristics, CopySource, - CreateIndex, CreateRule, CreateRuleAction, 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, + CreateIndex, CreateRule, CreateRuleAction, CreateTable, CreateTableOptions, CreateUserMapping, + 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, @@ -47,8 +47,9 @@ use super::{ 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, + 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. @@ -399,6 +400,7 @@ impl Spanned for Statement { Statement::CreateServer { .. } => Span::empty(), Statement::CreateForeignTable(stmt) => stmt.span(), Statement::CreateForeignDataWrapper(stmt) => stmt.span(), + Statement::CreateUserMapping(stmt) => stmt.span(), Statement::CreateConnector { .. } => Span::empty(), Statement::CreateOperator(create_operator) => create_operator.span(), Statement::CreateOperatorFamily(create_operator_family) => { @@ -2777,6 +2779,23 @@ impl Spanned for comments::CommentWithSpan { } } +impl Spanned for CreateUserMapping { + fn span(&self) -> Span { + let user = match &self.user { + UserMappingUser::Name(name) => name.span, + _ => Span::empty(), + }; + Span::union_iter( + [user, self.server_name.span].into_iter().chain( + self.options + .iter() + .flatten() + .flat_map(|option| [option.key.span, option.value.span]), + ), + ) + } +} + #[cfg(test)] pub mod tests { use crate::ast::Value; diff --git a/src/keywords.rs b/src/keywords.rs index 561919b6e1..fb09a17009 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -634,6 +634,7 @@ define_keywords!( MANAGEDLOCATION, MANIFEST, MAP, + MAPPING, MASKING, MATCH, MATCHED, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 1b35306d3f..2a5ca2d947 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5520,6 +5520,24 @@ impl<'a> Parser<'a> { self.parse_create_macro(or_replace, temporary) } else if self.parse_keyword(Keyword::SECRET) { self.parse_create_secret(or_replace, temporary, persistent) + } else if self.peek_keywords(&[Keyword::USER, Keyword::MAPPING, Keyword::FOR]) + || self.peek_keywords(&[ + Keyword::USER, + Keyword::MAPPING, + Keyword::IF, + Keyword::NOT, + Keyword::EXISTS, + ]) + { + if has_modifier { + return parser_err!( + "CREATE USER MAPPING does not accept this modifier", + modifier_loc + ); + } + self.advance_token(); // USER + self.advance_token(); // MAPPING + self.parse_create_user_mapping().map(Into::into) } else if self.parse_keyword(Keyword::USER) { self.parse_create_user(or_replace).map(Into::into) } else if self.parse_keyword(Keyword::AGGREGATE) { @@ -21458,6 +21476,38 @@ impl<'a> Parser<'a> { }) } + /// Parse a `CREATE USER MAPPING` statement, after `CREATE USER MAPPING`. + /// + /// See + pub fn parse_create_user_mapping(&mut self) -> Result { + let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + self.expect_keyword_is(Keyword::FOR)?; + let user = match self.parse_one_of_keywords(&[ + Keyword::USER, + Keyword::CURRENT_ROLE, + Keyword::CURRENT_USER, + Keyword::SESSION_USER, + Keyword::PUBLIC, + ]) { + Some(Keyword::USER) => UserMappingUser::User, + Some(Keyword::CURRENT_ROLE) => UserMappingUser::CurrentRole, + Some(Keyword::CURRENT_USER) => UserMappingUser::CurrentUser, + Some(Keyword::SESSION_USER) => UserMappingUser::SessionUser, + Some(Keyword::PUBLIC) => UserMappingUser::Public, + _ => UserMappingUser::Name(self.parse_identifier()?), + }; + self.expect_keyword_is(Keyword::SERVER)?; + let server_name = self.parse_identifier()?; + let options = self.parse_pg_options_clause()?; + + Ok(CreateUserMapping { + if_not_exists, + user, + server_name, + options, + }) + } + /// The index of the first unprocessed token. pub fn index(&self) -> usize { self.index diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 15479cbd41..03355e497c 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -12004,3 +12004,129 @@ fn display_table_inheritance_propagates_write_errors() { assert_eq!(write!(writer, "{stmt}"), Err(fmt::Error), "{sql}"); } } + +#[test] +fn parse_create_user_mapping() { + let sql = + r#"CREATE USER MAPPING FOR app SERVER s OPTIONS ("user" 'remote', password 'secret')"#; + let Statement::CreateUserMapping(stmt) = pg_and_generic().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!( + stmt, + CreateUserMapping { + if_not_exists: false, + user: UserMappingUser::Name(Ident::new("app")), + server_name: Ident::new("s"), + options: Some(vec![ + CreateServerOption { + key: Ident::with_quote('"', "user"), + value: Ident::with_quote('\'', "remote"), + }, + CreateServerOption { + key: Ident::new("password"), + value: Ident::with_quote('\'', "secret"), + }, + ]), + } + ); + + for (sql, user) in [ + ( + "CREATE USER MAPPING IF NOT EXISTS FOR USER SERVER s", + UserMappingUser::User, + ), + ( + "CREATE USER MAPPING FOR CURRENT_ROLE SERVER s", + UserMappingUser::CurrentRole, + ), + ( + "CREATE USER MAPPING FOR CURRENT_USER SERVER s", + UserMappingUser::CurrentUser, + ), + ( + "CREATE USER MAPPING FOR SESSION_USER SERVER s", + UserMappingUser::SessionUser, + ), + ( + "CREATE USER MAPPING FOR PUBLIC SERVER s", + UserMappingUser::Public, + ), + ] { + let Statement::CreateUserMapping(stmt) = pg_and_generic().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!(stmt.user, user); + } + // As written by pg_dump. + pg_and_generic().one_statement_parses_to( + "CREATE USER MAPPING FOR public SERVER s", + "CREATE USER MAPPING FOR PUBLIC SERVER s", + ); + + // A user named `mapping`. + assert!(matches!( + pg_and_generic().verified_stmt("CREATE USER mapping"), + Statement::CreateUser(_) + )); + + for (sql, expected) in [ + ( + "CREATE USER MAPPING FOR app", + "Expected: SERVER, found: EOF", + ), + ( + "CREATE USER MAPPING FOR", + "Expected: identifier, found: EOF", + ), + ( + "CREATE USER MAPPING FOR app SERVER", + "Expected: identifier, found: EOF", + ), + ( + "CREATE USER MAPPING IF NOT EXISTS app SERVER s", + "Expected: FOR, found: app", + ), + ( + "CREATE USER MAPPING FOR app SERVER s OPTIONS (password)", + "Expected: identifier, found: )", + ), + ( + "CREATE OR REPLACE USER MAPPING FOR app SERVER s", + "CREATE USER MAPPING does not accept this modifier", + ), + ] { + assert_eq!( + pg_and_generic() + .parse_sql_statements(sql) + .unwrap_err() + .to_string(), + format!("sql parser error: {expected}"), + "{sql}" + ); + } +} + +#[test] +fn display_create_user_mapping_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 stmt = pg_and_generic() + .verified_stmt("CREATE USER MAPPING FOR app SERVER s OPTIONS (password 'secret')"); + for rejected in ["CREATE USER MAPPING ", " OPTIONS ("] { + let mut writer = RejectingWriter(rejected); + assert_eq!(write!(writer, "{stmt}"), Err(fmt::Error), "{rejected:?}"); + } +}