Skip to content

Commit 15d5f71

Browse files
Dandandannickolay
andauthored
Add CREATE TABLE AS support (#206)
We parse it as a regular `CREATE TABLE` statement followed by an `AS <query>`, which is how BigQuery works: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_table_statement ANSI SQL and PostgreSQL only support a plain list of columns after the table name in a CTAS `CREATE TABLE t (a) AS SELECT a FROM foo` We currently only allow specifying a full schema with data types, or omitting it altogether. https://www.postgresql.org/docs/12/sql-createtableas.html https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#as-subquery-clause Finally, when no schema is specified, we print empty parens after a plain `CREATE TABLE t ();` as required by PostgreSQL, but skip them in a CTAS: `CREATE TABLE t AS ...`. This affects serialization only, the parser allows omitting the schema in a regular `CREATE TABLE` too since the first release of the parser: https://github.com/benesch/sqlparser-rs/blame/7d27abdfb4ac4a0980d79b12d9d8ef156434e984/src/sqlparser.rs#L325-L332 Co-authored-by: Nickolay Ponomarev <[email protected]>
1 parent 26361fd commit 15d5f71

File tree

6 files changed

+103
-25
lines changed

6 files changed

+103
-25
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Check https://github.com/andygrove/sqlparser-rs/commits/master for undocumented
3232
- Add serde support to AST structs and enums (#196) - thanks @panarch!
3333
- Support `ALTER TABLE ADD COLUMN`, `RENAME COLUMN`, and `RENAME TO` (#203) - thanks @mashuai!
3434
- Support `ALTER TABLE DROP COLUMN` (#148) - thanks @ivanceras!
35+
- Support `CREATE TABLE ... AS ...` (#206) - thanks @Dandandan!
3536

3637
### Fixed
3738
- Report an error for unterminated string literals (#165)

src/ast/mod.rs

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,7 @@ pub enum Statement {
481481
external: bool,
482482
file_format: Option<FileFormat>,
483483
location: Option<String>,
484+
query: Option<Box<Query>>,
484485
},
485486
/// CREATE INDEX
486487
CreateIndex {
@@ -645,19 +646,32 @@ impl fmt::Display for Statement {
645646
external,
646647
file_format,
647648
location,
649+
query,
648650
} => {
651+
// We want to allow the following options
652+
// Empty column list, allowed by PostgreSQL:
653+
// `CREATE TABLE t ()`
654+
// No columns provided for CREATE TABLE AS:
655+
// `CREATE TABLE t AS SELECT a from t2`
656+
// Columns provided for CREATE TABLE AS:
657+
// `CREATE TABLE t (a INT) AS SELECT a from t2`
649658
write!(
650659
f,
651-
"CREATE {}TABLE {}{} ({}",
652-
if *external { "EXTERNAL " } else { "" },
653-
if *if_not_exists { "IF NOT EXISTS " } else { "" },
654-
name,
655-
display_comma_separated(columns)
660+
"CREATE {external}TABLE {if_not_exists}{name}",
661+
external = if *external { "EXTERNAL " } else { "" },
662+
if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" },
663+
name = name,
656664
)?;
657-
if !constraints.is_empty() {
658-
write!(f, ", {}", display_comma_separated(constraints))?;
665+
if !columns.is_empty() || !constraints.is_empty() {
666+
write!(f, " ({}", display_comma_separated(columns))?;
667+
if !columns.is_empty() && !constraints.is_empty() {
668+
write!(f, ", ")?;
669+
}
670+
write!(f, "{})", display_comma_separated(constraints))?;
671+
} else if query.is_none() {
672+
// PostgreSQL allows `CREATE TABLE t ();`, but requires empty parens
673+
write!(f, " ()")?;
659674
}
660-
write!(f, ")")?;
661675

662676
if *external {
663677
write!(
@@ -670,6 +684,9 @@ impl fmt::Display for Statement {
670684
if !with_options.is_empty() {
671685
write!(f, " WITH ({})", display_comma_separated(with_options))?;
672686
}
687+
if let Some(query) = query {
688+
write!(f, " AS {}", query)?;
689+
}
673690
Ok(())
674691
}
675692
Statement::CreateIndex {

src/parser.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,6 +1020,7 @@ impl Parser {
10201020
external: true,
10211021
file_format: Some(file_format),
10221022
location: Some(location),
1023+
query: None,
10231024
})
10241025
}
10251026

@@ -1108,8 +1109,17 @@ impl Parser {
11081109
let table_name = self.parse_object_name()?;
11091110
// parse optional column list (schema)
11101111
let (columns, constraints) = self.parse_columns()?;
1112+
1113+
// PostgreSQL supports `WITH ( options )`, before `AS`
11111114
let with_options = self.parse_with_options()?;
11121115

1116+
// Parse optional `AS ( query )`
1117+
let query = if self.parse_keyword(Keyword::AS) {
1118+
Some(Box::new(self.parse_query()?))
1119+
} else {
1120+
None
1121+
};
1122+
11131123
Ok(Statement::CreateTable {
11141124
name: table_name,
11151125
columns,
@@ -1119,6 +1129,7 @@ impl Parser {
11191129
external: false,
11201130
file_format: None,
11211131
location: None,
1132+
query,
11221133
})
11231134
}
11241135

tests/sqlparser_common.rs

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,6 +1044,7 @@ fn parse_create_table() {
10441044
external: false,
10451045
file_format: None,
10461046
location: None,
1047+
query: _query,
10471048
} => {
10481049
assert_eq!("uk_cities", name.to_string());
10491050
assert_eq!(
@@ -1177,6 +1178,36 @@ fn parse_drop_schema() {
11771178
}
11781179
}
11791180

1181+
#[test]
1182+
fn parse_create_table_as() {
1183+
let sql = "CREATE TABLE t AS SELECT * FROM a";
1184+
1185+
match verified_stmt(sql) {
1186+
Statement::CreateTable { name, query, .. } => {
1187+
assert_eq!(name.to_string(), "t".to_string());
1188+
assert_eq!(query, Some(Box::new(verified_query("SELECT * FROM a"))));
1189+
}
1190+
_ => unreachable!(),
1191+
}
1192+
1193+
// BigQuery allows specifying table schema in CTAS
1194+
// ANSI SQL and PostgreSQL let you only specify the list of columns
1195+
// (without data types) in a CTAS, but we have yet to support that.
1196+
let sql = "CREATE TABLE t (a INT, b INT) AS SELECT 1 AS b, 2 AS a";
1197+
match verified_stmt(sql) {
1198+
Statement::CreateTable { columns, query, .. } => {
1199+
assert_eq!(columns.len(), 2);
1200+
assert_eq!(columns[0].to_string(), "a INT".to_string());
1201+
assert_eq!(columns[1].to_string(), "b INT".to_string());
1202+
assert_eq!(
1203+
query,
1204+
Some(Box::new(verified_query("SELECT 1 AS b, 2 AS a")))
1205+
);
1206+
}
1207+
_ => unreachable!(),
1208+
}
1209+
}
1210+
11801211
#[test]
11811212
fn parse_create_table_with_on_delete_on_update_2in_any_order() -> Result<(), ParserError> {
11821213
let sql = |options: &str| -> String {
@@ -1245,6 +1276,7 @@ fn parse_create_external_table() {
12451276
external,
12461277
file_format,
12471278
location,
1279+
query: _query,
12481280
} => {
12491281
assert_eq!("uk_cities", name.to_string());
12501282
assert_eq!(
@@ -1307,12 +1339,6 @@ fn parse_create_external_table_lowercase() {
13071339
assert_matches!(ast, Statement::CreateTable{..});
13081340
}
13091341

1310-
#[test]
1311-
fn parse_create_table_empty() {
1312-
// Zero-column tables are weird, but supported by at least PostgreSQL.
1313-
let _ = verified_stmt("CREATE TABLE t ()");
1314-
}
1315-
13161342
#[test]
13171343
fn parse_alter_table() {
13181344
let add_column = "ALTER TABLE tab ADD COLUMN foo TEXT";

tests/sqlparser_mysql.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ fn parse_show_columns() {
7777
Statement::ShowColumns {
7878
extended: false,
7979
full: false,
80-
table_name: table_name,
80+
table_name,
8181
filter: Some(ShowStatementFilter::Where(
8282
mysql_and_generic().verified_expr("1 = 2")
8383
)),

tests/sqlparser_postgres.rs

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ fn parse_create_table_with_defaults() {
4343
external: false,
4444
file_format: None,
4545
location: None,
46+
query: _query,
4647
} => {
4748
assert_eq!("public.customer", name.to_string());
4849
assert_eq!(
@@ -226,25 +227,47 @@ fn parse_create_table_with_inherit() {
226227
pg().verified_stmt(sql);
227228
}
228229

230+
#[test]
231+
fn parse_create_table_empty() {
232+
// Zero-column tables are weird, but supported by at least PostgreSQL.
233+
// <https://github.com/andygrove/sqlparser-rs/pull/94>
234+
let _ = pg_and_generic().verified_stmt("CREATE TABLE t ()");
235+
}
236+
237+
#[test]
238+
fn parse_create_table_constraints_only() {
239+
// Zero-column tables can also have constraints in PostgreSQL
240+
let sql = "CREATE TABLE t (CONSTRAINT positive CHECK (2 > 1))";
241+
let ast = pg_and_generic().verified_stmt(sql);
242+
match ast {
243+
Statement::CreateTable {
244+
name,
245+
columns,
246+
constraints,
247+
..
248+
} => {
249+
assert_eq!("t", name.to_string());
250+
assert!(columns.is_empty());
251+
assert_eq!(
252+
only(constraints).to_string(),
253+
"CONSTRAINT positive CHECK (2 > 1)"
254+
);
255+
}
256+
_ => unreachable!(),
257+
};
258+
}
259+
229260
#[test]
230261
fn parse_create_table_if_not_exists() {
231262
let sql = "CREATE TABLE IF NOT EXISTS uk_cities ()";
232-
let ast =
233-
pg_and_generic().one_statement_parses_to(sql, "CREATE TABLE IF NOT EXISTS uk_cities ()");
263+
let ast = pg_and_generic().verified_stmt(sql);
234264
match ast {
235265
Statement::CreateTable {
236266
name,
237-
columns: _columns,
238-
constraints,
239-
with_options,
240267
if_not_exists: true,
241-
external: false,
242-
file_format: None,
243-
location: None,
268+
..
244269
} => {
245270
assert_eq!("uk_cities", name.to_string());
246-
assert!(constraints.is_empty());
247-
assert_eq!(with_options, vec![]);
248271
}
249272
_ => unreachable!(),
250273
}

0 commit comments

Comments
 (0)