Support arbitrary WITH options for CREATE [TABLE|VIEW]

Both Postgres and MSSQL accept this syntax, though the particular
options they accept differ.
This commit is contained in:
Nikhil Benesch 2019-05-21 14:51:56 -04:00
parent 1931c76eb8
commit 69f0082db6
No known key found for this signature in database
GPG key ID: F7386C5DEADABA7F
4 changed files with 140 additions and 4 deletions

View file

@ -379,6 +379,7 @@ pub enum SQLStatement {
name: SQLObjectName, name: SQLObjectName,
query: Box<SQLQuery>, query: Box<SQLQuery>,
materialized: bool, materialized: bool,
with_options: Vec<SQLOption>,
}, },
/// CREATE TABLE /// CREATE TABLE
SQLCreateTable { SQLCreateTable {
@ -387,6 +388,7 @@ pub enum SQLStatement {
/// Optional schema /// Optional schema
columns: Vec<SQLColumnDef>, columns: Vec<SQLColumnDef>,
constraints: Vec<TableConstraint>, constraints: Vec<TableConstraint>,
with_options: Vec<SQLOption>,
external: bool, external: bool,
file_format: Option<FileFormat>, file_format: Option<FileFormat>,
location: Option<String>, location: Option<String>,
@ -473,12 +475,19 @@ impl ToString for SQLStatement {
name, name,
query, query,
materialized, materialized,
with_options,
} => { } => {
let modifier = if *materialized { " MATERIALIZED" } else { "" }; let modifier = if *materialized { " MATERIALIZED" } else { "" };
let with_options = if !with_options.is_empty() {
format!(" WITH ({})", comma_separated_string(with_options))
} else {
"".into()
};
format!( format!(
"CREATE{} VIEW {} AS {}", "CREATE{} VIEW {}{} AS {}",
modifier, modifier,
name.to_string(), name.to_string(),
with_options,
query.to_string() query.to_string()
) )
} }
@ -486,6 +495,7 @@ impl ToString for SQLStatement {
name, name,
columns, columns,
constraints, constraints,
with_options,
external, external,
file_format, file_format,
location, location,
@ -507,6 +517,9 @@ impl ToString for SQLStatement {
location.as_ref().unwrap() location.as_ref().unwrap()
); );
} }
if !with_options.is_empty() {
s += &format!(" WITH ({})", comma_separated_string(with_options));
}
s s
} }
SQLStatement::SQLAlterTable { name, operation } => { SQLStatement::SQLAlterTable { name, operation } => {
@ -670,3 +683,15 @@ impl SQLObjectType {
} }
} }
} }
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct SQLOption {
pub name: SQLIdent,
pub value: Value,
}
impl ToString for SQLOption {
fn to_string(&self) -> String {
format!("{} = {}", self.name.to_string(), self.value.to_string())
}
}

View file

