Skip to content

Add CREATE TABLE AS support #206

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jun 23, 2020
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
33 changes: 25 additions & 8 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,7 @@ pub enum Statement {
external: bool,
file_format: Option<FileFormat>,
location: Option<String>,
query: Option<Box<Query>>,
},
/// CREATE INDEX
CreateIndex {
Expand Down Expand Up @@ -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!(
Expand All @@ -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 {
Expand Down
11 changes: 11 additions & 0 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,7 @@ impl Parser {
external: true,
file_format: Some(file_format),
location: Some(location),
query: None,
})
}

Expand Down Expand Up @@ -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,
Expand All @@ -1119,6 +1129,7 @@ impl Parser {
external: false,
file_format: None,
location: None,
query,
})
}

Expand Down
38 changes: 32 additions & 6 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -1245,6 +1276,7 @@ fn parse_create_external_table() {
external,
file_format,
location,
query: _query,
} => {
assert_eq!("uk_cities", name.to_string());
assert_eq!(
Expand Down Expand Up @@ -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";
Expand Down
2 changes: 1 addition & 1 deletion tests/sqlparser_mysql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)),
Expand Down
43 changes: 33 additions & 10 deletions tests/sqlparser_postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down Expand Up @@ -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.
// <https://github.com/andygrove/sqlparser-rs/pull/94>
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!(),
}
Expand Down