diff --git a/fuzz/fuzz_seeds/postgres_2742_create_extension_schema.sql b/fuzz/fuzz_seeds/postgres_2742_create_extension_schema.sql new file mode 100644 index 0000000000..bac15f105a --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_2742_create_extension_schema.sql @@ -0,0 +1 @@ +CREATE EXTENSION hstore VERSION '1.8' SCHEMA exts; diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 1247826c9f..1314a72b74 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -4616,6 +4616,8 @@ pub struct CreateExtension { pub name: Ident, /// Whether `IF NOT EXISTS` was specified for the CREATE EXTENSION. pub if_not_exists: bool, + /// Whether the optional `WITH` keyword preceded the options. + pub with_keyword: bool, /// Whether `CASCADE` was specified for the CREATE EXTENSION. pub cascade: bool, /// Optional schema name for the extension. @@ -4636,18 +4638,17 @@ impl fmt::Display for CreateExtension { }, name = self.name )?; - if self.cascade || self.schema.is_some() || self.version.is_some() { + if self.with_keyword { write!(f, " WITH")?; - - if let Some(name) = &self.schema { - write!(f, " SCHEMA {name}")?; - } - if let Some(version) = &self.version { - write!(f, " VERSION {version}")?; - } - if self.cascade { - write!(f, " CASCADE")?; - } + } + if let Some(name) = &self.schema { + write!(f, " SCHEMA {name}")?; + } + if let Some(version) = &self.version { + write!(f, " VERSION {version}")?; + } + if self.cascade { + write!(f, " CASCADE")?; } Ok(()) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f86406fb26..ccd98890ce 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8932,29 +8932,31 @@ impl<'a> Parser<'a> { let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); let name = self.parse_identifier()?; - let (schema, version, cascade) = if self.parse_keyword(Keyword::WITH) { - let schema = if self.parse_keyword(Keyword::SCHEMA) { - Some(self.parse_identifier()?) - } else { - None + let with_keyword = self.parse_keyword(Keyword::WITH); + let mut schema = None; + let mut version = None; + let mut cascade = false; + loop { + let loc = self.peek_token_ref().span.start; + let Some(keyword) = + self.parse_one_of_keywords(&[Keyword::SCHEMA, Keyword::VERSION, Keyword::CASCADE]) + else { + break; }; - - let version = if self.parse_keyword(Keyword::VERSION) { - Some(self.parse_identifier()?) - } else { - None + let already_set = match keyword { + Keyword::SCHEMA => schema.replace(self.parse_identifier()?).is_some(), + Keyword::VERSION => version.replace(self.parse_identifier()?).is_some(), + _ => core::mem::replace(&mut cascade, true), }; - - let cascade = self.parse_keyword(Keyword::CASCADE); - - (schema, version, cascade) - } else { - (None, None, false) - }; + if already_set { + return parser_err!(format!("{keyword:?} specified more than once"), loc); + } + } Ok(CreateExtension { name, if_not_exists, + with_keyword, schema, version, cascade, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index eafc006c04..20edd07931 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -886,6 +886,94 @@ fn parse_create_extension() { pg_and_generic().verified_stmt("CREATE EXTENSION extension_name WITH VERSION version CASCADE"); pg_and_generic() .verified_stmt("CREATE EXTENSION extension_name WITH SCHEMA schema_name VERSION version"); + + pg_and_generic().verified_stmt("CREATE EXTENSION extension_name WITH"); + pg_and_generic().verified_stmt("CREATE EXTENSION extension_name SCHEMA schema_name"); + pg_and_generic().verified_stmt("CREATE EXTENSION extension_name VERSION version"); + pg_and_generic().verified_stmt("CREATE EXTENSION extension_name CASCADE"); + pg_and_generic().verified_stmt( + "CREATE EXTENSION IF NOT EXISTS extension_name SCHEMA schema_name VERSION version CASCADE", + ); + pg_and_generic().verified_stmt("CREATE EXTENSION \"uuid-ossp\" VERSION '1.1'"); + pg_and_generic().one_statement_parses_to( + "CREATE EXTENSION pgcrypto WITH VERSION '1.3' SCHEMA exts", + "CREATE EXTENSION pgcrypto WITH SCHEMA exts VERSION '1.3'", + ); + pg_and_generic().one_statement_parses_to( + "CREATE EXTENSION pgcrypto CASCADE SCHEMA exts", + "CREATE EXTENSION pgcrypto SCHEMA exts CASCADE", + ); + + for sql in [ + "CREATE EXTENSION extension_name WITH SCHEMA schema_name", + "CREATE EXTENSION extension_name SCHEMA schema_name", + ] { + match pg_and_generic().verified_stmt(sql) { + Statement::CreateExtension(CreateExtension { schema, .. }) => { + assert_eq!(schema, Some(Ident::new("schema_name"))); + } + other => panic!("unexpected statement: {other:?}"), + } + } + + for (sql, error) in [ + ( + "CREATE EXTENSION extension_name WITH WITH SCHEMA schema_name", + "Expected: end of statement, found: WITH", + ), + ("CREATE EXTENSION", "Expected: identifier, found: EOF"), + ( + "CREATE EXTENSION e SCHEMA", + "Expected: identifier, found: EOF", + ), + ( + "CREATE EXTENSION e VERSION", + "Expected: identifier, found: EOF", + ), + ] { + assert_eq!( + pg_and_generic().parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError(error.to_string()), + "{sql}" + ); + } + for (sql, keyword) in [ + ("CREATE EXTENSION e SCHEMA a SCHEMA b", "SCHEMA"), + ("CREATE EXTENSION e VERSION a CASCADE VERSION b", "VERSION"), + ( + "CREATE EXTENSION e WITH CASCADE SCHEMA a CASCADE", + "CASCADE", + ), + ] { + assert_eq!( + pg_and_generic().parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError(format!("{keyword} specified more than once")), + ); + } +} + +#[test] +fn display_create_extension_propagates_write_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.contains(self.0) { + Err(fmt::Error) + } else { + Ok(()) + } + } + } + + let statement = + pg_and_generic().verified_stmt("CREATE EXTENSION e WITH SCHEMA s VERSION v CASCADE"); + for rejected in ["CREATE", "WITH", "SCHEMA", "VERSION", "CASCADE"] { + let mut writer = RejectingWriter(rejected); + assert_eq!(write!(writer, "{statement}"), Err(fmt::Error), "{rejected}"); + } } #[test]