diff --git a/fuzz/fuzz_seeds/postgres_create_foreign_table_inherits.sql b/fuzz/fuzz_seeds/postgres_create_foreign_table_inherits.sql new file mode 100644 index 0000000000..9b30eab751 --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_create_foreign_table_inherits.sql @@ -0,0 +1 @@ +CREATE FOREIGN TABLE child () INHERITS (parent, public.other) SERVER s \ No newline at end of file diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 1247826c9f..e434c58994 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -6315,6 +6315,8 @@ pub struct CreateForeignTable { /// PostgreSQL's grammar accepts these here, but rejects primary key, unique, /// foreign key and exclusion constraints on a foreign table at execution. pub constraints: Vec, + /// `INHERITS ( parent_table [, ...] )` + pub inherits: Option>, /// The `SERVER server_name` clause. pub server_name: Ident, /// Optional `OPTIONS (key 'value', ...)` clause at the table level. @@ -6338,7 +6340,11 @@ impl fmt::Display for CreateForeignTable { write!(f, ", ")?; } write!(f, "{}", display_comma_separated(&self.constraints))?; - write!(f, ") SERVER {}", self.server_name)?; + f.write_str(")")?; + if let Some(inherits) = &self.inherits { + write!(f, " INHERITS ({})", display_comma_separated(inherits))?; + } + write!(f, " SERVER {}", self.server_name)?; if let Some(options) = &self.options { write!(f, " OPTIONS ({})", display_comma_separated(options))?; } @@ -6414,6 +6420,7 @@ impl Spanned for CreateForeignTable { core::iter::once(self.name.span()) .chain(self.columns.iter().map(|column| column.span())) .chain(self.constraints.iter().map(|constraint| constraint.span())) + .chain(self.inherits.iter().flatten().map(|parent| parent.span())) .chain(core::iter::once(self.server_name.span)) .chain( self.options diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f86406fb26..6f82dfaac8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -21354,8 +21354,7 @@ impl<'a> Parser<'a> { /// Parse a `CREATE FOREIGN TABLE` statement. /// - /// Per-column `OPTIONS ( ... )`, `INHERITS`, and the `PARTITION OF` form are - /// not parsed yet. + /// Per-column `OPTIONS ( ... )` and the `PARTITION OF` form are not parsed yet. /// /// See pub fn parse_create_foreign_table(&mut self) -> Result { @@ -21368,6 +21367,11 @@ impl<'a> Parser<'a> { ); } let (columns, constraints) = self.parse_columns()?; + let inherits = if self.parse_keyword(Keyword::INHERITS) { + Some(self.parse_parenthesized_qualified_column_list(IsOptional::Mandatory, false)?) + } else { + None + }; self.expect_keyword_is(Keyword::SERVER)?; let server_name = self.parse_identifier()?; let options = self.parse_pg_options_clause()?; @@ -21377,6 +21381,7 @@ impl<'a> Parser<'a> { if_not_exists, columns, constraints, + inherits, server_name, options, }) diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index eafc006c04..70b058b422 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -11529,3 +11529,95 @@ fn parse_named_argument_with_assignment_operator() { "sql parser error: Expected: end of statement, found: :=" ); } + +#[test] +fn parse_create_foreign_table_inherits() { + let sql = "CREATE FOREIGN TABLE child (id INT) INHERITS (parent, public.other) SERVER s OPTIONS (schema_name 'public')"; + let Statement::CreateForeignTable(stmt) = pg_and_generic().verified_stmt(sql) else { + unreachable!() + }; + assert_eq!( + stmt.inherits, + Some(vec![ + ObjectName::from(vec![Ident::new("parent")]), + ObjectName::from(vec![Ident::new("public"), Ident::new("other")]), + ]) + ); + + // As written by pg_dump. + pg_and_generic().one_statement_parses_to( + "CREATE FOREIGN TABLE public.child (\n)\nINHERITS (public.parent)\nSERVER s", + "CREATE FOREIGN TABLE public.child () INHERITS (public.parent) SERVER s", + ); + + for (sql, expected) in [ + ( + "CREATE FOREIGN TABLE child INHERITS (parent) SERVER s", + "Expected: '(' before the column list of CREATE FOREIGN TABLE, found: INHERITS", + ), + ( + "CREATE FOREIGN TABLE child () INHERITS () SERVER s", + "Expected: identifier, found: )", + ), + ( + "CREATE FOREIGN TABLE child () INHERITS parent SERVER s", + "Expected: a list of columns in parentheses, found: parent", + ), + ( + "CREATE FOREIGN TABLE child () INHERITS (parent) OPTIONS (schema_name 'public')", + "Expected: SERVER, found: OPTIONS", + ), + ( + "CREATE FOREIGN TABLE child () SERVER s INHERITS (parent)", + "Expected: end of statement, found: INHERITS", + ), + ] { + assert_eq!( + pg_and_generic() + .parse_sql_statements(sql) + .unwrap_err() + .to_string(), + format!("sql parser error: {expected}"), + "{sql}" + ); + } +} + +#[test] +fn parse_create_foreign_table_inherits_span() { + let sql = "CREATE FOREIGN TABLE child (id INT) INHERITS (parent, public.other) SERVER s OPTIONS (schema_name 'public')"; + let mut parser = Parser::new(&PostgreSqlDialect {}) + .try_with_sql(sql) + .unwrap(); + let Statement::CreateForeignTable(stmt) = parser.parse_statement().unwrap() else { + unreachable!() + }; + assert_eq!( + stmt.span(), + Span::new(Location::new(1, 22), Location::new(1, 107)), + ); +} + +#[test] +fn display_create_foreign_table_inherits_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 FOREIGN TABLE child () INHERITS (parent) SERVER s"); + for rejected in [")", " INHERITS (", " SERVER "] { + let mut writer = RejectingWriter(rejected); + assert_eq!(write!(writer, "{stmt}"), Err(fmt::Error), "{rejected:?}"); + } +}