diff --git a/fuzz/fuzz_seeds/postgres_drop_comment_foreign_table.sql b/fuzz/fuzz_seeds/postgres_drop_comment_foreign_table.sql new file mode 100644 index 0000000000..463cdd1876 --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_drop_comment_foreign_table.sql @@ -0,0 +1,2 @@ +COMMENT ON FOREIGN TABLE ft IS 'remote orders'; +DROP FOREIGN TABLE IF EXISTS ft, other CASCADE \ No newline at end of file diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 9b5441738a..a486519680 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2578,6 +2578,8 @@ pub enum CommentObject { Domain, /// An extension. Extension, + /// A foreign table. + ForeignTable, /// A function. Function, /// An index. @@ -2610,6 +2612,7 @@ impl fmt::Display for CommentObject { CommentObject::Database => f.write_str("DATABASE"), CommentObject::Domain => f.write_str("DOMAIN"), CommentObject::Extension => f.write_str("EXTENSION"), + CommentObject::ForeignTable => f.write_str("FOREIGN TABLE"), CommentObject::Function => f.write_str("FUNCTION"), CommentObject::Index => f.write_str("INDEX"), CommentObject::MaterializedView => f.write_str("MATERIALIZED VIEW"), @@ -8757,6 +8760,8 @@ pub enum ObjectType { Collation, /// A table. Table, + /// A foreign table. + ForeignTable, /// A view. View, /// A materialized view. @@ -8788,6 +8793,7 @@ impl fmt::Display for ObjectType { f.write_str(match self { ObjectType::Collation => "COLLATION", ObjectType::Table => "TABLE", + ObjectType::ForeignTable => "FOREIGN TABLE", ObjectType::View => "VIEW", ObjectType::MaterializedView => "MATERIALIZED VIEW", ObjectType::Index => "INDEX", diff --git a/src/parser/mod.rs b/src/parser/mod.rs index fb22bada5b..7dc92d24ff 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -977,6 +977,10 @@ impl<'a> Parser<'a> { Token::Word(w) if w.keyword == Keyword::EXTENSION => { (CommentObject::Extension, self.parse_object_name(false)?) } + Token::Word(w) if w.keyword == Keyword::FOREIGN => { + self.expect_keyword_is(Keyword::TABLE)?; + (CommentObject::ForeignTable, self.parse_object_name(false)?) + } Token::Word(w) if w.keyword == Keyword::FUNCTION => { (CommentObject::Function, self.parse_object_name(false)?) } @@ -8083,6 +8087,8 @@ impl<'a> Parser<'a> { let object_type = if self.parse_keyword(Keyword::TABLE) { ObjectType::Table + } else if self.parse_keywords(&[Keyword::FOREIGN, Keyword::TABLE]) { + ObjectType::ForeignTable } else if self.parse_keyword(Keyword::COLLATION) { ObjectType::Collation } else if self.parse_keyword(Keyword::VIEW) { @@ -8136,7 +8142,7 @@ impl<'a> Parser<'a> { }; } else { return self.expected_ref( - "COLLATION, CONNECTOR, DATABASE, EXTENSION, FUNCTION, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW, USER or WAREHOUSE after DROP", + "COLLATION, CONNECTOR, DATABASE, EXTENSION, FOREIGN TABLE, FUNCTION, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW, USER or WAREHOUSE after DROP", self.peek_token_ref(), ); }; @@ -8163,6 +8169,12 @@ impl<'a> Parser<'a> { } else { None }; + if object_type == ObjectType::ForeignTable && (temporary || purge || table.is_some()) { + return parser_err!( + "Cannot specify TEMPORARY, PURGE, or ON in DROP FOREIGN TABLE", + loc + ); + } Ok(Statement::Drop { object_type, if_exists, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index fbb5ef6357..e2d0214605 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -11167,3 +11167,125 @@ fn parse_alter_table_attach_partition_of() { ); } } + +#[test] +fn parse_drop_foreign_table() { + match pg_and_generic().verified_stmt("DROP FOREIGN TABLE IF EXISTS ft, public.other CASCADE") { + Statement::Drop { + object_type, + if_exists, + names, + cascade, + restrict, + .. + } => { + assert_eq!(object_type, ObjectType::ForeignTable); + assert!(if_exists); + assert_eq!( + names, + vec![ + ObjectName::from(vec![Ident::new("ft")]), + ObjectName::from(vec![Ident::new("public"), Ident::new("other")]), + ] + ); + assert!(cascade); + assert!(!restrict); + } + _ => unreachable!(), + } + pg_and_generic().verified_stmt("DROP FOREIGN TABLE ft RESTRICT"); + + assert_eq!( + pg_and_generic() + .parse_sql_statements("DROP FOREIGN ft") + .unwrap_err() + .to_string(), + "sql parser error: Expected: COLLATION, CONNECTOR, DATABASE, EXTENSION, FOREIGN TABLE, FUNCTION, INDEX, OPERATOR, POLICY, PROCEDURE, ROLE, SCHEMA, SECRET, SEQUENCE, STAGE, TABLE, TRIGGER, TYPE, VIEW, MATERIALIZED VIEW, USER or WAREHOUSE after DROP, found: FOREIGN" + ); + for (sql, expected) in [ + ("DROP FOREIGN TABLE", "Expected: identifier, found: EOF"), + ( + "DROP FOREIGN TABLE ft CASCADE RESTRICT", + "Cannot specify both CASCADE and RESTRICT in DROP", + ), + ] { + assert_eq!( + pg_and_generic() + .parse_sql_statements(sql) + .unwrap_err() + .to_string(), + format!("sql parser error: {expected}"), + "{sql}" + ); + } +} + +#[test] +fn parse_comment_on_foreign_table() { + match pg_and_generic().verified_stmt("COMMENT ON FOREIGN TABLE public.ft IS 'remote orders'") { + Statement::Comment { + object_type, + object_name, + comment, + if_exists, + } => { + assert_eq!(object_type, CommentObject::ForeignTable); + assert_eq!( + object_name, + ObjectName::from(vec![Ident::new("public"), Ident::new("ft")]) + ); + assert_eq!(comment, Some("remote orders".to_string())); + assert!(!if_exists); + } + _ => unreachable!(), + } + pg_and_generic().verified_stmt("COMMENT ON FOREIGN TABLE ft IS NULL"); + + assert_eq!( + pg_and_generic() + .parse_sql_statements("COMMENT ON FOREIGN ft IS 'x'") + .unwrap_err() + .to_string(), + "sql parser error: Expected: TABLE, found: ft" + ); + assert_eq!( + pg_and_generic() + .parse_sql_statements("COMMENT ON FOREIGN TABLE ft 'x'") + .unwrap_err() + .to_string(), + "sql parser error: Expected: IS, found: 'x'" + ); + assert_eq!( + pg_and_generic() + .parse_sql_statements("COMMENT ON FOREIGN TABLE") + .unwrap_err() + .to_string(), + "sql parser error: Expected: identifier, found: EOF" + ); +} + +#[test] +fn parse_drop_foreign_table_rejects_modifiers() { + for sql in [ + "DROP TEMPORARY FOREIGN TABLE ft", + "DROP FOREIGN TABLE ft PURGE", + "DROP FOREIGN TABLE ft ON t", + ] { + for dialect in all_dialects().dialects { + assert!( + Parser::parse_sql(dialect.as_ref(), sql).is_err(), + "{sql} with {dialect:?}" + ); + } + } + for sql in ["DROP FOREIGN TABLE ft PURGE", "DROP FOREIGN TABLE ft ON t"] { + assert_eq!( + pg_and_generic() + .parse_sql_statements(sql) + .unwrap_err() + .to_string(), + "sql parser error: Cannot specify TEMPORARY, PURGE, or ON in DROP FOREIGN TABLE", + "{sql}" + ); + } +}