Postgres: require a column list, AS, PARTITION OF, or LIKE on CREATE TABLE
diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 69bbe35..742cdf5 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs
@@ -1381,6 +1381,11 @@ false } + /// Returns true if the dialect accepts `CREATE TABLE t` with no column list, `AS`, `OF`, `PARTITION OF` or `LIKE` clause after the table name, as a zero-column table. + fn supports_create_table_without_column_list(&self) -> bool { + true + } + /// Returns true if this dialect supports `PROC` as an abbreviation for /// `PROCEDURE` in a `CREATE` statement. fn supports_create_proc_syntax(&self) -> bool {
diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index e49417c..85d6406 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs
@@ -355,6 +355,11 @@ true } + /// PostgreSQL requires one of those forms; a bare table name is a syntax error. + fn supports_create_table_without_column_list(&self) -> bool { + false + } + fn supports_select_wildcard_with_alias(&self) -> bool { true }
diff --git a/src/parser/mod.rs b/src/parser/mod.rs index db54296..bff3411 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs
@@ -8886,7 +8886,22 @@ }; // parse optional column list (schema) + let has_column_list = matches!(self.peek_token_ref().token, Token::LParen); let (columns, constraints) = self.parse_columns()?; + if !has_column_list + && partition_of.is_none() + && like.is_none() + && clone.is_none() + && !self.dialect.supports_create_table_without_column_list() + && !(self.peek_keyword(Keyword::AS) + || (self.dialect.supports_create_table_select() + && self.peek_keyword(Keyword::SELECT))) + { + return self.expected_ref( + "'(', AS, PARTITION OF, or LIKE after CREATE TABLE table name", + self.peek_token_ref(), + ); + } let comment_after_column_def = if dialect_of!(self is HiveDialect) && self.parse_keyword(Keyword::COMMENT) { let next_token = self.next_token();
diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 1e00288..46a5d11 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs
@@ -1368,25 +1368,37 @@ fn parse_bad_if_not_exists() { let res = pg().parse_sql_statements("CREATE TABLE NOT EXISTS uk_cities ()"); assert_eq!( - ParserError::ParserError("Expected: end of statement, found: EXISTS".to_string()), + ParserError::ParserError( + "Expected: '(', AS, PARTITION OF, or LIKE after CREATE TABLE table name, found: EXISTS" + .to_string() + ), res.unwrap_err() ); let res = pg().parse_sql_statements("CREATE TABLE IF EXISTS uk_cities ()"); assert_eq!( - ParserError::ParserError("Expected: end of statement, found: EXISTS".to_string()), + ParserError::ParserError( + "Expected: '(', AS, PARTITION OF, or LIKE after CREATE TABLE table name, found: EXISTS" + .to_string() + ), res.unwrap_err() ); let res = pg().parse_sql_statements("CREATE TABLE IF uk_cities ()"); assert_eq!( - ParserError::ParserError("Expected: end of statement, found: uk_cities".to_string()), + ParserError::ParserError( + "Expected: '(', AS, PARTITION OF, or LIKE after CREATE TABLE table name, found: uk_cities" + .to_string() + ), res.unwrap_err() ); let res = pg().parse_sql_statements("CREATE TABLE IF NOT uk_cities ()"); assert_eq!( - ParserError::ParserError("Expected: end of statement, found: NOT".to_string()), + ParserError::ParserError( + "Expected: '(', AS, PARTITION OF, or LIKE after CREATE TABLE table name, found: NOT" + .to_string() + ), res.unwrap_err() ); } @@ -10202,3 +10214,29 @@ } pg().verified_stmt(r#"SELECT a AS "x" FROM t AS "y""#); } + +#[test] +fn parse_create_table_requires_column_list_or_alternative() { + let expected_err = "sql parser error: Expected: '(', AS, PARTITION OF, or LIKE after CREATE TABLE table name, found: EOF"; + assert_eq!( + pg().parse_sql_statements("CREATE TABLE t") + .unwrap_err() + .to_string(), + expected_err + ); + assert_eq!( + pg().parse_sql_statements("CREATE TEMP TABLE t") + .unwrap_err() + .to_string(), + expected_err + ); + + pg().verified_stmt("CREATE TABLE t ()"); + pg().verified_stmt("CREATE TABLE t (id INT)"); + pg().verified_stmt("CREATE TABLE t AS SELECT 1"); + pg().verified_stmt("CREATE TABLE t PARTITION OF p FOR VALUES IN (1)"); + + let generic = TestedDialects::new(vec![Box::new(GenericDialect {})]); + generic.verified_stmt("CREATE TABLE t ()"); + generic.one_statement_parses_to("CREATE TABLE t", "CREATE TABLE t ()"); +}
diff --git a/tests/sqlparser_teradata.rs b/tests/sqlparser_teradata.rs index 86e7f06..5c4bb07 100644 --- a/tests/sqlparser_teradata.rs +++ b/tests/sqlparser_teradata.rs
@@ -18,7 +18,7 @@ //! Test SQL syntax, specific to [sqlparser::dialect::TeradataDialect]. use sqlparser::dialect::{Dialect, GenericDialect, TeradataDialect}; -use sqlparser::parser::ParserError; +use sqlparser::parser::{Parser, ParserError}; use sqlparser::test_utils::all_dialects_where; use test_utils::TestedDialects; @@ -118,10 +118,17 @@ let unsupported_dialects = all_dialects_where(|d| !d.supports_leading_comma_before_table_options()); - assert_eq!( - unsupported_dialects.parse_sql_statements("CREATE TABLE foo, FALLBACK (id INT)"), - Err(ParserError::ParserError( - "Expected: end of statement, found: ,".to_string() - )) - ); + for dialect in &unsupported_dialects.dialects { + let expected = if dialect.supports_create_table_without_column_list() { + "Expected: end of statement, found: ," + } else { + "Expected: '(', AS, PARTITION OF, or LIKE after CREATE TABLE table name, found: ," + }; + assert_eq!( + Parser::parse_sql(dialect.as_ref(), "CREATE TABLE foo, FALLBACK (id INT)"), + Err(ParserError::ParserError(format!( + "{expected} at Line: 1, Column: 17" + ))) + ); + } }