From 364f62f333ece4953b0b594482f67a39ee015740 Mon Sep 17 00:00:00 2001 From: Nickolay Ponomarev Date: Mon, 11 Feb 2019 23:00:41 +0300 Subject: [PATCH] Parse table-valued functions and MSSQL-specific WITH hints 1) Table-valued functions (`FROM possibly_qualified.fn(arg1, ...)`) is not part of ANSI SQL, but is supported in Postgres and MSSQL at least: - "38.5.7. SQL Functions as Table Sources" - `user_defined_function` in "FROM (Transact-SQL)" I've considered renaming TableFactor::Table to something else (Object?), now that it can be a TVF, but couldn't come up with a satisfactory name. 2) "WITH hints" is MSSQL-specific syntax Note that MSSQL supports the following ways of specifying hints, which are parsed with varying degrees of accuracy: - `FROM tab (NOLOCK)` -- deprecated syntax, parsed as a function with a `NOLOCK` argument - `FROM tab C (NOLOCK)` -- deprecated syntax, rejected ATM - `FROM TAB C WITH (NOLOCK)` -- OK --- src/sqlast/query.rs | 40 ++++++++++++++++++++++++++++-------- src/sqlparser.rs | 42 ++++++++++++++++++++++++++++++-------- tests/sqlparser_generic.rs | 23 ++++++++++++++++++++- 3 files changed, 87 insertions(+), 18 deletions(-) diff --git a/src/sqlast/query.rs b/src/sqlast/query.rs index 50b81841..6668b785 100644 --- a/src/sqlast/query.rs +++ b/src/sqlast/query.rs @@ -183,6 +183,12 @@ pub enum TableFactor { Table { name: SQLObjectName, alias: Option, + /// Arguments of a table-valued function, as supported by Postgres + /// and MSSQL. Note that deprecated MSSQL `FROM foo (NOLOCK)` syntax + /// will also be parsed as `args`. + args: Option>, + /// MSSQL-specific `WITH (...)` hints such as NOLOCK. + with_hints: Vec, }, Derived { subquery: Box, @@ -192,16 +198,32 @@ pub enum TableFactor { impl ToString for TableFactor { fn to_string(&self) -> String { - let (base, alias) = match self { - TableFactor::Table { name, alias } => (name.to_string(), alias), - TableFactor::Derived { subquery, alias } => { - (format!("({})", subquery.to_string()), alias) + match self { + TableFactor::Table { + name, + alias, + args, + with_hints, + } => { + let mut s = name.to_string(); + if let Some(args) = args { + s += &format!("({})", comma_separated_string(args)) + }; + if let Some(alias) = alias { + s += &format!(" AS {}", alias); + } + if !with_hints.is_empty() { + s += &format!(" WITH ({})", comma_separated_string(with_hints)); + } + s + } + TableFactor::Derived { subquery, alias } => { + let mut s = format!("({})", subquery.to_string()); + if let Some(alias) = alias { + s += &format!(" AS {}", alias); + } + s } - }; - if let Some(alias) = alias { - format!("{} AS {}", base, alias) - } else { - base } } } diff --git a/src/sqlparser.rs b/src/sqlparser.rs index 69d694f5..3b6f9c28 100644 --- a/src/sqlparser.rs +++ b/src/sqlparser.rs @@ -242,13 +242,7 @@ impl Parser { pub fn parse_function(&mut self, name: SQLObjectName) -> Result { self.expect_token(&Token::LParen)?; - let args = if self.consume_token(&Token::RParen) { - vec![] - } else { - let args = self.parse_expr_list()?; - self.expect_token(&Token::RParen)?; - args - }; + let args = self.parse_optional_args()?; let over = if self.parse_keyword("OVER") { // TBD: support window names (`OVER mywin`) in place of inline specification self.expect_token(&Token::LParen)?; @@ -1430,8 +1424,30 @@ impl Parser { Ok(TableFactor::Derived { subquery, alias }) } else { let name = self.parse_object_name()?; + // Postgres, MSSQL: table-valued functions: + let args = if self.consume_token(&Token::LParen) { + Some(self.parse_optional_args()?) + } else { + None + }; let alias = self.parse_optional_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; - Ok(TableFactor::Table { name, alias }) + // MSSQL-specific table hints: + let mut with_hints = vec![]; + if self.parse_keyword("WITH") { + if self.consume_token(&Token::LParen) { + with_hints = self.parse_expr_list()?; + self.expect_token(&Token::RParen)?; + } else { + // rewind, as WITH may belong to the next statement's CTE + self.prev_token(); + } + }; + Ok(TableFactor::Table { + name, + alias, + args, + with_hints, + }) } } @@ -1576,6 +1592,16 @@ impl Parser { Ok(expr_list) } + pub fn parse_optional_args(&mut self) -> Result, ParserError> { + if self.consume_token(&Token::RParen) { + Ok(vec![]) + } else { + let args = self.parse_expr_list()?; + self.expect_token(&Token::RParen)?; + Ok(args) + } + } + /// Parse a comma-delimited list of projections after SELECT pub fn parse_select_list(&mut self) -> Result, ParserError> { let mut projections: Vec = vec![]; diff --git a/tests/sqlparser_generic.rs b/tests/sqlparser_generic.rs index a505575c..e1e96972 100644 --- a/tests/sqlparser_generic.rs +++ b/tests/sqlparser_generic.rs @@ -641,9 +641,16 @@ fn parse_delimited_identifiers() { ); // check FROM match select.relation.unwrap() { - TableFactor::Table { name, alias } => { + TableFactor::Table { + name, + alias, + args, + with_hints, + } => { assert_eq!(vec![r#""a table""#.to_string()], name.0); assert_eq!(r#""alias""#, alias.unwrap()); + assert!(args.is_none()); + assert!(with_hints.is_empty()); } _ => panic!("Expecting TableFactor::Table"), } @@ -751,6 +758,12 @@ fn parse_simple_case_expression() { ); } +#[test] +fn parse_from_advanced() { + let sql = "SELECT * FROM fn(1, 2) AS foo, schema.bar AS bar WITH (NOLOCK)"; + let _select = verified_only_select(sql); +} + #[test] fn parse_implicit_join() { let sql = "SELECT * FROM t1, t2"; @@ -760,6 +773,8 @@ fn parse_implicit_join() { relation: TableFactor::Table { name: SQLObjectName(vec!["t2".to_string()]), alias: None, + args: None, + with_hints: vec![], }, join_operator: JoinOperator::Implicit }, @@ -776,6 +791,8 @@ fn parse_cross_join() { relation: TableFactor::Table { name: SQLObjectName(vec!["t2".to_string()]), alias: None, + args: None, + with_hints: vec![], }, join_operator: JoinOperator::Cross }, @@ -794,6 +811,8 @@ fn parse_joins_on() { relation: TableFactor::Table { name: SQLObjectName(vec![relation.into()]), alias, + args: None, + with_hints: vec![], }, join_operator: f(JoinConstraint::On(ASTNode::SQLBinaryExpr { left: Box::new(ASTNode::SQLIdentifier("c1".into())), @@ -845,6 +864,8 @@ fn parse_joins_using() { relation: TableFactor::Table { name: SQLObjectName(vec![relation.into()]), alias, + args: None, + with_hints: vec![], }, join_operator: f(JoinConstraint::Using(vec!["c1".into()])), }