From 69f0082db63d1aecdea5794f7065b56b22ffbd04 Mon Sep 17 00:00:00 2001 From: Nikhil Benesch Date: Tue, 21 May 2019 14:51:56 -0400 Subject: [PATCH] Support arbitrary WITH options for CREATE [TABLE|VIEW] Both Postgres and MSSQL accept this syntax, though the particular options they accept differ. --- src/sqlast/mod.rs | 27 +++++++++++++++++- src/sqlparser.rs | 33 ++++++++++++++++++++-- tests/sqlparser_common.rs | 56 +++++++++++++++++++++++++++++++++++++ tests/sqlparser_postgres.rs | 28 ++++++++++++++++++- 4 files changed, 140 insertions(+), 4 deletions(-) diff --git a/src/sqlast/mod.rs b/src/sqlast/mod.rs index c37a139df..8b0d071ce 100644 --- a/src/sqlast/mod.rs +++ b/src/sqlast/mod.rs @@ -379,6 +379,7 @@ pub enum SQLStatement { name: SQLObjectName, query: Box, materialized: bool, + with_options: Vec, }, /// CREATE TABLE SQLCreateTable { @@ -387,6 +388,7 @@ pub enum SQLStatement { /// Optional schema columns: Vec, constraints: Vec, + with_options: Vec, external: bool, file_format: Option, location: Option, @@ -473,12 +475,19 @@ impl ToString for SQLStatement { name, query, materialized, + with_options, } => { let modifier = if *materialized { " MATERIALIZED" } else { "" }; + let with_options = if !with_options.is_empty() { + format!(" WITH ({})", comma_separated_string(with_options)) + } else { + "".into() + }; format!( - "CREATE{} VIEW {} AS {}", + "CREATE{} VIEW {}{} AS {}", modifier, name.to_string(), + with_options, query.to_string() ) } @@ -486,6 +495,7 @@ impl ToString for SQLStatement { name, columns, constraints, + with_options, external, file_format, location, @@ -507,6 +517,9 @@ impl ToString for SQLStatement { location.as_ref().unwrap() ); } + if !with_options.is_empty() { + s += &format!(" WITH ({})", comma_separated_string(with_options)); + } s } 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()) + } +} diff --git a/src/sqlparser.rs b/src/sqlparser.rs index d76994733..0de984292 100644 --- a/src/sqlparser.rs +++ b/src/sqlparser.rs @@ -761,6 +761,7 @@ impl Parser { name: table_name, columns, constraints, + with_options: vec![], external: true, file_format: Some(file_format), location: Some(location), @@ -774,8 +775,11 @@ impl Parser { // ANSI SQL and Postgres support RECURSIVE here, but we don't support it either. let name = self.parse_object_name()?; // Parenthesized "output" columns list could be handled here. - // Some dialects allow WITH here, followed by some keywords (e.g. MS SQL) - // or `(k1=v1, k2=v2, ...)` (Postgres) + let with_options = if self.parse_keyword("WITH") { + self.parse_with_options()? + } else { + vec![] + }; self.expect_keyword("AS")?; let query = Box::new(self.parse_query()?); // Optional `WITH [ CASCADED | LOCAL ] CHECK OPTION` is widely supported here. @@ -783,6 +787,7 @@ impl Parser { name, query, materialized, + with_options, }) } @@ -828,10 +833,17 @@ impl Parser { // parse optional column list (schema) let (columns, constraints) = self.parse_columns()?; + let with_options = if self.parse_keyword("WITH") { + self.parse_with_options()? + } else { + vec![] + }; + Ok(SQLStatement::SQLCreateTable { name: table_name, columns, constraints, + with_options, external: false, file_format: None, location: None, @@ -941,6 +953,23 @@ impl Parser { } } + pub fn parse_with_options(&mut self) -> Result, 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 { self.expect_keyword("TABLE")?; let _ = self.parse_keyword("ONLY"); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 272b17391..f33df954f 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -765,6 +765,7 @@ fn parse_create_table() { name, columns, constraints, + with_options, external: false, file_format: None, location: None, @@ -787,6 +788,31 @@ fn parse_create_table() { assert_eq!("lng", c_lng.name); assert_eq!(SQLType::Double, c_lng.data_type); 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!(), } @@ -818,6 +844,7 @@ fn parse_create_external_table() { name, columns, constraints, + with_options, external, file_format, location, @@ -844,6 +871,8 @@ fn parse_create_external_table() { assert!(external); assert_eq!(FileFormat::TEXTFILE, file_format.unwrap()); assert_eq!("/tmp/example.csv", location.unwrap()); + + assert_eq!(with_options, vec![]); } _ => unreachable!(), } @@ -1512,10 +1541,35 @@ fn parse_create_view() { name, query, materialized, + with_options, } => { assert_eq!("myschema.myview", name.to_string()); assert_eq!("SELECT foo FROM bar", query.to_string()); 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!(), } @@ -1529,10 +1583,12 @@ fn parse_create_materialized_view() { name, query, materialized, + with_options, } => { assert_eq!("myschema.myview", name.to_string()); assert_eq!("SELECT foo FROM bar", query.to_string()); assert!(materialized); + assert_eq!(with_options, vec![]); } _ => unreachable!(), } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 4c1fe3ad2..f8753429c 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -18,12 +18,14 @@ fn parse_create_table_with_defaults() { activebool boolean DEFAULT true NOT NULL, create_date date DEFAULT now()::text 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, "") { SQLStatement::SQLCreateTable { name, columns, constraints, + with_options, external: false, file_format: None, location: None, @@ -46,6 +48,24 @@ fn parse_create_table_with_defaults() { assert_eq!("first_name", c_lng.name); assert_eq!(SQLType::Varchar(Some(45)), c_lng.data_type); 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!(), } @@ -72,6 +92,7 @@ fn parse_create_table_from_pg_dump() { name, columns, constraints, + with_options, external: false, file_format: None, location: None, @@ -116,6 +137,8 @@ fn parse_create_table_from_pg_dump() { ])), c_release_year.data_type ); + + assert_eq!(with_options, vec![]); } _ => unreachable!(), } @@ -135,6 +158,7 @@ fn parse_create_table_with_inherit() { name, columns, constraints, + with_options, external: false, file_format: None, location: None, @@ -155,6 +179,8 @@ fn parse_create_table_with_inherit() { assert_eq!(true, c_name.allow_null); assert_eq!(false, c_name.is_primary); assert_eq!(true, c_name.is_unique); + + assert_eq!(with_options, vec![]); } _ => unreachable!(), }