From fe10fac0ad5d61969986854e327ee1866cb39217 Mon Sep 17 00:00:00 2001 From: Justin Haug Date: Mon, 20 May 2019 19:10:49 -0400 Subject: [PATCH 1/2] Add FETCH and OFFSET support --- src/dialect/keywords.rs | 5 +- src/sqlast/mod.rs | 4 +- src/sqlast/query.rs | 37 +++++++- src/sqlparser.rs | 82 +++++++++++++++++ tests/sqlparser_common.rs | 181 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 305 insertions(+), 4 deletions(-) diff --git a/src/dialect/keywords.rs b/src/dialect/keywords.rs index 02bee1038..fb0873e14 100644 --- a/src/dialect/keywords.rs +++ b/src/dialect/keywords.rs @@ -155,6 +155,7 @@ define_keywords!( EXTRACT, FALSE, FETCH, + FIRST, FILTER, FIRST_VALUE, FLOAT, @@ -229,6 +230,7 @@ define_keywords!( NATURAL, NCHAR, NCLOB, + NEXT, NEW, NO, NONE, @@ -341,6 +343,7 @@ define_keywords!( TABLESAMPLE, TEXT, THEN, + TIES, TIME, TIMESTAMP, TIMEZONE_HOUR, @@ -396,7 +399,7 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[&str] = &[ // Reserved as both a table and a column alias: WITH, SELECT, WHERE, GROUP, ORDER, UNION, EXCEPT, INTERSECT, // Reserved only as a table alias in the `FROM`/`JOIN` clauses: - ON, JOIN, INNER, CROSS, FULL, LEFT, RIGHT, NATURAL, USING, LIMIT, + ON, JOIN, INNER, CROSS, FULL, LEFT, RIGHT, NATURAL, USING, LIMIT, OFFSET, FETCH, ]; /// Can't be used as a column alias, so that `SELECT alias` diff --git a/src/sqlast/mod.rs b/src/sqlast/mod.rs index c959b5243..e8ec8765d 100644 --- a/src/sqlast/mod.rs +++ b/src/sqlast/mod.rs @@ -21,8 +21,8 @@ mod table_key; mod value; pub use self::query::{ - Cte, Join, JoinConstraint, JoinOperator, SQLOrderByExpr, SQLQuery, SQLSelect, SQLSelectItem, - SQLSetExpr, SQLSetOperator, TableFactor, + Cte, Fetch, Join, JoinConstraint, JoinOperator, SQLOrderByExpr, SQLQuery, SQLSelect, + SQLSelectItem, SQLSetExpr, SQLSetOperator, TableFactor, }; pub use self::sqltype::SQLType; pub use self::table_key::{AlterOperation, Key, TableKey}; diff --git a/src/sqlast/query.rs b/src/sqlast/query.rs index e3a2f141c..71a8762a0 100644 --- a/src/sqlast/query.rs +++ b/src/sqlast/query.rs @@ -10,8 +10,12 @@ pub struct SQLQuery { pub body: SQLSetExpr, /// ORDER BY pub order_by: Vec, - /// LIMIT + /// LIMIT { | ALL } pub limit: Option, + /// OFFSET { ROW | ROWS } + pub offset: Option, + /// FETCH { FIRST | NEXT } [ PERCENT ] { ROW | ROWS } | { ONLY | WITH TIES } + pub fetch: Option, } impl ToString for SQLQuery { @@ -27,6 +31,13 @@ impl ToString for SQLQuery { if let Some(ref limit) = self.limit { s += &format!(" LIMIT {}", limit.to_string()); } + if let Some(ref offset) = self.offset { + s += &format!(" OFFSET {} ROWS", offset.to_string()); + } + if let Some(ref fetch) = self.fetch { + s.push(' '); + s += &fetch.to_string(); + } s } } @@ -320,3 +331,27 @@ impl ToString for SQLOrderByExpr { } } } + +#[derive(Debug, Clone, PartialEq)] +pub struct Fetch { + pub with_ties: bool, + pub percent: bool, + pub quantity: Option, +} + +impl ToString for Fetch { + fn to_string(&self) -> String { + let extension = if self.with_ties { "WITH TIES" } else { "ONLY" }; + if let Some(ref quantity) = self.quantity { + let percent = if self.percent { " PERCENT" } else { "" }; + format!( + "FETCH FIRST {}{} ROWS {}", + quantity.to_string(), + percent, + extension + ) + } else { + format!("FETCH FIRST ROWS {}", extension) + } + } +} diff --git a/src/sqlparser.rs b/src/sqlparser.rs index 0db160d5b..872f4da96 100644 --- a/src/sqlparser.rs +++ b/src/sqlparser.rs @@ -684,6 +684,40 @@ impl Parser { true } + /// Look for one of the given keywords and return the one that matches. + #[must_use] + pub fn parse_one_of_keywords(&mut self, keywords: &[&'static str]) -> Option<&'static str> { + for keyword in keywords { + assert!(keywords::ALL_KEYWORDS.contains(keyword)); + } + match self.peek_token() { + Some(Token::SQLWord(ref k)) => keywords + .iter() + .find(|keyword| keyword.eq_ignore_ascii_case(&k.keyword)) + .map(|keyword| { + self.next_token(); + *keyword + }), + _ => None, + } + } + + /// Bail out if the current token is not one of the expected keywords, or consume it if it is + #[must_use] + pub fn expect_one_of_keywords( + &mut self, + keywords: &[&'static str], + ) -> Result<&'static str, ParserError> { + if let Some(keyword) = self.parse_one_of_keywords(keywords) { + Ok(keyword) + } else { + self.expected( + &format!("one of {}", keywords.join(" or ")), + self.peek_token(), + ) + } + } + /// Bail out if the current token is not an expected keyword, or consume it if it is pub fn expect_keyword(&mut self, expected: &'static str) -> Result<(), ParserError> { if self.parse_keyword(expected) { @@ -1279,11 +1313,25 @@ impl Parser { None }; + let offset = if self.parse_keyword("OFFSET") { + Some(self.parse_offset()?) + } else { + None + }; + + let fetch = if self.parse_keyword("FETCH") { + Some(self.parse_fetch()?) + } else { + None + }; + Ok(SQLQuery { ctes, body, limit, order_by, + offset, + fetch, }) } @@ -1655,6 +1703,40 @@ impl Parser { .map(|n| Some(ASTNode::SQLValue(Value::Long(n)))) } } + + /// Parse an OFFSET clause + pub fn parse_offset(&mut self) -> Result { + let value = self + .parse_literal_int() + .map(|n| ASTNode::SQLValue(Value::Long(n)))?; + self.expect_one_of_keywords(&["ROW", "ROWS"])?; + Ok(value) + } + + /// Parse a FETCH clause + pub fn parse_fetch(&mut self) -> Result { + self.expect_one_of_keywords(&["FIRST", "NEXT"])?; + let (quantity, percent) = if self.parse_one_of_keywords(&["ROW", "ROWS"]).is_some() { + (None, false) + } else { + let quantity = self.parse_sql_value()?; + let percent = self.parse_keyword("PERCENT"); + self.expect_one_of_keywords(&["ROW", "ROWS"])?; + (Some(quantity), percent) + }; + let with_ties = if self.parse_keyword("ONLY") { + false + } else if self.parse_keywords(vec!["WITH", "TIES"]) { + true + } else { + return self.expected("one of ONLY or WITH TIES", self.peek_token()); + }; + Ok(Fetch { + with_ties, + percent, + quantity, + }) + } } impl SQLWord { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 2139b34ad..7bcb71156 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1388,6 +1388,187 @@ fn parse_invalid_subquery_without_parens() { ); } +#[test] +fn parse_offset() { + let ast = verified_query("SELECT foo FROM bar OFFSET 2 ROWS"); + assert_eq!(ast.offset, Some(ASTNode::SQLValue(Value::Long(2)))); + let ast = verified_query("SELECT foo FROM bar WHERE foo = 4 OFFSET 2 ROWS"); + assert_eq!(ast.offset, Some(ASTNode::SQLValue(Value::Long(2)))); + let ast = verified_query("SELECT foo FROM bar ORDER BY baz OFFSET 2 ROWS"); + assert_eq!(ast.offset, Some(ASTNode::SQLValue(Value::Long(2)))); + let ast = verified_query("SELECT foo FROM bar WHERE foo = 4 ORDER BY baz OFFSET 2 ROWS"); + assert_eq!(ast.offset, Some(ASTNode::SQLValue(Value::Long(2)))); + let ast = verified_query("SELECT foo FROM (SELECT * FROM bar OFFSET 2 ROWS) OFFSET 2 ROWS"); + assert_eq!(ast.offset, Some(ASTNode::SQLValue(Value::Long(2)))); + match ast.body { + SQLSetExpr::Select(s) => match s.relation { + Some(TableFactor::Derived { subquery, .. }) => { + assert_eq!(subquery.offset, Some(ASTNode::SQLValue(Value::Long(2)))); + } + _ => panic!("Test broke"), + }, + _ => panic!("Test broke"), + } +} + +#[test] +fn parse_singular_row_offset() { + one_statement_parses_to( + "SELECT foo FROM bar OFFSET 1 ROW", + "SELECT foo FROM bar OFFSET 1 ROWS", + ); +} + +#[test] +fn parse_fetch() { + let ast = verified_query("SELECT foo FROM bar FETCH FIRST 2 ROWS ONLY"); + assert_eq!( + ast.fetch, + Some(Fetch { + with_ties: false, + percent: false, + quantity: Some(ASTNode::SQLValue(Value::Long(2))), + }) + ); + let ast = verified_query("SELECT foo FROM bar FETCH FIRST ROWS ONLY"); + assert_eq!( + ast.fetch, + Some(Fetch { + with_ties: false, + percent: false, + quantity: None, + }) + ); + let ast = verified_query("SELECT foo FROM bar WHERE foo = 4 FETCH FIRST 2 ROWS ONLY"); + assert_eq!( + ast.fetch, + Some(Fetch { + with_ties: false, + percent: false, + quantity: Some(ASTNode::SQLValue(Value::Long(2))), + }) + ); + let ast = verified_query("SELECT foo FROM bar ORDER BY baz FETCH FIRST 2 ROWS ONLY"); + assert_eq!( + ast.fetch, + Some(Fetch { + with_ties: false, + percent: false, + quantity: Some(ASTNode::SQLValue(Value::Long(2))), + }) + ); + let ast = verified_query( + "SELECT foo FROM bar WHERE foo = 4 ORDER BY baz FETCH FIRST 2 ROWS WITH TIES", + ); + assert_eq!( + ast.fetch, + Some(Fetch { + with_ties: true, + percent: false, + quantity: Some(ASTNode::SQLValue(Value::Long(2))), + }) + ); + let ast = verified_query("SELECT foo FROM bar FETCH FIRST 50 PERCENT ROWS ONLY"); + assert_eq!( + ast.fetch, + Some(Fetch { + with_ties: false, + percent: true, + quantity: Some(ASTNode::SQLValue(Value::Long(50))), + }) + ); + let ast = verified_query( + "SELECT foo FROM bar WHERE foo = 4 ORDER BY baz OFFSET 2 ROWS FETCH FIRST 2 ROWS ONLY", + ); + assert_eq!(ast.offset, Some(ASTNode::SQLValue(Value::Long(2)))); + assert_eq!( + ast.fetch, + Some(Fetch { + with_ties: false, + percent: false, + quantity: Some(ASTNode::SQLValue(Value::Long(2))), + }) + ); + let ast = verified_query( + "SELECT foo FROM (SELECT * FROM bar FETCH FIRST 2 ROWS ONLY) FETCH FIRST 2 ROWS ONLY", + ); + assert_eq!( + ast.fetch, + Some(Fetch { + with_ties: false, + percent: false, + quantity: Some(ASTNode::SQLValue(Value::Long(2))), + }) + ); + match ast.body { + SQLSetExpr::Select(s) => match s.relation { + Some(TableFactor::Derived { subquery, .. }) => { + assert_eq!( + subquery.fetch, + Some(Fetch { + with_ties: false, + percent: false, + quantity: Some(ASTNode::SQLValue(Value::Long(2))), + }) + ); + } + _ => panic!("Test broke"), + }, + _ => panic!("Test broke"), + } + let ast = verified_query("SELECT foo FROM (SELECT * FROM bar OFFSET 2 ROWS FETCH FIRST 2 ROWS ONLY) OFFSET 2 ROWS FETCH FIRST 2 ROWS ONLY"); + assert_eq!(ast.offset, Some(ASTNode::SQLValue(Value::Long(2)))); + assert_eq!( + ast.fetch, + Some(Fetch { + with_ties: false, + percent: false, + quantity: Some(ASTNode::SQLValue(Value::Long(2))), + }) + ); + match ast.body { + SQLSetExpr::Select(s) => match s.relation { + Some(TableFactor::Derived { subquery, .. }) => { + assert_eq!(subquery.offset, Some(ASTNode::SQLValue(Value::Long(2)))); + assert_eq!( + subquery.fetch, + Some(Fetch { + with_ties: false, + percent: false, + quantity: Some(ASTNode::SQLValue(Value::Long(2))), + }) + ); + } + _ => panic!("Test broke"), + }, + _ => panic!("Test broke"), + } +} + +#[test] +fn parse_fetch_variations() { + one_statement_parses_to( + "SELECT foo FROM bar FETCH FIRST 10 ROW ONLY", + "SELECT foo FROM bar FETCH FIRST 10 ROWS ONLY", + ); + one_statement_parses_to( + "SELECT foo FROM bar FETCH NEXT 10 ROW ONLY", + "SELECT foo FROM bar FETCH FIRST 10 ROWS ONLY", + ); + one_statement_parses_to( + "SELECT foo FROM bar FETCH NEXT 10 ROWS WITH TIES", + "SELECT foo FROM bar FETCH FIRST 10 ROWS WITH TIES", + ); + one_statement_parses_to( + "SELECT foo FROM bar FETCH NEXT ROWS WITH TIES", + "SELECT foo FROM bar FETCH FIRST ROWS WITH TIES", + ); + one_statement_parses_to( + "SELECT foo FROM bar FETCH FIRST ROWS ONLY", + "SELECT foo FROM bar FETCH FIRST ROWS ONLY", + ); +} + #[test] #[should_panic( expected = "Parse results with GenericSqlDialect are different from PostgreSqlDialect" From 2d00ea7187cb80920218de436d6e823e3f846d5b Mon Sep 17 00:00:00 2001 From: Justin Haug Date: Thu, 23 May 2019 17:18:15 -0400 Subject: [PATCH 2/2] Add lateral derived support --- src/sqlast/query.rs | 13 ++++++++++-- src/sqlparser.rs | 9 +++++++- tests/sqlparser_common.rs | 44 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/sqlast/query.rs b/src/sqlast/query.rs index 71a8762a0..bbf359887 100644 --- a/src/sqlast/query.rs +++ b/src/sqlast/query.rs @@ -209,6 +209,7 @@ pub enum TableFactor { with_hints: Vec, }, Derived { + lateral: bool, subquery: Box, alias: Option, }, @@ -235,8 +236,16 @@ impl ToString for TableFactor { } s } - TableFactor::Derived { subquery, alias } => { - let mut s = format!("({})", subquery.to_string()); + TableFactor::Derived { + lateral, + subquery, + alias, + } => { + let mut s = String::new(); + if *lateral { + s += "LATERAL "; + } + s += &format!("({})", subquery.to_string()); if let Some(alias) = alias { s += &format!(" AS {}", alias); } diff --git a/src/sqlparser.rs b/src/sqlparser.rs index 872f4da96..d084fffe2 100644 --- a/src/sqlparser.rs +++ b/src/sqlparser.rs @@ -1464,11 +1464,18 @@ impl Parser { /// A table name or a parenthesized subquery, followed by optional `[AS] alias` pub fn parse_table_factor(&mut self) -> Result { + let lateral = self.parse_keyword("LATERAL"); if self.consume_token(&Token::LParen) { let subquery = Box::new(self.parse_query()?); self.expect_token(&Token::RParen)?; let alias = self.parse_optional_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; - Ok(TableFactor::Derived { subquery, alias }) + Ok(TableFactor::Derived { + lateral, + subquery, + alias, + }) + } else if lateral { + self.expected("subquery after LATERAL", self.peek_token()) } else { let name = self.parse_object_name()?; // Postgres, MSSQL: table-valued functions: diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 7bcb71156..29ca67a16 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1569,6 +1569,50 @@ fn parse_fetch_variations() { ); } +#[test] +fn lateral_derived() { + fn chk(lateral_in: bool) { + let lateral_str = if lateral_in { "LATERAL " } else { "" }; + let sql = format!( + "SELECT * FROM customer LEFT JOIN {}\ + (SELECT * FROM order WHERE order.customer = customer.id LIMIT 3) AS order ON true", + lateral_str + ); + let select = verified_only_select(&sql); + assert_eq!(select.joins.len(), 1); + assert_eq!( + select.joins[0].join_operator, + JoinOperator::LeftOuter(JoinConstraint::On(ASTNode::SQLValue(Value::Boolean(true)))) + ); + if let TableFactor::Derived { + lateral, + ref subquery, + ref alias, + } = select.joins[0].relation + { + assert_eq!(lateral_in, lateral); + assert_eq!(Some("order".to_string()), *alias); + assert_eq!( + subquery.to_string(), + "SELECT * FROM order WHERE order.customer = customer.id LIMIT 3" + ); + } else { + unreachable!() + } + } + chk(false); + chk(true); + + let sql = "SELECT * FROM customer LEFT JOIN LATERAL generate_series(1, customer.id)"; + let res = parse_sql_statements(sql); + assert_eq!( + ParserError::ParserError( + "Expected subquery after LATERAL, found: generate_series".to_string() + ), + res.unwrap_err() + ); +} + #[test] #[should_panic( expected = "Parse results with GenericSqlDialect are different from PostgreSqlDialect"