@ -761,6 +761,7 @@ impl Parser {
name: table_name, name: table_name,
columns, columns,
constraints, constraints,
with_options: vec![],
external: true, external: true,
file_format: Some(file_format), file_format: Some(file_format),
location: Some(location), location: Some(location),
@ -774,8 +775,11 @@ impl Parser {
// ANSI SQL and Postgres support RECURSIVE here, but we don't support it either. // ANSI SQL and Postgres support RECURSIVE here, but we don't support it either.
let name = self.parse_object_name()?; let name = self.parse_object_name()?;
// Parenthesized "output" columns list could be handled here. // Parenthesized "output" columns list could be handled here.
// Some dialects allow WITH here, followed by some keywords (e.g. MS SQL) let with_options = if self.parse_keyword("WITH") {
// or `(k1=v1, k2=v2, ...)` (Postgres) self.parse_with_options()?
} else {
vec![]
};
self.expect_keyword("AS")?; self.expect_keyword("AS")?;
let query = Box::new(self.parse_query()?); let query = Box::new(self.parse_query()?);
// Optional `WITH [ CASCADED | LOCAL ] CHECK OPTION` is widely supported here. // Optional `WITH [ CASCADED | LOCAL ] CHECK OPTION` is widely supported here.
@ -783,6 +787,7 @@ impl Parser {
name, name,
query, query,
materialized, materialized,
with_options,
}) })
} }
@ -828,10 +833,17 @@ impl Parser {
// parse optional column list (schema) // parse optional column list (schema)
let (columns, constraints) = self.parse_columns()?; let (columns, constraints) = self.parse_columns()?;
let with_options = if self.parse_keyword("WITH") {
self.parse_with_options()?
} else {
vec![]
};
Ok(SQLStatement::SQLCreateTable { Ok(SQLStatement::SQLCreateTable {
name: table_name, name: table_name,
columns, columns,
constraints, constraints,
with_options,
external: false, external: false,
file_format: None, file_format: None,
location: None, location: None,
@ -941,6 +953,23 @@ impl Parser {
} }
} }
pub fn parse_with_options(&mut self) -> Result<Vec<SQLOption>, ParserError> {
self.expect_token(&Token::LParen)?;
let mut options = vec![];
loop {
let name = self.parse_identifier()?;
self.expect_token(&Token::Eq)?;
let value = self.parse_value()?;
options.push(SQLOption { name, value });
match self.peek_token() {
Some(Token::Comma) => self.next_token(),
_ => break,
};
}
self.expect_token(&Token::RParen)?;
Ok(options)
}
pub fn parse_alter(&mut self) -> Result<SQLStatement, ParserError> { pub fn parse_alter(&mut self) -> Result<SQLStatement, ParserError> {
self.expect_keyword("TABLE")?; self.expect_keyword("TABLE")?;
let _ = self.parse_keyword("ONLY"); let _ = self.parse_keyword("ONLY");

View file

@ -765,6 +765,7 @@ fn parse_create_table() {
name, name,
columns, columns,
constraints, constraints,
with_options,
external: false, external: false,
file_format: None, file_format: None,
location: None, location: None,
@ -787,6 +788,31 @@ fn parse_create_table() {
assert_eq!("lng", c_lng.name); assert_eq!("lng", c_lng.name);
assert_eq!(SQLType::Double, c_lng.data_type); assert_eq!(SQLType::Double, c_lng.data_type);
assert_eq!(true, c_lng.allow_null); assert_eq!(true, c_lng.allow_null);
assert_eq!(with_options, vec![]);
}
_ => unreachable!(),
}
}
#[test]
fn parse_create_table_with_options() {
let sql = "CREATE TABLE t (c int) WITH (foo = 'bar', a = 123)";
match verified_stmt(sql) {
SQLStatement::SQLCreateTable { with_options, .. } => {
assert_eq!(
vec![
SQLOption {
name: "foo".into(),
value: Value::SingleQuotedString("bar".into())
},
SQLOption {
name: "a".into(),
value: Value::Long(123)
},
],
with_options
);
} }
_ => unreachable!(), _ => unreachable!(),
} }
@ -818,6 +844,7 @@ fn parse_create_external_table() {
name, name,
columns, columns,
constraints, constraints,
with_options,
external, external,
file_format, file_format,
location, location,
@ -844,6 +871,8 @@ fn parse_create_external_table() {
assert!(external); assert!(external);
assert_eq!(FileFormat::TEXTFILE, file_format.unwrap()); assert_eq!(FileFormat::TEXTFILE, file_format.unwrap());
assert_eq!("/tmp/example.csv", location.unwrap()); assert_eq!("/tmp/example.csv", location.unwrap());
assert_eq!(with_options, vec![]);
} }
_ => unreachable!(), _ => unreachable!(),
} }
@ -1512,10 +1541,35 @@ fn parse_create_view() {
name, name,
query, query,
materialized, materialized,
with_options,
} => { } => {
assert_eq!("myschema.myview", name.to_string()); assert_eq!("myschema.myview", name.to_string());
assert_eq!("SELECT foo FROM bar", query.to_string()); assert_eq!("SELECT foo FROM bar", query.to_string());
assert!(!materialized); assert!(!materialized);
assert_eq!(with_options, vec![]);
}
_ => unreachable!(),
}
}
#[test]
fn parse_create_view_with_options() {
let sql = "CREATE VIEW v WITH (foo = 'bar', a = 123) AS SELECT 1";
match verified_stmt(sql) {
SQLStatement::SQLCreateView { with_options, .. } => {
assert_eq!(
vec![
SQLOption {
name: "foo".into(),
value: Value::SingleQuotedString("bar".into())
},
SQLOption {
name: "a".into(),
value: Value::Long(123)
},
],
with_options
);
} }
_ => unreachable!(), _ => unreachable!(),
} }
@ -1529,10 +1583,12 @@ fn parse_create_materialized_view() {
name, name,
query, query,
materialized, materialized,
with_options,
} => { } => {
assert_eq!("myschema.myview", name.to_string()); assert_eq!("myschema.myview", name.to_string());
assert_eq!("SELECT foo FROM bar", query.to_string()); assert_eq!("SELECT foo FROM bar", query.to_string());
assert!(materialized); assert!(materialized);
assert_eq!(with_options, vec![]);
} }
_ => unreachable!(), _ => unreachable!(),
} }

