From 24aa3124e535a3a2d34315d058d05dc2b87a7bff Mon Sep 17 00:00:00 2001 From: Dandandan Date: Sat, 20 Jun 2020 23:09:44 +0200 Subject: [PATCH 01/12] Add CREATE TABLE AS support --- src/ast/mod.rs | 21 +++++++++++++++------ src/parser.rs | 8 ++++++++ tests/sqlparser_common.rs | 15 +++++++++++++++ tests/sqlparser_postgres.rs | 2 ++ 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index cab337920..c18224f74 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,24 @@ impl fmt::Display for Statement { external, file_format, location, + query, } => { + let include_parens = query.is_none() || !columns.is_empty(); 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} {lparen}{columns}", + external = if *external { "EXTERNAL " } else { "" }, + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + name = name, + lparen = if include_parens { "(" } else { "" }, + columns = display_comma_separated(columns), )?; if !constraints.is_empty() { write!(f, ", {}", display_comma_separated(constraints))?; } - write!(f, ")")?; + if include_parens { + write!(f, ")")?; + } if *external { write!( @@ -670,6 +676,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..87309a946 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, }) } @@ -1110,6 +1111,12 @@ impl Parser { let (columns, constraints) = self.parse_columns()?; let with_options = self.parse_with_options()?; + 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 +1126,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..9d75f905b 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,19 @@ 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!(), + } +} + #[test] fn parse_create_table_with_on_delete_on_update_2in_any_order() -> Result<(), ParserError> { let sql = |options: &str| -> String { @@ -1245,6 +1259,7 @@ fn parse_create_external_table() { external, file_format, location, + query, } => { assert_eq!("uk_cities", name.to_string()); assert_eq!( diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 88e94d01c..e256f0b59 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, } => { assert_eq!("public.customer", name.to_string()); assert_eq!( @@ -241,6 +242,7 @@ fn parse_create_table_if_not_exists() { external: false, file_format: None, location: None, + query, } => { assert_eq!("uk_cities", name.to_string()); assert!(constraints.is_empty()); From c67bed20b5b2f0cf28c33fd7ba00c4fe3366d06c Mon Sep 17 00:00:00 2001 From: Dandandan Date: Sat, 20 Jun 2020 23:13:02 +0200 Subject: [PATCH 02/12] Fix linting issue --- src/ast/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index c18224f74..9e067cf1d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -677,7 +677,7 @@ impl fmt::Display for Statement { write!(f, " WITH ({})", display_comma_separated(with_options))?; } if let Some(query) = query { - write!(f, "AS {}", query); + write!(f, "AS {}", query)?; } Ok(()) } From baf4dc4183cce00c2bf71bd8a4bf3f34c966c7a0 Mon Sep 17 00:00:00 2001 From: Dandandan Date: Sat, 20 Jun 2020 23:25:01 +0200 Subject: [PATCH 03/12] Fix linting issue --- tests/sqlparser_common.rs | 2 +- tests/sqlparser_postgres.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 9d75f905b..600c97ee9 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1259,7 +1259,7 @@ fn parse_create_external_table() { external, file_format, location, - query, + query: _query, } => { assert_eq!("uk_cities", name.to_string()); assert_eq!( diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index e256f0b59..f850d4fc2 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -43,7 +43,7 @@ fn parse_create_table_with_defaults() { external: false, file_format: None, location: None, - query, + query: _query, } => { assert_eq!("public.customer", name.to_string()); assert_eq!( @@ -242,7 +242,7 @@ fn parse_create_table_if_not_exists() { external: false, file_format: None, location: None, - query, + query: _query, } => { assert_eq!("uk_cities", name.to_string()); assert!(constraints.is_empty()); From 7dd2578bff6ac60c91104fad2c02f73b69262943 Mon Sep 17 00:00:00 2001 From: Dandandan Date: Sat, 20 Jun 2020 23:27:02 +0200 Subject: [PATCH 04/12] Fix new clippy warning --- tests/sqlparser_mysql.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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") )), From 058dca2238c2105adb829d3a439eab63f5d395a9 Mon Sep 17 00:00:00 2001 From: Dandandan Date: Mon, 22 Jun 2020 21:29:47 +0200 Subject: [PATCH 05/12] Change ordering, add documentation for display implementation --- src/ast/mod.rs | 7 +++++++ src/parser.rs | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 9e067cf1d..18de53a26 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -648,6 +648,13 @@ impl fmt::Display for Statement { 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 let include_parens = query.is_none() || !columns.is_empty(); write!( f, diff --git a/src/parser.rs b/src/parser.rs index 87309a946..4cb241232 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1109,7 +1109,6 @@ impl Parser { let table_name = self.parse_object_name()?; // parse optional column list (schema) let (columns, constraints) = self.parse_columns()?; - let with_options = self.parse_with_options()?; let query = if self.parse_keyword(Keyword::AS) { Some(Box::new(self.parse_query()?)) @@ -1117,6 +1116,8 @@ impl Parser { None }; + let with_options = self.parse_with_options()?; + Ok(Statement::CreateTable { name: table_name, columns, From e7ce5c506507b6aeeae7ef40efa68a23f16893e6 Mon Sep 17 00:00:00 2001 From: Dandandan Date: Mon, 22 Jun 2020 21:43:00 +0200 Subject: [PATCH 06/12] Add documentation about ordering in CREATE TABLE --- src/parser.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 4cb241232..611ae586c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1110,14 +1110,16 @@ impl Parser { // 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 }; - let with_options = self.parse_with_options()?; - Ok(Statement::CreateTable { name: table_name, columns, From 18f5189dd94f365d175b8ca1ca6d2e99e1b3d847 Mon Sep 17 00:00:00 2001 From: Dandandan Date: Tue, 23 Jun 2020 08:15:59 +0200 Subject: [PATCH 07/12] Extract columns/constraints to other block to simplify --- src/ast/mod.rs | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 18de53a26..93c06dc64 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -650,28 +650,29 @@ impl fmt::Display for Statement { } => { // We want to allow the following options // Empty column list, allowed by PostgreSQL: - // CREATE TABLE t () + // `CREATE TABLE t ()` // No columns provided for CREATE TABLE AS: - // CREATE TABLE t AS SELECT a from t2 + // `CREATE TABLE t AS SELECT a from t2` // Columns provided for CREATE TABLE AS: - // CREATE TABLE t (a INT) AS SELECT a from t2 - let include_parens = query.is_none() || !columns.is_empty(); + // `CREATE TABLE t (a INT) AS SELECT a from t2` write!( f, - "CREATE {external}TABLE {if_not_exists}{name} {lparen}{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, - lparen = if include_parens { "(" } else { "" }, - columns = display_comma_separated(columns), )?; - if !constraints.is_empty() { - write!(f, ", {}", display_comma_separated(constraints))?; - } - if include_parens { - write!(f, ")")?; + 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, "()")?; } - + if *external { write!( f, From 3b519913e3c51b370f97cf379c6c9f8efac64aee Mon Sep 17 00:00:00 2001 From: Dandandan Date: Tue, 23 Jun 2020 08:17:00 +0200 Subject: [PATCH 08/12] Formatting --- src/ast/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 93c06dc64..dc2fe50e6 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -669,10 +669,10 @@ impl fmt::Display for Statement { } write!(f, "{})", display_comma_separated(constraints))?; } else if query.is_none() { - // PostgreSQL allows `CREATE TABLE t ();`, but requires empty parens + // PostgreSQL allows `CREATE TABLE t ();`, but requires empty parens write!(f, "()")?; } - + if *external { write!( f, From f4a0b8a971d5366f1b0d8accf38b60f3838987c4 Mon Sep 17 00:00:00 2001 From: Dandandan Date: Tue, 23 Jun 2020 08:30:46 +0200 Subject: [PATCH 09/12] Include one more space for consistency --- src/ast/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index dc2fe50e6..a67b7fdb4 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -685,7 +685,7 @@ impl fmt::Display for Statement { write!(f, " WITH ({})", display_comma_separated(with_options))?; } if let Some(query) = query { - write!(f, "AS {}", query)?; + write!(f, " AS {}", query)?; } Ok(()) } From 781f6b171873e4b6422263686f0ea5a466d7bfb0 Mon Sep 17 00:00:00 2001 From: Dandandan Date: Tue, 23 Jun 2020 09:40:48 +0200 Subject: [PATCH 10/12] Add spacing as start of different blocks instead --- src/ast/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index a67b7fdb4..c3c8a8ebb 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -657,20 +657,20 @@ impl fmt::Display for Statement { // `CREATE TABLE t (a INT) AS SELECT a from t2` write!( f, - "CREATE {external}TABLE {if_not_exists}{name} ", + "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 !columns.is_empty() || !constraints.is_empty() { - write!(f, "({}", display_comma_separated(columns))?; + 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 { From b989d51ad5f4dfc885d50117797f45e995a22822 Mon Sep 17 00:00:00 2001 From: Nickolay Ponomarev Date: Tue, 23 Jun 2020 15:38:39 +0300 Subject: [PATCH 11/12] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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) From 0bdeae69042352ff292ba8d1efc2f624374e399e Mon Sep 17 00:00:00 2001 From: Nickolay Ponomarev Date: Tue, 23 Jun 2020 15:42:29 +0300 Subject: [PATCH 12/12] Update tests Add a test for CTAS with schema and note it's bigquery-only Add a constraints-only schema and note it's postgresql-only Simplify the parse_create_table_if_not_exists test to focus on `IF NOT EXISTS` parsing --- tests/sqlparser_common.rs | 23 ++++++++++++++------ tests/sqlparser_postgres.rs | 43 +++++++++++++++++++++++++++---------- 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 600c97ee9..9812d5d5a 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1189,6 +1189,23 @@ fn parse_create_table_as() { } _ => 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] @@ -1322,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_postgres.rs b/tests/sqlparser_postgres.rs index f850d4fc2..4b339744f 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -227,26 +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, - query: _query, + .. } => { assert_eq!("uk_cities", name.to_string()); - assert!(constraints.is_empty()); - assert_eq!(with_options, vec![]); } _ => unreachable!(), }