Skip to content

Commit 172ba42

Browse files
alexkyllobenesch
authored andcommitted
Add support for MSSQL's SELECT TOP N syntax (#150)
Add support for MSSQL SELECT TOP (N) [PERCENT] [WITH TIES] syntax.
1 parent 4cdd6e2 commit 172ba42

File tree

7 files changed

+126
-10
lines changed

7 files changed

+126
-10
lines changed

src/ast/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ pub use self::ddl::{
2727
pub use self::operator::{BinaryOperator, UnaryOperator};
2828
pub use self::query::{
2929
Cte, Fetch, Join, JoinConstraint, JoinOperator, OrderByExpr, Query, Select, SelectItem,
30-
SetExpr, SetOperator, TableAlias, TableFactor, TableWithJoins, Values,
30+
SetExpr, SetOperator, TableAlias, TableFactor, TableWithJoins, Top, Values,
3131
};
3232
pub use self::value::{DateTimeField, Value};
3333

src/ast/query.rs

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ impl fmt::Display for SetOperator {
114114
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
115115
pub struct Select {
116116
pub distinct: bool,
117+
/// MSSQL syntax: `TOP (<N>) [ PERCENT ] [ WITH TIES ]`
118+
pub top: Option<Top>,
117119
/// projection expressions
118120
pub projection: Vec<SelectItem>,
119121
/// FROM
@@ -128,12 +130,11 @@ pub struct Select {
128130

129131
impl fmt::Display for Select {
130132
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
131-
write!(
132-
f,
133-
"SELECT{} {}",
134-
if self.distinct { " DISTINCT" } else { "" },
135-
display_comma_separated(&self.projection)
136-
)?;
133+
write!(f, "SELECT{}", if self.distinct { " DISTINCT" } else { "" })?;
134+
if let Some(ref top) = self.top {
135+
write!(f, " {}", top)?;
136+
}
137+
write!(f, " {}", display_comma_separated(&self.projection))?;
137138
if !self.from.is_empty() {
138139
write!(f, " FROM {}", display_comma_separated(&self.from))?;
139140
}
@@ -408,6 +409,26 @@ impl fmt::Display for Fetch {
408409
}
409410
}
410411

412+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
413+
pub struct Top {
414+
/// SQL semantic equivalent of LIMIT but with same structure as FETCH.
415+
pub with_ties: bool,
416+
pub percent: bool,
417+
pub quantity: Option<Expr>,
418+
}
419+
420+
impl fmt::Display for Top {
421+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
422+
let extension = if self.with_ties { " WITH TIES" } else { "" };
423+
if let Some(ref quantity) = self.quantity {
424+
let percent = if self.percent { " PERCENT" } else { "" };
425+
write!(f, "TOP ({}){}{}", quantity, percent, extension)
426+
} else {
427+
write!(f, "TOP{}", extension)
428+
}
429+
}
430+
}
431+
411432
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
412433
pub struct Values(pub Vec<Vec<Expr>>);
413434

src/dialect/keywords.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ define_keywords!(
374374
TIMEZONE_HOUR,
375375
TIMEZONE_MINUTE,
376376
TO,
377+
TOP,
377378
TRAILING,
378379
TRANSACTION,
379380
TRANSLATE,
@@ -426,7 +427,7 @@ define_keywords!(
426427
/// can be parsed unambiguously without looking ahead.
427428
pub const RESERVED_FOR_TABLE_ALIAS: &[&str] = &[
428429
// Reserved as both a table and a column alias:
429-
WITH, SELECT, WHERE, GROUP, HAVING, ORDER, LIMIT, OFFSET, FETCH, UNION, EXCEPT, INTERSECT,
430+
WITH, SELECT, WHERE, GROUP, HAVING, ORDER, TOP, LIMIT, OFFSET, FETCH, UNION, EXCEPT, INTERSECT,
430431
// Reserved only as a table alias in the `FROM`/`JOIN` clauses:
431432
ON, JOIN, INNER, CROSS, FULL, LEFT, RIGHT, NATURAL, USING,
432433
// for MSSQL-specific OUTER APPLY (seems reserved in most dialects)

src/parser.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -783,7 +783,6 @@ impl Parser {
783783
}
784784

785785
/// Bail out if the current token is not one of the expected keywords, or consume it if it is
786-
#[must_use]
787786
pub fn expect_one_of_keywords(
788787
&mut self,
789788
keywords: &[&'static str],
@@ -1561,6 +1560,13 @@ impl Parser {
15611560
if all && distinct {
15621561
return parser_err!("Cannot specify both ALL and DISTINCT in SELECT");
15631562
}
1563+
1564+
let top = if self.parse_keyword("TOP") {
1565+
Some(self.parse_top()?)
1566+
} else {
1567+
None
1568+
};
1569+
15641570
let projection = self.parse_comma_separated(Parser::parse_select_item)?;
15651571

15661572
// Note that for keywords to be properly handled here, they need to be
@@ -1594,6 +1600,7 @@ impl Parser {
15941600

15951601
Ok(Select {
15961602
distinct,
1603+
top,
15971604
projection,
15981605
from,
15991606
selection,
@@ -1940,6 +1947,28 @@ impl Parser {
19401947
Ok(OrderByExpr { expr, asc })
19411948
}
19421949

1950+
/// Parse a TOP clause, MSSQL equivalent of LIMIT,
1951+
/// that follows after SELECT [DISTINCT].
1952+
pub fn parse_top(&mut self) -> Result<Top, ParserError> {
1953+
let quantity = if self.consume_token(&Token::LParen) {
1954+
let quantity = self.parse_expr()?;
1955+
self.expect_token(&Token::RParen)?;
1956+
Some(quantity)
1957+
} else {
1958+
Some(Expr::Value(self.parse_number_value()?))
1959+
};
1960+
1961+
let percent = self.parse_keyword("PERCENT");
1962+
1963+
let with_ties = self.parse_keywords(vec!["WITH", "TIES"]);
1964+
1965+
Ok(Top {
1966+
with_ties,
1967+
percent,
1968+
quantity,
1969+
})
1970+
}
1971+
19431972
/// Parse a LIMIT clause
19441973
pub fn parse_limit(&mut self) -> Result<Option<Expr>, ParserError> {
19451974
if self.parse_keyword("ALL") {

src/tokenizer.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,7 @@ fn peeking_take_while(
522522
#[cfg(test)]
523523
mod tests {
524524
use super::super::dialect::GenericDialect;
525+
use super::super::dialect::MsSqlDialect;
525526
use super::*;
526527

527528
#[test]
@@ -782,6 +783,28 @@ mod tests {
782783
compare(expected, tokens);
783784
}
784785

786+
#[test]
787+
fn tokenize_mssql_top() {
788+
let sql = "SELECT TOP 5 [bar] FROM foo";
789+
let dialect = MsSqlDialect {};
790+
let mut tokenizer = Tokenizer::new(&dialect, sql);
791+
let tokens = tokenizer.tokenize().unwrap();
792+
let expected = vec![
793+
Token::make_keyword("SELECT"),
794+
Token::Whitespace(Whitespace::Space),
795+
Token::make_keyword("TOP"),
796+
Token::Whitespace(Whitespace::Space),
797+
Token::Number(String::from("5")),
798+
Token::Whitespace(Whitespace::Space),
799+
Token::make_word("bar", Some('[')),
800+
Token::Whitespace(Whitespace::Space),
801+
Token::make_keyword("FROM"),
802+
Token::Whitespace(Whitespace::Space),
803+
Token::make_word("foo", None),
804+
];
805+
compare(expected, tokens);
806+
}
807+
785808
fn compare(expected: Vec<Token>, actual: Vec<Token>) {
786809
//println!("------------------------------");
787810
//println!("tokens = {:?}", actual);

tests/sqlparser_mssql.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,48 @@ fn parse_mssql_apply_join() {
6868
);
6969
}
7070

71+
#[test]
72+
fn parse_mssql_top_paren() {
73+
let sql = "SELECT TOP (5) * FROM foo";
74+
let select = ms_and_generic().verified_only_select(sql);
75+
let top = select.top.unwrap();
76+
assert_eq!(Some(Expr::Value(number("5"))), top.quantity);
77+
assert!(!top.percent);
78+
}
79+
80+
#[test]
81+
fn parse_mssql_top_percent() {
82+
let sql = "SELECT TOP (5) PERCENT * FROM foo";
83+
let select = ms_and_generic().verified_only_select(sql);
84+
let top = select.top.unwrap();
85+
assert_eq!(Some(Expr::Value(number("5"))), top.quantity);
86+
assert!(top.percent);
87+
}
88+
89+
#[test]
90+
fn parse_mssql_top_with_ties() {
91+
let sql = "SELECT TOP (5) WITH TIES * FROM foo";
92+
let select = ms_and_generic().verified_only_select(sql);
93+
let top = select.top.unwrap();
94+
assert_eq!(Some(Expr::Value(number("5"))), top.quantity);
95+
assert!(top.with_ties);
96+
}
97+
98+
#[test]
99+
fn parse_mssql_top_percent_with_ties() {
100+
let sql = "SELECT TOP (10) PERCENT WITH TIES * FROM foo";
101+
let select = ms_and_generic().verified_only_select(sql);
102+
let top = select.top.unwrap();
103+
assert_eq!(Some(Expr::Value(number("10"))), top.quantity);
104+
assert!(top.percent);
105+
}
106+
107+
#[test]
108+
fn parse_mssql_top() {
109+
let sql = "SELECT TOP 5 bar, baz FROM foo";
110+
let _ = ms_and_generic().one_statement_parses_to(sql, "SELECT TOP (5) bar, baz FROM foo");
111+
}
112+
71113
fn ms() -> TestedDialects {
72114
TestedDialects {
73115
dialects: vec![Box::new(MsSqlDialect {})],

tests/sqlparser_mysql.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ fn parse_show_columns() {
7777
Statement::ShowColumns {
7878
extended: false,
7979
full: false,
80-
table_name: table_name.clone(),
80+
table_name: table_name,
8181
filter: Some(ShowStatementFilter::Where(
8282
mysql_and_generic().verified_expr("1 = 2")
8383
)),

0 commit comments

Comments
 (0)