View file

@ -18,12 +18,14 @@ fn parse_create_table_with_defaults() {
activebool boolean DEFAULT true NOT NULL, activebool boolean DEFAULT true NOT NULL,
create_date date DEFAULT now()::text NOT NULL, create_date date DEFAULT now()::text NOT NULL,
last_update timestamp without time zone DEFAULT now() NOT NULL, last_update timestamp without time zone DEFAULT now() NOT NULL,
active integer NOT NULL)"; active integer NOT NULL
) WITH (fillfactor = 20, user_catalog_table = true, autovacuum_vacuum_threshold = 100)";
match pg_and_generic().one_statement_parses_to(sql, "") { match pg_and_generic().one_statement_parses_to(sql, "") {
SQLStatement::SQLCreateTable { SQLStatement::SQLCreateTable {
name, name,
columns, columns,
constraints, constraints,
with_options,
external: false, external: false,
file_format: None, file_format: None,
location: None, location: None,
@ -46,6 +48,24 @@ fn parse_create_table_with_defaults() {
assert_eq!("first_name", c_lng.name); assert_eq!("first_name", c_lng.name);
assert_eq!(SQLType::Varchar(Some(45)), c_lng.data_type); assert_eq!(SQLType::Varchar(Some(45)), c_lng.data_type);
assert_eq!(false, c_lng.allow_null); assert_eq!(false, c_lng.allow_null);
assert_eq!(
with_options,
vec![
SQLOption {
name: "fillfactor".into(),
value: Value::Long(20)
},
SQLOption {
name: "user_catalog_table".into(),
value: Value::Boolean(true)
},
SQLOption {
name: "autovacuum_vacuum_threshold".into(),
value: Value::Long(100)
},
]
);
} }
_ => unreachable!(), _ => unreachable!(),
} }
@ -72,6 +92,7 @@ fn parse_create_table_from_pg_dump() {
name, name,
columns, columns,
constraints, constraints,
with_options,
external: false, external: false,
file_format: None, file_format: None,
location: None, location: None,
@ -116,6 +137,8 @@ fn parse_create_table_from_pg_dump() {
])), ])),
c_release_year.data_type c_release_year.data_type
); );
assert_eq!(with_options, vec![]);
} }
_ => unreachable!(), _ => unreachable!(),
} }
@ -135,6 +158,7 @@ fn parse_create_table_with_inherit() {
name, name,
columns, columns,
constraints, constraints,
with_options,
external: false, external: false,
file_format: None, file_format: None,
location: None, location: None,
@ -155,6 +179,8 @@ fn parse_create_table_with_inherit() {
assert_eq!(true, c_name.allow_null); assert_eq!(true, c_name.allow_null);
assert_eq!(false, c_name.is_primary); assert_eq!(false, c_name.is_primary);
assert_eq!(true, c_name.is_unique); assert_eq!(true, c_name.is_unique);
assert_eq!(with_options, vec![]);
} }
_ => unreachable!(), _ => unreachable!(),
} }