Add support for MSSQL's SELECT TOP N syntax (#150)

Add support for MSSQL SELECT TOP (N) [PERCENT] [WITH TIES] syntax.
This commit is contained in:
Alex Kyllo 2020-01-12 20:20:48 -08:00 committed by Nikhil Benesch
parent 4cdd6e2f84
commit 172ba42001
7 changed files with 126 additions and 10 deletions

View file

@ -27,7 +27,7 @@ pub use self::ddl::{
pub use self::operator::{BinaryOperator, UnaryOperator};
pub use self::query::{
Cte, Fetch, Join, JoinConstraint, JoinOperator, OrderByExpr, Query, Select, SelectItem,
SetExpr, SetOperator, TableAlias, TableFactor, TableWithJoins, Values,
SetExpr, SetOperator, TableAlias, TableFactor, TableWithJoins, Top, Values,
};
pub use self::value::{DateTimeField, Value};

View file

@ -114,6 +114,8 @@ impl fmt::Display for SetOperator {
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Select {
pub distinct: bool,
/// MSSQL syntax: `TOP (<N>) [ PERCENT ] [ WITH TIES ]`
pub top: Option<Top>,
/// projection expressions
pub projection: Vec<SelectItem>,
/// FROM
@ -128,12 +130,11 @@ pub struct Select {
impl fmt::Display for Select {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"SELECT{} {}",
if self.distinct { " DISTINCT" } else { "" },
display_comma_separated(&self.projection)
)?;
write!(f, "SELECT{}", if self.distinct { " DISTINCT" } else { "" })?;
if let Some(ref top) = self.top {
write!(f, " {}", top)?;
}
write!(f, " {}", display_comma_separated(&self.projection))?;
if !self.from.is_empty() {
write!(f, " FROM {}", display_comma_separated(&self.from))?;
}
@ -408,6 +409,26 @@ impl fmt::Display for Fetch {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Top {
/// SQL semantic equivalent of LIMIT but with same structure as FETCH.
pub with_ties: bool,
pub percent: bool,
pub quantity: Option<Expr>,
}
impl fmt::Display for Top {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let extension = if self.with_ties { " WITH TIES" } else { "" };
if let Some(ref quantity) = self.quantity {
let percent = if self.percent { " PERCENT" } else { "" };
write!(f, "TOP ({}){}{}", quantity, percent, extension)
} else {
write!(f, "TOP{}", extension)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Values(pub Vec<Vec<Expr>>);

View file

@ -374,6 +374,7 @@ define_keywords!(
TIMEZONE_HOUR,
TIMEZONE_MINUTE,
TO,
TOP,
TRAILING,
TRANSACTION,
TRANSLATE,
@ -426,7 +427,7 @@ define_keywords!(
/// can be parsed unambiguously without looking ahead.
pub const RESERVED_FOR_TABLE_ALIAS: &[&str] = &[
// Reserved as both a table and a column alias:
WITH, SELECT, WHERE, GROUP, HAVING, ORDER, LIMIT, OFFSET, FETCH, UNION, EXCEPT, INTERSECT,
WITH, SELECT, WHERE, GROUP, HAVING, ORDER, TOP, LIMIT, OFFSET, FETCH, UNION, EXCEPT, INTERSECT,
// Reserved only as a table alias in the `FROM`/`JOIN` clauses:
ON, JOIN, INNER, CROSS, FULL, LEFT, RIGHT, NATURAL, USING,
// for MSSQL-specific OUTER APPLY (seems reserved in most dialects)

View file

@ -783,7 +783,6 @@ impl Parser {
}
/// 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],
@ -1561,6 +1560,13 @@ impl Parser {
if all && distinct {
return parser_err!("Cannot specify both ALL and DISTINCT in SELECT");
}
let top = if self.parse_keyword("TOP") {
Some(self.parse_top()?)
} else {
None
};
let projection = self.parse_comma_separated(Parser::parse_select_item)?;
// Note that for keywords to be properly handled here, they need to be
@ -1594,6 +1600,7 @@ impl Parser {
Ok(Select {
distinct,
top,
projection,
from,
selection,
@ -1940,6 +1947,28 @@ impl Parser {
Ok(OrderByExpr { expr, asc })
}
/// Parse a TOP clause, MSSQL equivalent of LIMIT,
/// that follows after SELECT [DISTINCT].
pub fn parse_top(&mut self) -> Result<Top, ParserError> {
let quantity = if self.consume_token(&Token::LParen) {
let quantity = self.parse_expr()?;
self.expect_token(&Token::RParen)?;
Some(quantity)
} else {
Some(Expr::Value(self.parse_number_value()?))
};
let percent = self.parse_keyword("PERCENT");
let with_ties = self.parse_keywords(vec!["WITH", "TIES"]);
Ok(Top {
with_ties,
percent,
quantity,
})
}
/// Parse a LIMIT clause
pub fn parse_limit(&mut self) -> Result<Option<Expr>, ParserError> {
if self.parse_keyword("ALL") {

View file

@ -522,6 +522,7 @@ fn peeking_take_while(
#[cfg(test)]
mod tests {
use super::super::dialect::GenericDialect;
use super::super::dialect::MsSqlDialect;
use super::*;
#[test]
@ -782,6 +783,28 @@ mod tests {
compare(expected, tokens);
}
#[test]
fn tokenize_mssql_top() {
let sql = "SELECT TOP 5 [bar] FROM foo";
let dialect = MsSqlDialect {};
let mut tokenizer = Tokenizer::new(&dialect, sql);
let tokens = tokenizer.tokenize().unwrap();
let expected = vec![
Token::make_keyword("SELECT"),
Token::Whitespace(Whitespace::Space),
Token::make_keyword("TOP"),
Token::Whitespace(Whitespace::Space),
Token::Number(String::from("5")),
Token::Whitespace(Whitespace::Space),
Token::make_word("bar", Some('[')),
Token::Whitespace(Whitespace::Space),
Token::make_keyword("FROM"),
Token::Whitespace(Whitespace::Space),
Token::make_word("foo", None),
];
compare(expected, tokens);
}
fn compare(expected: Vec<Token>, actual: Vec<Token>) {
//println!("------------------------------");
//println!("tokens = {:?}", actual);

View file

@ -68,6 +68,48 @@ fn parse_mssql_apply_join() {
);
}
#[test]
fn parse_mssql_top_paren() {
let sql = "SELECT TOP (5) * FROM foo";
let select = ms_and_generic().verified_only_select(sql);
let top = select.top.unwrap();
assert_eq!(Some(Expr::Value(number("5"))), top.quantity);
assert!(!top.percent);
}
#[test]
fn parse_mssql_top_percent() {
let sql = "SELECT TOP (5) PERCENT * FROM foo";
let select = ms_and_generic().verified_only_select(sql);
let top = select.top.unwrap();
assert_eq!(Some(Expr::Value(number("5"))), top.quantity);
assert!(top.percent);
}
#[test]
fn parse_mssql_top_with_ties() {
let sql = "SELECT TOP (5) WITH TIES * FROM foo";
let select = ms_and_generic().verified_only_select(sql);
let top = select.top.unwrap();
assert_eq!(Some(Expr::Value(number("5"))), top.quantity);
assert!(top.with_ties);
}
#[test]
fn parse_mssql_top_percent_with_ties() {
let sql = "SELECT TOP (10) PERCENT WITH TIES * FROM foo";
let select = ms_and_generic().verified_only_select(sql);
let top = select.top.unwrap();
assert_eq!(Some(Expr::Value(number("10"))), top.quantity);
assert!(top.percent);
}
#[test]
fn parse_mssql_top() {
let sql = "SELECT TOP 5 bar, baz FROM foo";
let _ = ms_and_generic().one_statement_parses_to(sql, "SELECT TOP (5) bar, baz FROM foo");
}
fn ms() -> TestedDialects {
TestedDialects {
dialects: vec![Box::new(MsSqlDialect {})],

View file

@ -77,7 +77,7 @@ fn parse_show_columns() {
Statement::ShowColumns {
extended: false,
full: false,
table_name: table_name.clone(),
table_name: table_name,
filter: Some(ShowStatementFilter::Where(
mysql_and_generic().verified_expr("1 = 2")
)),