Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions fuzz/fuzz_seeds/postgres_create_user_mapping.sql
Original file line number Diff line number Diff line change
@@ -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
75 changes: 75 additions & 0 deletions src/ast/ddl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6628,3 +6628,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<Vec<CreateServerOption>>,
}

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<CreateUserMapping> for crate::ast::Statement {
fn from(v: CreateUserMapping) -> Self {
crate::ast::Statement::CreateUserMapping(v)
}
}
13 changes: 10 additions & 3 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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),
Expand Down
35 changes: 27 additions & 8 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -2773,6 +2775,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;
Expand Down
1 change: 1 addition & 0 deletions src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,7 @@ define_keywords!(
MANAGEDLOCATION,
MANIFEST,
MAP,
MAPPING,
MASKING,
MATCH,
MATCHED,
Expand Down
50 changes: 50 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5557,6 +5557,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) {
Expand Down Expand Up @@ -21415,6 +21433,38 @@ impl<'a> Parser<'a> {
})
}

/// Parse a `CREATE USER MAPPING` statement, after `CREATE USER MAPPING`.
///
/// See <https://www.postgresql.org/docs/current/sql-createusermapping.html>
pub fn parse_create_user_mapping(&mut self) -> Result<CreateUserMapping, ParserError> {
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
Expand Down
126 changes: 126 additions & 0 deletions tests/sqlparser_postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11529,3 +11529,129 @@ fn parse_named_argument_with_assignment_operator() {
"sql parser error: Expected: end of statement, found: :="
);
}

#[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:?}");
}
}
Loading