From b1c4dbbe745b0abf2b67c3a35609b0d3ded8e067 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Wed, 28 Oct 2026 08:44:32 +0100 Subject: [PATCH 1/4] PostgreSQL: Support DROP and COMMENT ON FOREIGN TABLE --- .../postgres_drop_comment_foreign_table.sql | 2 + src/ast/mod.rs | 6 ++ src/parser/mod.rs | 8 ++- tests/sqlparser_postgres.rs | 66 +++++++++++++++++++ 4 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 fuzz/fuzz_seeds/postgres_drop_comment_foreign_table.sql 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..63221cfc5b 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(), ); }; diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index fbb5ef6357..0e2e0b938c 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -11167,3 +11167,69 @@ 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" + ); +} + +#[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" + ); +} From f91dd89189850eb888f7649b3d982388ae7fa3e8 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Wed, 28 Oct 2026 09:54:20 +0100 Subject: [PATCH 2/4] Test error paths and formatter errors --- tests/sqlparser_postgres.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 0e2e0b938c..308e5bcd3a 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -11202,6 +11202,22 @@ fn parse_drop_foreign_table() { .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] @@ -11232,4 +11248,11 @@ fn parse_comment_on_foreign_table() { .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'" + ); } From 8195cac7b977f2decefefe673e8a7c2f2c5b6ff5 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Wed, 28 Oct 2026 09:59:53 +0100 Subject: [PATCH 3/4] Test COMMENT ON FOREIGN TABLE without a name --- tests/sqlparser_postgres.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 308e5bcd3a..552e67af94 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -11255,4 +11255,11 @@ fn parse_comment_on_foreign_table() { .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" + ); } From 248056b96301c61d4014b63f6c6e18c8e2349e88 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Wed, 28 Oct 2026 11:11:36 +0100 Subject: [PATCH 4/4] Reject TEMPORARY, PURGE and ON in DROP FOREIGN TABLE --- src/parser/mod.rs | 6 ++++++ tests/sqlparser_postgres.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 63221cfc5b..7dc92d24ff 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8169,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 552e67af94..e2d0214605 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -11263,3 +11263,29 @@ fn parse_comment_on_foreign_table() { "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}" + ); + } +}