diff --git a/fuzz/fuzz_seeds/postgres_2762_partition_of_untyped_columns.sql b/fuzz/fuzz_seeds/postgres_2762_partition_of_untyped_columns.sql new file mode 100644 index 0000000000..6e158f8cba --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_2762_partition_of_untyped_columns.sql @@ -0,0 +1 @@ +CREATE TABLE part PARTITION OF root (id WITH OPTIONS NOT NULL, flag) DEFAULT diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f86406fb26..069ded613f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9333,7 +9333,11 @@ impl<'a> Parser<'a> { }; // parse optional column list (schema) - let (columns, constraints) = self.parse_columns()?; + let (columns, constraints) = if partition_of.is_some() { + self.parse_partition_of_columns()? + } else { + self.parse_columns()? + }; let comment_after_column_def = if dialect_of!(self is HiveDialect) && self.parse_keyword(Keyword::COMMENT) { let next_token = self.next_token(); @@ -10014,6 +10018,31 @@ impl<'a> Parser<'a> { /// Parse columns and constraints. pub fn parse_columns(&mut self) -> Result<(Vec, Vec), ParserError> { + self.parse_table_elements(Parser::parse_column_def) + } + + /// Parse the columns and constraints of a `PARTITION OF` table, whose column entries + /// carry no data type. + /// + /// See + fn parse_partition_of_columns( + &mut self, + ) -> Result<(Vec, Vec), ParserError> { + self.parse_table_elements(|parser| { + let name = parser.parse_identifier()?; + let _ = parser.parse_keywords(&[Keyword::WITH, Keyword::OPTIONS]); // [ WITH OPTIONS ] + Ok(ColumnDef { + name, + data_type: DataType::Unspecified, + options: parser.parse_column_option_defs()?, + }) + }) + } + + fn parse_table_elements( + &mut self, + mut parse_column: impl FnMut(&mut Self) -> Result, + ) -> Result<(Vec, Vec), ParserError> { let mut columns = vec![]; let mut constraints = vec![]; if !self.consume_token(&Token::LParen) || self.consume_token(&Token::RParen) { @@ -10024,11 +10053,11 @@ impl<'a> Parser<'a> { if let Some(constraint) = self.parse_optional_table_constraint()? { constraints.push(constraint); } else if let Token::Word(_) = &self.peek_token_ref().token { - columns.push(self.parse_column_def()?); + columns.push(parse_column(self)?); } else if matches!(&self.peek_token_ref().token, Token::SingleQuotedString(_)) && self.dialect.supports_string_literal_column_names() { - columns.push(self.parse_column_def()?); + columns.push(parse_column(self)?); } else { return self.expected_ref( "column name or constraint definition", @@ -10102,29 +10131,32 @@ impl<'a> Parser<'a> { } else { self.parse_data_type()? }; + Ok(ColumnDef { + name: col_name, + data_type, + options: self.parse_column_option_defs()?, + }) + } + + fn parse_column_option_defs(&mut self) -> Result, ParserError> { let mut options = vec![]; loop { if self.parse_keyword(Keyword::CONSTRAINT) { let name = Some(self.parse_identifier()?); - if let Some(option) = self.parse_optional_column_option()? { - options.push(ColumnOptionDef { name, option }); - } else { + let Some(option) = self.parse_optional_column_option()? else { return self.expected_ref( "constraint details after CONSTRAINT ", self.peek_token_ref(), ); - } + }; + options.push(ColumnOptionDef { name, option }); } else if let Some(option) = self.parse_optional_column_option()? { options.push(ColumnOptionDef { name: None, option }); } else { break; }; } - Ok(ColumnDef { - name: col_name, - data_type, - options, - }) + Ok(options) } fn is_column_type_sqlite_unspecified(&mut self) -> bool { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 7c5ac8712d..50997248f0 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -11495,3 +11495,74 @@ fn display_create_function_begin_atomic_propagates_write_errors() { assert_eq!(write!(writer, "{stmt}"), Err(fmt::Error), "{rejected:?}"); } } + +#[test] +fn parse_create_table_partition_of_untyped_columns() { + let column = |name: &str, options: Vec| ColumnDef { + name: Ident::new(name), + data_type: DataType::Unspecified, + options, + }; + let option = |option| ColumnOptionDef { name: None, option }; + let create_table = |sql: &str| match pg_and_generic().verified_stmt(sql) { + Statement::CreateTable(create_table) => create_table, + stmt => panic!("unexpected statement: {stmt:?}"), + }; + + assert_eq!( + create_table( + "CREATE TABLE part PARTITION OF root (id NOT NULL, note DEFAULT 'x', flag) DEFAULT" + ) + .columns, + vec![ + column("id", vec![option(ColumnOption::NotNull)]), + column( + "note", + vec![option(ColumnOption::Default(Expr::value( + Value::SingleQuotedString("x".to_string()) + )))] + ), + column("flag", vec![]), + ] + ); + + let stmt = create_table( + "CREATE TABLE part PARTITION OF root (id CONSTRAINT id_present NOT NULL, CONSTRAINT positive CHECK (id > 0)) FOR VALUES IN (1)", + ); + assert_eq!( + stmt.columns, + vec![column( + "id", + vec![ColumnOptionDef { + name: Some(Ident::new("id_present")), + option: ColumnOption::NotNull, + }] + )] + ); + assert_eq!(stmt.constraints.len(), 1); + + pg_and_generic().one_statement_parses_to( + "CREATE TABLE part PARTITION OF root (id WITH OPTIONS NOT NULL, flag WITH OPTIONS) DEFAULT", + "CREATE TABLE part PARTITION OF root (id NOT NULL, flag) DEFAULT", + ); + + for (sql, expected) in [ + ( + "CREATE TABLE part PARTITION OF root (id INT) DEFAULT", + "Expected: ',' or ')' after column definition, found: INT", + ), + ( + "CREATE TABLE part PARTITION OF root (id WITH NOT NULL) DEFAULT", + "Expected: ',' or ')' after column definition, found: WITH", + ), + ] { + assert_eq!( + pg_and_generic() + .parse_sql_statements(sql) + .unwrap_err() + .to_string(), + format!("sql parser error: {expected}"), + "{sql}" + ); + } +}