diff --git a/CHANGELOG.md b/CHANGELOG.md index d391940b0..dc775ba7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ Check https://github.com/andygrove/sqlparser-rs/commits/master for undocumented - Add serde support to AST structs and enums (#196) - thanks @panarch! - Support `ALTER TABLE ADD COLUMN`, `RENAME COLUMN`, and `RENAME TO` (#203) - thanks @mashuai! - Support `ALTER TABLE DROP COLUMN` (#148) - thanks @ivanceras! +- Support `CREATE TABLE ... AS ...` (#206) - thanks @Dandandan! ### Fixed - Report an error for unterminated string literals (#165) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index cab337920..c3c8a8ebb 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -481,6 +481,7 @@ pub enum Statement { external: bool, file_format: Option, location: Option, + query: Option>, }, /// CREATE INDEX CreateIndex { @@ -645,19 +646,32 @@ impl fmt::Display for Statement { external, file_format, location, + query, } => { + // We want to allow the following options + // Empty column list, allowed by PostgreSQL: + // `CREATE TABLE t ()` + // No columns provided for CREATE TABLE AS: + // `CREATE TABLE t AS SELECT a from t2` + // Columns provided for CREATE TABLE AS: + // `CREATE TABLE t (a INT) AS SELECT a from t2` write!( f, - "CREATE {}TABLE {}{} ({}", - if *external { "EXTERNAL " } else { "" }, - if *if_not_exists { "IF NOT EXISTS " } else { "" }, - name, - display_comma_separated(columns) + "CREATE {external}TABLE {if_not_exists}{name}", + external = if *external { "EXTERNAL " } else { "" }, + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + name = name, )?; - if !constraints.is_empty() { - write!(f, ", {}", display_comma_separated(constraints))?; + if !columns.is_empty() || !constraints.is_empty() { + write!(f, " ({}", display_comma_separated(columns))?; + if !columns.is_empty() && !constraints.is_empty() { + write!(f, ", ")?; + } + write!(f, "{})", display_comma_separated(constraints))?; + } else if query.is_none() { + // PostgreSQL allows `CREATE TABLE t ();`, but requires empty parens + write!(f, " ()")?; } - write!(f, ")")?; if *external { write!( @@ -670,6 +684,9 @@ impl fmt::Display for Statement { if !with_options.is_empty() { write!(f, " WITH ({})", display_comma_separated(with_options))?; } + if let Some(query) = query { + write!(f, " AS {}", query)?; + } Ok(()) } Statement::CreateIndex { diff --git a/src/parser.rs b/src/parser.rs index 543c79a6e..611ae586c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1020,6 +1020,7 @@ impl Parser { external: true, file_format: Some(file_format), location: Some(location), + query: None, }) } @@ -1108,8 +1109,17 @@ impl Parser { let table_name = self.parse_object_name()?; // parse optional column list (schema) let (columns, constraints) = self.parse_columns()?; + + // PostgreSQL supports `WITH ( options )`, before `AS` let with_options = self.parse_with_options()?; + // Parse optional `AS ( query )` + let query = if self.parse_keyword(Keyword::AS) { + Some(Box::new(self.parse_query()?)) + } else { + None + }; + Ok(Statement::CreateTable { name: table_name, columns, @@ -1119,6 +1129,7 @@ impl Parser { external: false, file_format: None, location: None, + query, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index abfa33fdf..9812d5d5a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1044,6 +1044,7 @@ fn parse_create_table() { external: false, file_format: None, location: None, + query: _query, } => { assert_eq!("uk_cities", name.to_string()); assert_eq!( @@ -1177,6 +1178,36 @@ fn parse_drop_schema() { } } +#[test] +fn parse_create_table_as() { + let sql = "CREATE TABLE t AS SELECT * FROM a"; + + match verified_stmt(sql) { + Statement::CreateTable { name, query, .. } => { + assert_eq!(name.to_string(), "t".to_string()); + assert_eq!(query, Some(Box::new(verified_query("SELECT * FROM a")))); + } + _ => unreachable!(), + } + + // BigQuery allows specifying table schema in CTAS + // ANSI SQL and PostgreSQL let you only specify the list of columns + // (without data types) in a CTAS, but we have yet to support that. + let sql = "CREATE TABLE t (a INT, b INT) AS SELECT 1 AS b, 2 AS a"; + match verified_stmt(sql) { + Statement::CreateTable { columns, query, .. } => { + assert_eq!(columns.len(), 2); + assert_eq!(columns[0].to_string(), "a INT".to_string()); + assert_eq!(columns[1].to_string(), "b INT".to_string()); + assert_eq!( + query, + Some(Box::new(verified_query("SELECT 1 AS b, 2 AS a"))) + ); + } + _ => unreachable!(), + } +} + #[test] fn parse_create_table_with_on_delete_on_update_2in_any_order() -> Result<(), ParserError> { let sql = |options: &str| -> String { @@ -1245,6 +1276,7 @@ fn parse_create_external_table() { external, file_format, location, + query: _query, } => { assert_eq!("uk_cities", name.to_string()); assert_eq!( @@ -1307,12 +1339,6 @@ fn parse_create_external_table_lowercase() { assert_matches!(ast, Statement::CreateTable{..}); } -#[test] -fn parse_create_table_empty() { - // Zero-column tables are weird, but supported by at least PostgreSQL. - let _ = verified_stmt("CREATE TABLE t ()"); -} - #[test] fn parse_alter_table() { let add_column = "ALTER TABLE tab ADD COLUMN foo TEXT"; diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index cc6433322..1ac8e384c 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -77,7 +77,7 @@ fn parse_show_columns() { Statement::ShowColumns { extended: false, full: false, - table_name: table_name, + table_name, filter: Some(ShowStatementFilter::Where( mysql_and_generic().verified_expr("1 = 2") )), diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 88e94d01c..4b339744f 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -43,6 +43,7 @@ fn parse_create_table_with_defaults() { external: false, file_format: None, location: None, + query: _query, } => { assert_eq!("public.customer", name.to_string()); assert_eq!( @@ -226,25 +227,47 @@ fn parse_create_table_with_inherit() { pg().verified_stmt(sql); } +#[test] +fn parse_create_table_empty() { + // Zero-column tables are weird, but supported by at least PostgreSQL. + // + let _ = pg_and_generic().verified_stmt("CREATE TABLE t ()"); +} + +#[test] +fn parse_create_table_constraints_only() { + // Zero-column tables can also have constraints in PostgreSQL + let sql = "CREATE TABLE t (CONSTRAINT positive CHECK (2 > 1))"; + let ast = pg_and_generic().verified_stmt(sql); + match ast { + Statement::CreateTable { + name, + columns, + constraints, + .. + } => { + assert_eq!("t", name.to_string()); + assert!(columns.is_empty()); + assert_eq!( + only(constraints).to_string(), + "CONSTRAINT positive CHECK (2 > 1)" + ); + } + _ => unreachable!(), + }; +} + #[test] fn parse_create_table_if_not_exists() { let sql = "CREATE TABLE IF NOT EXISTS uk_cities ()"; - let ast = - pg_and_generic().one_statement_parses_to(sql, "CREATE TABLE IF NOT EXISTS uk_cities ()"); + let ast = pg_and_generic().verified_stmt(sql); match ast { Statement::CreateTable { name, - columns: _columns, - constraints, - with_options, if_not_exists: true, - external: false, - file_format: None, - location: None, + .. } => { assert_eq!("uk_cities", name.to_string()); - assert!(constraints.is_empty()); - assert_eq!(with_options, vec![]); } _ => unreachable!(), }