diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 1e430171e..047cde530 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2459,6 +2459,8 @@ pub enum CommentObject { Schema, /// A sequence. Sequence, + /// A subscription. + Subscription, /// A table. Table, /// A type. @@ -2483,6 +2485,7 @@ impl fmt::Display for CommentObject { CommentObject::Role => f.write_str("ROLE"), CommentObject::Schema => f.write_str("SCHEMA"), CommentObject::Sequence => f.write_str("SEQUENCE"), + CommentObject::Subscription => f.write_str("SUBSCRIPTION"), CommentObject::Table => f.write_str("TABLE"), CommentObject::Type => f.write_str("TYPE"), CommentObject::User => f.write_str("USER"), @@ -3682,6 +3685,12 @@ pub enum Statement { /// A `CREATE SERVER` statement. CreateServer(CreateServerStatement), /// ```sql + /// CREATE SUBSCRIPTION + /// ``` + /// + /// Note: this is a PostgreSQL-specific statement. + CreateSubscription(CreateSubscription), + /// ```sql /// CREATE POLICY /// ``` /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-createpolicy.html) @@ -3768,6 +3777,12 @@ pub enum Statement { operation: AlterRoleOperation, }, /// ```sql + /// ALTER SUBSCRIPTION + /// ``` + /// + /// Note: this is a PostgreSQL-specific statement. + AlterSubscription(AlterSubscription), + /// ```sql /// ALTER POLICY ON [] /// ``` /// (Postgresql-specific) @@ -5436,6 +5451,9 @@ impl fmt::Display for Statement { Statement::CreateServer(stmt) => { write!(f, "{stmt}") } + Statement::CreateSubscription(stmt) => { + write!(f, "{stmt}") + } Statement::CreatePolicy(policy) => write!(f, "{policy}"), Statement::CreateConnector(create_connector) => create_connector.fmt(f), Statement::CreateOperator(create_operator) => create_operator.fmt(f), @@ -5475,6 +5493,9 @@ impl fmt::Display for Statement { Statement::AlterRole { name, operation } => { write!(f, "ALTER ROLE {name} {operation}") } + Statement::AlterSubscription(alter_subscription) => { + write!(f, "{alter_subscription}") + } Statement::AlterPolicy(alter_policy) => write!(f, "{alter_policy}"), Statement::AlterConnector { name, @@ -6760,6 +6781,11 @@ pub enum Action { BindServiceEndpoint, /// Connect permission. Connect, + /// Custom privilege name (primarily PostgreSQL). + Custom { + /// The custom privilege identifier. + name: Ident, + }, /// Create action, optionally specifying an object type. Create { /// Optional object type to create. @@ -6874,6 +6900,7 @@ impl fmt::Display for Action { Action::Audit => f.write_str("AUDIT")?, Action::BindServiceEndpoint => f.write_str("BIND SERVICE ENDPOINT")?, Action::Connect => f.write_str("CONNECT")?, + Action::Custom { name } => write!(f, "{name}")?, Action::Create { obj_type } => { f.write_str("CREATE")?; if let Some(obj_type) = obj_type { @@ -8246,6 +8273,8 @@ pub enum ObjectType { Role, /// A sequence. Sequence, + /// A subscription. + Subscription, /// A stage. Stage, /// A type definition. @@ -8267,6 +8296,7 @@ impl fmt::Display for ObjectType { ObjectType::Database => "DATABASE", ObjectType::Role => "ROLE", ObjectType::Sequence => "SEQUENCE", + ObjectType::Subscription => "SUBSCRIPTION", ObjectType::Stage => "STAGE", ObjectType::Type => "TYPE", ObjectType::User => "USER", @@ -8770,6 +8800,206 @@ impl fmt::Display for CreateServerOption { } } +/// A subscription option used by `CREATE/ALTER SUBSCRIPTION`. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct SubscriptionOption { + /// Subscription parameter name. + pub name: Ident, + /// Optional parameter value. + pub value: Option, +} + +impl fmt::Display for SubscriptionOption { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(value) = &self.value { + write!(f, "{} = {}", self.name, value) + } else { + write!(f, "{}", self.name) + } + } +} + +/// A `CREATE SUBSCRIPTION` statement. +/// +/// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-createsubscription.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 CreateSubscription { + /// Subscription name. + pub name: ObjectName, + /// Connection string. + pub connection: String, + /// Publication names. + pub publications: Vec, + /// Optional subscription parameters from `WITH (...)`. + pub with_options: Vec, +} + +impl fmt::Display for CreateSubscription { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "CREATE SUBSCRIPTION {} CONNECTION '{}' PUBLICATION {}", + self.name, + value::escape_single_quote_string(&self.connection), + display_comma_separated(&self.publications) + )?; + + if !self.with_options.is_empty() { + write!(f, " WITH ({})", display_comma_separated(&self.with_options))?; + } + + Ok(()) + } +} + +/// An `ALTER SUBSCRIPTION` statement. +/// +/// [PostgreSQL Documentation](https://www.postgresql.org/docs/current/sql-altersubscription.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 AlterSubscription { + /// Subscription name. + pub name: ObjectName, + /// Operation to perform. + pub operation: AlterSubscriptionOperation, +} + +impl fmt::Display for AlterSubscription { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "ALTER SUBSCRIPTION {} {}", self.name, self.operation) + } +} + +/// Operations supported by `ALTER SUBSCRIPTION`. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterSubscriptionOperation { + /// Update the subscription connection string. + Connection { + /// New connection string. + connection: String, + }, + /// Replace subscription publications. + SetPublication { + /// Publication names. + publications: Vec, + /// Optional `WITH (...)` parameters. + with_options: Vec, + }, + /// Add publications to the subscription. + AddPublication { + /// Publication names. + publications: Vec, + /// Optional `WITH (...)` parameters. + with_options: Vec, + }, + /// Drop publications from the subscription. + DropPublication { + /// Publication names. + publications: Vec, + /// Optional `WITH (...)` parameters. + with_options: Vec, + }, + /// Refresh subscription publications. + RefreshPublication { + /// Optional `WITH (...)` parameters. + with_options: Vec, + }, + /// Enable the subscription. + Enable, + /// Disable the subscription. + Disable, + /// Set subscription parameters. + SetOptions { + /// Parameters within `SET (...)`. + options: Vec, + }, + /// Change subscription owner. + OwnerTo { + /// New owner. + owner: Owner, + }, + /// Rename the subscription. + RenameTo { + /// New subscription name. + new_name: ObjectName, + }, +} + +impl fmt::Display for AlterSubscriptionOperation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn write_with_options( + f: &mut fmt::Formatter<'_>, + with_options: &[SubscriptionOption], + ) -> fmt::Result { + if !with_options.is_empty() { + write!(f, " WITH ({})", display_comma_separated(with_options))?; + } + Ok(()) + } + + match self { + AlterSubscriptionOperation::Connection { connection } => { + write!( + f, + "CONNECTION '{}'", + value::escape_single_quote_string(connection) + ) + } + AlterSubscriptionOperation::SetPublication { + publications, + with_options, + } => { + write!( + f, + "SET PUBLICATION {}", + display_comma_separated(publications) + )?; + write_with_options(f, with_options) + } + AlterSubscriptionOperation::AddPublication { + publications, + with_options, + } => { + write!( + f, + "ADD PUBLICATION {}", + display_comma_separated(publications) + )?; + write_with_options(f, with_options) + } + AlterSubscriptionOperation::DropPublication { + publications, + with_options, + } => { + write!( + f, + "DROP PUBLICATION {}", + display_comma_separated(publications) + )?; + write_with_options(f, with_options) + } + AlterSubscriptionOperation::RefreshPublication { with_options } => { + write!(f, "REFRESH PUBLICATION")?; + write_with_options(f, with_options) + } + AlterSubscriptionOperation::Enable => write!(f, "ENABLE"), + AlterSubscriptionOperation::Disable => write!(f, "DISABLE"), + AlterSubscriptionOperation::SetOptions { options } => { + write!(f, "SET ({})", display_comma_separated(options)) + } + AlterSubscriptionOperation::OwnerTo { owner } => write!(f, "OWNER TO {owner}"), + AlterSubscriptionOperation::RenameTo { new_name } => write!(f, "RENAME TO {new_name}"), + } + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] @@ -11669,6 +11899,8 @@ impl fmt::Display for VacuumStatement { pub enum Reset { /// Resets all session parameters to their default values. ALL, + /// Resets session authorization to the session user. + SessionAuthorization, /// Resets a specific session parameter to its default value. ConfigurationParameter(ObjectName), @@ -11751,6 +11983,7 @@ impl fmt::Display for ResetStatement { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self.reset { Reset::ALL => write!(f, "RESET ALL"), + Reset::SessionAuthorization => write!(f, "RESET SESSION AUTHORIZATION"), Reset::ConfigurationParameter(param) => write!(f, "RESET {}", param), } } @@ -11888,6 +12121,12 @@ impl From for Statement { } } +impl From for Statement { + fn from(c: CreateSubscription) -> Self { + Self::CreateSubscription(c) + } +} + impl From for Statement { fn from(c: CreateConnector) -> Self { Self::CreateConnector(c) @@ -11918,6 +12157,12 @@ impl From for Statement { } } +impl From for Statement { + fn from(a: AlterSubscription) -> Self { + Self::AlterSubscription(a) + } +} + impl From for Statement { fn from(a: AlterType) -> Self { Self::AlterType(a) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 0b95c3ed7..28ea7e583 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -382,6 +382,7 @@ impl Spanned for Statement { Statement::DropOperatorClass(drop_operator_class) => drop_operator_class.span(), Statement::CreateSecret { .. } => Span::empty(), Statement::CreateServer { .. } => Span::empty(), + Statement::CreateSubscription { .. } => Span::empty(), Statement::CreateConnector { .. } => Span::empty(), Statement::CreateOperator(create_operator) => create_operator.span(), Statement::CreateOperatorFamily(create_operator_family) => { @@ -407,6 +408,7 @@ impl Spanned for Statement { Statement::AlterOperatorFamily { .. } => Span::empty(), Statement::AlterOperatorClass { .. } => Span::empty(), Statement::AlterRole { .. } => Span::empty(), + Statement::AlterSubscription { .. } => Span::empty(), Statement::AlterSession { .. } => Span::empty(), Statement::AttachDatabase { .. } => Span::empty(), Statement::AttachDuckDBDatabase { .. } => Span::empty(), diff --git a/src/keywords.rs b/src/keywords.rs index cc2b9e9dd..3c01ab12f 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -808,6 +808,7 @@ define_keywords!( PROGRAM, PROJECTION, PUBLIC, + PUBLICATION, PURCHASE, PURGE, QUALIFY, @@ -994,6 +995,7 @@ define_keywords!( STRUCT, SUBMULTISET, SUBSCRIPT, + SUBSCRIPTION, SUBSTR, SUBSTRING, SUBSTRING_REGEX, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index bea566bbe..139f00a67 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -935,6 +935,9 @@ impl<'a> Parser<'a> { Token::Word(w) if w.keyword == Keyword::SEQUENCE => { (CommentObject::Sequence, self.parse_object_name(false)?) } + Token::Word(w) if w.keyword == Keyword::SUBSCRIPTION => { + (CommentObject::Subscription, self.parse_subscription_name()?) + } Token::Word(w) if w.keyword == Keyword::TABLE => { (CommentObject::Table, self.parse_object_name(false)?) } @@ -5168,6 +5171,8 @@ impl<'a> Parser<'a> { } } else if self.parse_keyword(Keyword::SERVER) { self.parse_pg_create_server() + } else if self.parse_keyword(Keyword::SUBSCRIPTION) { + self.parse_create_subscription() } else { self.expected_ref("an object type after CREATE", self.peek_token_ref()) } @@ -7214,6 +7219,8 @@ impl<'a> Parser<'a> { ObjectType::Database } else if self.parse_keyword(Keyword::SEQUENCE) { ObjectType::Sequence + } else if self.parse_keyword(Keyword::SUBSCRIPTION) { + ObjectType::Subscription } else if self.parse_keyword(Keyword::STAGE) { ObjectType::Stage } else if self.parse_keyword(Keyword::TYPE) { @@ -7249,14 +7256,18 @@ impl<'a> Parser<'a> { }; } else { return self.expected_ref( - "CONNECTOR, DATABASE, EXTENSION, FUNCTION, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW or USER after DROP", + "CONNECTOR, DATABASE, EXTENSION, FUNCTION, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, SUBSCRIPTION, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW or USER after DROP", self.peek_token_ref(), ); }; // Many dialects support the non-standard `IF EXISTS` clause and allow // specifying multiple objects to delete in a single statement let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); - let names = self.parse_comma_separated(|p| p.parse_object_name(false))?; + let names = if object_type == ObjectType::Subscription { + self.parse_comma_separated(|p| p.parse_subscription_name())? + } else { + self.parse_comma_separated(|p| p.parse_object_name(false))? + }; let loc = self.peek_token_ref().span.start; let cascade = self.parse_keyword(Keyword::CASCADE); @@ -10440,6 +10451,7 @@ impl<'a> Parser<'a> { Keyword::SCHEMA, Keyword::USER, Keyword::OPERATOR, + Keyword::SUBSCRIPTION, ])?; match object_type { Keyword::SCHEMA => { @@ -10482,12 +10494,13 @@ impl<'a> Parser<'a> { } } Keyword::ROLE => self.parse_alter_role(), + Keyword::SUBSCRIPTION => self.parse_alter_subscription(), Keyword::POLICY => self.parse_alter_policy().map(Into::into), Keyword::CONNECTOR => self.parse_alter_connector(), Keyword::USER => self.parse_alter_user().map(Into::into), // unreachable because expect_one_of_keywords used above unexpected_keyword => Err(ParserError::ParserError( - format!("Internal parser error: expected any of {{VIEW, TYPE, TABLE, INDEX, ROLE, POLICY, CONNECTOR, ICEBERG, SCHEMA, USER, OPERATOR}}, got {unexpected_keyword:?}"), + format!("Internal parser error: expected any of {{VIEW, TYPE, TABLE, INDEX, ROLE, POLICY, CONNECTOR, ICEBERG, SCHEMA, USER, OPERATOR, SUBSCRIPTION}}, got {unexpected_keyword:?}"), )), } } @@ -16508,9 +16521,18 @@ impl<'a> Parser<'a> { /// Parse a GRANT statement. pub fn parse_grant(&mut self) -> Result { - let (privileges, objects) = self.parse_grant_deny_revoke_privileges_objects()?; + let (privileges, objects, to_parsed) = if let Some(privileges) = + self.maybe_parse_postgres_predefined_role_privileges(Keyword::TO)? + { + (privileges, None, true) + } else { + let (privileges, objects) = self.parse_grant_deny_revoke_privileges_objects()?; + (privileges, objects, false) + }; - self.expect_keyword_is(Keyword::TO)?; + if !to_parsed { + self.expect_keyword_is(Keyword::TO)?; + } let grantees = self.parse_grantees()?; let with_grant_option = @@ -16548,6 +16570,34 @@ impl<'a> Parser<'a> { }) } + fn parse_postgres_predefined_role_privilege(&mut self) -> Result { + let ident = self.parse_identifier()?; + if ident.quote_style.is_none() && ident.value.to_ascii_lowercase().starts_with("pg_") { + Ok(Action::Custom { name: ident }) + } else { + self.expected_ref( + "a PostgreSQL predefined role name (starting with pg_)", + self.peek_token_ref(), + ) + } + } + + fn maybe_parse_postgres_predefined_role_privileges( + &mut self, + target_keyword: Keyword, + ) -> Result, ParserError> { + if !dialect_of!(self is PostgreSqlDialect) { + return Ok(None); + } + + self.maybe_parse(|parser| { + let actions = + parser.parse_comma_separated(Parser::parse_postgres_predefined_role_privilege)?; + parser.expect_keyword_is(target_keyword)?; + Ok(Privileges::Actions(actions)) + }) + } + fn parse_grantees(&mut self) -> Result, ParserError> { let mut values = vec![]; let mut grantee_type = GranteesType::None; @@ -17133,9 +17183,18 @@ impl<'a> Parser<'a> { /// Parse a REVOKE statement pub fn parse_revoke(&mut self) -> Result { - let (privileges, objects) = self.parse_grant_deny_revoke_privileges_objects()?; + let (privileges, objects, from_parsed) = if let Some(privileges) = + self.maybe_parse_postgres_predefined_role_privileges(Keyword::FROM)? + { + (privileges, None, true) + } else { + let (privileges, objects) = self.parse_grant_deny_revoke_privileges_objects()?; + (privileges, objects, false) + }; - self.expect_keyword_is(Keyword::FROM)?; + if !from_parsed { + self.expect_keyword_is(Keyword::FROM)?; + } let grantees = self.parse_grantees()?; let granted_by = if self.parse_keywords(&[Keyword::GRANTED, Keyword::BY]) { @@ -18979,6 +19038,125 @@ impl<'a> Parser<'a> { })) } + fn parse_subscription_option(&mut self) -> Result { + let name = self.parse_identifier()?; + let value = if self.consume_token(&Token::Eq) { + Some(self.parse_expr()?) + } else { + None + }; + Ok(SubscriptionOption { name, value }) + } + + fn parse_subscription_name(&mut self) -> Result { + let name = self.parse_object_name(false)?; + if name.0.len() == 1 && name.0[0].as_ident().is_some() { + Ok(name) + } else { + parser_err!( + format!("Expected: subscription name (single identifier), found: {name}"), + self.peek_token_ref().span.start + ) + } + } + + fn parse_subscription_options(&mut self) -> Result, ParserError> { + if self.parse_keyword(Keyword::WITH) { + self.expect_token(&Token::LParen)?; + let options = self.parse_comma_separated(Parser::parse_subscription_option)?; + self.expect_token(&Token::RParen)?; + Ok(options) + } else { + Ok(vec![]) + } + } + + fn parse_subscription_publications(&mut self) -> Result, ParserError> { + self.parse_comma_separated(|p| p.parse_identifier()) + } + + /// Parse a `CREATE SUBSCRIPTION` statement. + /// + /// See [Statement::CreateSubscription]. + pub fn parse_create_subscription(&mut self) -> Result { + let name = self.parse_subscription_name()?; + self.expect_keyword_is(Keyword::CONNECTION)?; + let connection = self.parse_literal_string()?; + self.expect_keyword_is(Keyword::PUBLICATION)?; + let publications = self.parse_subscription_publications()?; + let with_options = self.parse_subscription_options()?; + + Ok(Statement::CreateSubscription(CreateSubscription { + name, + connection, + publications, + with_options, + })) + } + + /// Parse an `ALTER SUBSCRIPTION` statement. + /// + /// See [Statement::AlterSubscription]. + pub fn parse_alter_subscription(&mut self) -> Result { + let name = self.parse_subscription_name()?; + let operation = if self.parse_keyword(Keyword::CONNECTION) { + AlterSubscriptionOperation::Connection { + connection: self.parse_literal_string()?, + } + } else if self.parse_keywords(&[Keyword::SET, Keyword::PUBLICATION]) { + let publications = self.parse_subscription_publications()?; + let with_options = self.parse_subscription_options()?; + AlterSubscriptionOperation::SetPublication { + publications, + with_options, + } + } else if self.parse_keywords(&[Keyword::ADD, Keyword::PUBLICATION]) { + let publications = self.parse_subscription_publications()?; + let with_options = self.parse_subscription_options()?; + AlterSubscriptionOperation::AddPublication { + publications, + with_options, + } + } else if self.parse_keywords(&[Keyword::DROP, Keyword::PUBLICATION]) { + let publications = self.parse_subscription_publications()?; + let with_options = self.parse_subscription_options()?; + AlterSubscriptionOperation::DropPublication { + publications, + with_options, + } + } else if self.parse_keywords(&[Keyword::REFRESH, Keyword::PUBLICATION]) { + let with_options = self.parse_subscription_options()?; + AlterSubscriptionOperation::RefreshPublication { with_options } + } else if self.parse_keyword(Keyword::ENABLE) { + AlterSubscriptionOperation::Enable + } else if self.parse_keyword(Keyword::DISABLE) { + AlterSubscriptionOperation::Disable + } else if self.parse_keyword(Keyword::SET) { + self.expect_token(&Token::LParen)?; + let options = self.parse_comma_separated(Parser::parse_subscription_option)?; + self.expect_token(&Token::RParen)?; + AlterSubscriptionOperation::SetOptions { options } + } else if self.parse_keywords(&[Keyword::OWNER, Keyword::TO]) { + AlterSubscriptionOperation::OwnerTo { + owner: self.parse_owner()?, + } + } else if self.parse_keywords(&[Keyword::RENAME, Keyword::TO]) { + AlterSubscriptionOperation::RenameTo { + new_name: self.parse_subscription_name()?, + } + } else { + return self.expected_ref( + "CONNECTION, SET PUBLICATION, ADD PUBLICATION, DROP PUBLICATION, REFRESH PUBLICATION, ENABLE, DISABLE, SET, OWNER TO, or RENAME TO after ALTER SUBSCRIPTION", + self.peek_token_ref(), + ); + }; + + Ok(Statement::AlterSubscription(AlterSubscription { + name, + operation, + })) + } + /// The index of the first unprocessed token. pub fn index(&self) -> usize { self.index @@ -19763,6 +19941,13 @@ impl<'a> Parser<'a> { if self.parse_keyword(Keyword::ALL) { return Ok(ResetStatement { reset: Reset::ALL }); } + if dialect_of!(self is PostgreSqlDialect) + && self.parse_keywords(&[Keyword::SESSION, Keyword::AUTHORIZATION]) + { + return Ok(ResetStatement { + reset: Reset::SessionAuthorization, + }); + } let obj = self.parse_object_name(false)?; Ok(ResetStatement { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 982bf1088..c3ccadfe5 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -15207,26 +15207,31 @@ fn parse_comments() { // https://www.postgresql.org/docs/current/sql-comment.html let object_types = [ - ("COLUMN", CommentObject::Column), - ("DATABASE", CommentObject::Database), - ("DOMAIN", CommentObject::Domain), - ("EXTENSION", CommentObject::Extension), - ("FUNCTION", CommentObject::Function), - ("INDEX", CommentObject::Index), - ("MATERIALIZED VIEW", CommentObject::MaterializedView), - ("PROCEDURE", CommentObject::Procedure), - ("ROLE", CommentObject::Role), - ("SCHEMA", CommentObject::Schema), - ("SEQUENCE", CommentObject::Sequence), - ("TABLE", CommentObject::Table), - ("TYPE", CommentObject::Type), - ("USER", CommentObject::User), - ("VIEW", CommentObject::View), + ("COLUMN", CommentObject::Column, "db.t0"), + ("DATABASE", CommentObject::Database, "db.t0"), + ("DOMAIN", CommentObject::Domain, "db.t0"), + ("EXTENSION", CommentObject::Extension, "db.t0"), + ("FUNCTION", CommentObject::Function, "db.t0"), + ("INDEX", CommentObject::Index, "db.t0"), + ( + "MATERIALIZED VIEW", + CommentObject::MaterializedView, + "db.t0", + ), + ("PROCEDURE", CommentObject::Procedure, "db.t0"), + ("ROLE", CommentObject::Role, "db.t0"), + ("SCHEMA", CommentObject::Schema, "db.t0"), + ("SEQUENCE", CommentObject::Sequence, "db.t0"), + ("SUBSCRIPTION", CommentObject::Subscription, "t0"), + ("TABLE", CommentObject::Table, "db.t0"), + ("TYPE", CommentObject::Type, "db.t0"), + ("USER", CommentObject::User, "db.t0"), + ("VIEW", CommentObject::View, "db.t0"), ]; - for (keyword, expected_object_type) in object_types.iter() { - match all_dialects_where(|d| d.supports_comment_on()) - .verified_stmt(format!("COMMENT IF EXISTS ON {keyword} db.t0 IS 'comment'").as_str()) - { + for (keyword, expected_object_type, object_name_sql) in object_types.iter() { + match all_dialects_where(|d| d.supports_comment_on()).verified_stmt( + format!("COMMENT IF EXISTS ON {keyword} {object_name_sql} IS 'comment'").as_str(), + ) { Statement::Comment { object_type, object_name, @@ -15234,7 +15239,7 @@ fn parse_comments() { if_exists, } => { assert_eq!("comment", comment); - assert_eq!("db.t0", object_name.to_string()); + assert_eq!(*object_name_sql, object_name.to_string()); assert_eq!(*expected_object_type, object_type); assert!(if_exists); } @@ -18363,6 +18368,18 @@ fn parse_reset_statement() { Statement::Reset(ResetStatement { reset }) => assert_eq!(reset, Reset::ALL), _ => unreachable!(), } + let postgres = TestedDialects::new(vec![Box::new(PostgreSqlDialect {})]); + match postgres.verified_stmt("RESET SESSION AUTHORIZATION") { + Statement::Reset(ResetStatement { reset }) => { + assert_eq!(reset, Reset::SessionAuthorization) + } + _ => unreachable!(), + } + + let non_pg = all_dialects_where(|d| !d.is::()); + assert!(non_pg + .parse_sql_statements("RESET SESSION AUTHORIZATION") + .is_err()); } #[test] diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 7c19f51e5..a1ee13cb2 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -6897,6 +6897,156 @@ fn parse_create_server() { } } +#[test] +fn parse_subscription_statements() { + let statements = [ + "CREATE SUBSCRIPTION regress_testsub CONNECTION 'testconn' PUBLICATION testpub WITH (create_slot)", + "CREATE SUBSCRIPTION regress_testsub CONNECTION 'testconn' PUBLICATION testpub", + "CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION foo, testpub, foo WITH (connect = false)", + "CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false)", + "COMMENT ON SUBSCRIPTION regress_testsub IS 'test subscription'", + "CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION foo WITH (connect = false)", + "CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, copy_data = true)", + "CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, enabled = true)", + "CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, create_slot = true)", + "CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE, enabled = true)", + "CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE, enabled = false, create_slot = true)", + "CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE)", + "CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE, enabled = false)", + "CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE, create_slot = false)", + "CREATE SUBSCRIPTION regress_testsub3 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE, connect = false)", + "ALTER SUBSCRIPTION regress_testsub3 ENABLE", + "ALTER SUBSCRIPTION regress_testsub3 REFRESH PUBLICATION", + "CREATE SUBSCRIPTION regress_testsub4 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE, connect = false, origin = foo)", + "CREATE SUBSCRIPTION regress_testsub4 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE, connect = false, origin = none)", + "DROP SUBSCRIPTION regress_testsub4", + "CREATE SUBSCRIPTION regress_testsub5 CONNECTION 'i_dont_exist=param' PUBLICATION testpub", + "DROP SUBSCRIPTION regress_testsub", + "ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE)", + "DROP SUBSCRIPTION IF EXISTS regress_testsub", + "CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, binary = foo)", + "CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, binary = true)", + "CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, streaming = foo)", + "CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, streaming = true)", + "ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub1 WITH (refresh = false)", + "ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refresh = false)", + "ALTER SUBSCRIPTION regress_testsub DROP PUBLICATION testpub, testpub1, testpub2 WITH (refresh = false)", + "ALTER SUBSCRIPTION regress_testsub DROP PUBLICATION testpub3 WITH (refresh = false)", + "ALTER SUBSCRIPTION regress_testsub DROP PUBLICATION testpub1, testpub2 WITH (refresh = false)", + "CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION mypub WITH (connect = false, create_slot = false, copy_data = false)", + "ALTER SUBSCRIPTION regress_testsub ENABLE", + "ALTER SUBSCRIPTION regress_testsub SET PUBLICATION mypub WITH (refresh = true)", + "ALTER SUBSCRIPTION regress_testsub REFRESH PUBLICATION", + "ALTER SUBSCRIPTION regress_testsub DISABLE", + "CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, two_phase = foo)", + "CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, two_phase = true)", + "CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, streaming = true, two_phase = true)", + "CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, disable_on_error = foo)", + "CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, disable_on_error = false)", + "CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, retain_dead_tuples = foo)", + "CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, retain_dead_tuples = false)", + "CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, max_retention_duration = foo)", + "CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, max_retention_duration = 1000)", + "CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, password_required = false)", + "CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist password=regress_fakepassword' PUBLICATION testpub WITH (connect = false)", + "ALTER SUBSCRIPTION regress_testsub OWNER TO regress_subscription_user", + "ALTER SUBSCRIPTION regress_testsub RENAME TO regress_testsub2", + "REVOKE pg_create_subscription FROM regress_subscription_user3", + "ALTER SUBSCRIPTION regress_testsub2 RENAME TO regress_testsub", + ]; + + for sql in statements { + pg().verified_stmt(sql); + } + + let multi_statement = "SET SESSION AUTHORIZATION regress_subscription_user3; CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false); RESET SESSION AUTHORIZATION; GRANT CREATE ON DATABASE REGRESSION TO regress_subscription_user3; SET SESSION AUTHORIZATION regress_subscription_user3; CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false); RESET SESSION AUTHORIZATION"; + assert_eq!(pg().parse_sql_statements(multi_statement).unwrap().len(), 7); +} + +#[test] +fn parse_postgres_predefined_role_grant_revoke() { + match pg().verified_stmt("GRANT pg_create_subscription TO regress_subscription_user3") { + Statement::Grant(Grant { + privileges: Privileges::Actions(actions), + objects: None, + grantees, + .. + }) => { + assert_eq!( + actions, + vec![Action::Custom { + name: Ident::new("pg_create_subscription"), + }] + ); + assert_eq_vec(&["regress_subscription_user3"], &grantees); + } + _ => unreachable!(), + } + + match pg().verified_stmt( + "REVOKE pg_create_subscription, pg_read_all_data FROM regress_subscription_user3", + ) { + Statement::Revoke(Revoke { + privileges: Privileges::Actions(actions), + objects: None, + grantees, + granted_by, + cascade, + }) => { + assert_eq!( + actions, + vec![ + Action::Custom { + name: Ident::new("pg_create_subscription"), + }, + Action::Custom { + name: Ident::new("pg_read_all_data"), + } + ] + ); + assert_eq_vec(&["regress_subscription_user3"], &grantees); + assert_eq!(granted_by, None); + assert_eq!(cascade, None); + } + _ => unreachable!(), + } + + assert!(pg() + .parse_sql_statements("REVOKE some_random_privilege FROM regress_subscription_user3") + .is_err()); + assert!(pg() + .parse_sql_statements("REVOKE selct FROM regress_subscription_user3") + .is_err()); + assert!(pg() + .parse_sql_statements( + "REVOKE pg_create_subscription ON testpub FROM regress_subscription_user3" + ) + .is_err()); +} + +#[test] +fn parse_subscription_names_must_be_single_identifiers() { + assert!(pg() + .parse_sql_statements( + "CREATE SUBSCRIPTION public.regress_testsub CONNECTION 'testconn' PUBLICATION testpub" + ) + .is_err()); + assert!(pg() + .parse_sql_statements("ALTER SUBSCRIPTION public.regress_testsub ENABLE") + .is_err()); + assert!(pg() + .parse_sql_statements("COMMENT ON SUBSCRIPTION public.regress_testsub IS 'x'") + .is_err()); + assert!(pg() + .parse_sql_statements("DROP SUBSCRIPTION public.regress_testsub") + .is_err()); + assert!(pg() + .parse_sql_statements( + "ALTER SUBSCRIPTION regress_testsub RENAME TO public.regress_testsub2" + ) + .is_err()); +} + #[test] fn parse_alter_schema() { // Test RENAME operation