Skip to content

Commit 07a9b59

Browse files
committed
Preserve MySQL-style LIMIT <offset>, <limit> syntax
We already parse it, but were rewriting it into standard `LIMIT <limit> OFFSET <offset>` syntax. Now we preserve the original syntax.
1 parent 3392623 commit 07a9b59

10 files changed

+322
-266
lines changed

src/ast/mod.rs

+12-11
Original file line numberDiff line numberDiff line change
@@ -66,17 +66,18 @@ pub use self::query::{
6666
FormatClause, GroupByExpr, GroupByWithModifier, IdentWithAlias, IlikeSelectItem,
6767
InputFormatClause, Interpolate, InterpolateExpr, Join, JoinConstraint, JoinOperator,
6868
JsonTableColumn, JsonTableColumnErrorHandling, JsonTableNamedColumn, JsonTableNestedColumn,
69-
LateralView, LockClause, LockType, MatchRecognizePattern, MatchRecognizeSymbol, Measure,
70-
NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, OffsetRows, OpenJsonTableColumn,
71-
OrderBy, OrderByExpr, OrderByKind, OrderByOptions, PivotValueSource, ProjectionSelect, Query,
72-
RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch,
73-
Select, SelectFlavor, SelectInto, SelectItem, SelectItemQualifiedWildcardKind, SetExpr,
74-
SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, TableAlias, TableAliasColumnDef,
75-
TableFactor, TableFunctionArgs, TableIndexHintForClause, TableIndexHintType, TableIndexHints,
76-
TableIndexType, TableSample, TableSampleBucket, TableSampleKind, TableSampleMethod,
77-
TableSampleModifier, TableSampleQuantity, TableSampleSeed, TableSampleSeedModifier,
78-
TableSampleUnit, TableVersion, TableWithJoins, Top, TopQuantity, UpdateTableFromKind,
79-
ValueTableMode, Values, WildcardAdditionalOptions, With, WithFill,
69+
LateralView, LimitClause, LockClause, LockType, MatchRecognizePattern, MatchRecognizeSymbol,
70+
Measure, NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, OffsetRows,
71+
OpenJsonTableColumn, OrderBy, OrderByExpr, OrderByKind, OrderByOptions, PivotValueSource,
72+
ProjectionSelect, Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement,
73+
ReplaceSelectItem, RowsPerMatch, Select, SelectFlavor, SelectInto, SelectItem,
74+
SelectItemQualifiedWildcardKind, SetExpr, SetOperator, SetQuantifier, Setting,
75+
SymbolDefinition, Table, TableAlias, TableAliasColumnDef, TableFactor, TableFunctionArgs,
76+
TableIndexHintForClause, TableIndexHintType, TableIndexHints, TableIndexType, TableSample,
77+
TableSampleBucket, TableSampleKind, TableSampleMethod, TableSampleModifier,
78+
TableSampleQuantity, TableSampleSeed, TableSampleSeedModifier, TableSampleUnit, TableVersion,
79+
TableWithJoins, Top, TopQuantity, UpdateTableFromKind, ValueTableMode, Values,
80+
WildcardAdditionalOptions, With, WithFill,
8081
};
8182

8283
pub use self::trigger::{

src/ast/query.rs

+57-16
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,8 @@ pub struct Query {
4343
pub body: Box<SetExpr>,
4444
/// ORDER BY
4545
pub order_by: Option<OrderBy>,
46-
/// `LIMIT { <N> | ALL }`
47-
pub limit: Option<Expr>,
48-
49-
/// `LIMIT { <N> } BY { <expr>,<expr>,... } }`
50-
pub limit_by: Vec<Expr>,
51-
52-
/// `OFFSET <N> [ { ROW | ROWS } ]`
53-
pub offset: Option<Offset>,
46+
/// `LIMIT ... OFFSET ... | LIMIT <offset>, <limit>`
47+
pub limit_clause: Option<LimitClause>,
5448
/// `FETCH { FIRST | NEXT } <N> [ PERCENT ] { ROW | ROWS } | { ONLY | WITH TIES }`
5549
pub fetch: Option<Fetch>,
5650
/// `FOR { UPDATE | SHARE } [ OF table_name ] [ SKIP LOCKED | NOWAIT ]`
@@ -79,14 +73,9 @@ impl fmt::Display for Query {
7973
if let Some(ref order_by) = self.order_by {
8074
write!(f, " {order_by}")?;
8175
}
82-
if let Some(ref limit) = self.limit {
83-
write!(f, " LIMIT {limit}")?;
84-
}
85-
if let Some(ref offset) = self.offset {
86-
write!(f, " {offset}")?;
87-
}
88-
if !self.limit_by.is_empty() {
89-
write!(f, " BY {}", display_separated(&self.limit_by, ", "))?;
76+
77+
if let Some(ref limit_clause) = self.limit_clause {
78+
limit_clause.fmt(f)?;
9079
}
9180
if let Some(ref settings) = self.settings {
9281
write!(f, " SETTINGS {}", display_comma_separated(settings))?;
@@ -2374,6 +2363,58 @@ impl fmt::Display for OrderByOptions {
23742363
}
23752364
}
23762365

2366+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
2367+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
2368+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
2369+
pub enum LimitClause {
2370+
/// Standard SQL syntax
2371+
///
2372+
/// `LIMIT <limit> [BY <expr>,<expr>,...] [OFFSET <offset>]`
2373+
LimitOffset {
2374+
/// `LIMIT { <N> | ALL }`
2375+
limit: Option<Expr>,
2376+
/// `OFFSET <N> [ { ROW | ROWS } ]`
2377+
offset: Option<Offset>,
2378+
/// `BY { <expr>,<expr>,... } }`
2379+
///
2380+
/// [ClickHouse](https://clickhouse.com/docs/sql-reference/statements/select/limit-by)
2381+
limit_by: Vec<Expr>,
2382+
},
2383+
/// [MySQL]-specific syntax; the order of expressions is reversed.
2384+
///
2385+
/// `LIMIT <offset>, <limit>`
2386+
///
2387+
/// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/select.html
2388+
OffsetCommaLimit { offset: Expr, limit: Expr },
2389+
}
2390+
2391+
impl fmt::Display for LimitClause {
2392+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2393+
match self {
2394+
LimitClause::LimitOffset {
2395+
limit,
2396+
limit_by,
2397+
offset,
2398+
} => {
2399+
if let Some(ref limit) = limit {
2400+
write!(f, " LIMIT {limit}")?;
2401+
}
2402+
if let Some(ref offset) = offset {
2403+
write!(f, " {offset}")?;
2404+
}
2405+
if !limit_by.is_empty() {
2406+
debug_assert!(limit.is_some());
2407+
write!(f, " BY {}", display_separated(limit_by, ", "))?;
2408+
}
2409+
Ok(())
2410+
}
2411+
LimitClause::OffsetCommaLimit { offset, limit } => {
2412+
write!(f, " LIMIT {}, {}", offset, limit)
2413+
}
2414+
}
2415+
}
2416+
}
2417+
23772418
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
23782419
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
23792420
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]

src/ast/spans.rs

+23-8
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ use super::{
2929
Function, FunctionArg, FunctionArgExpr, FunctionArgumentClause, FunctionArgumentList,
3030
FunctionArguments, GroupByExpr, HavingBound, IlikeSelectItem, Insert, Interpolate,
3131
InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonPath, JsonPathElem, LateralView,
32-
MatchRecognizePattern, Measure, NamedWindowDefinition, ObjectName, ObjectNamePart, Offset,
33-
OnConflict, OnConflictAction, OnInsert, OrderBy, OrderByExpr, OrderByKind, Partition,
32+
LimitClause, MatchRecognizePattern, Measure, NamedWindowDefinition, ObjectName, ObjectNamePart,
33+
Offset, OnConflict, OnConflictAction, OnInsert, OrderBy, OrderByExpr, OrderByKind, Partition,
3434
PivotValueSource, ProjectionSelect, Query, ReferentialAction, RenameSelectItem,
3535
ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SqlOption,
3636
Statement, Subscript, SymbolDefinition, TableAlias, TableAliasColumnDef, TableConstraint,
@@ -94,9 +94,7 @@ impl Spanned for Query {
9494
with,
9595
body,
9696
order_by,
97-
limit,
98-
limit_by,
99-
offset,
97+
limit_clause,
10098
fetch,
10199
locks: _, // todo
102100
for_clause: _, // todo, mssql specific
@@ -109,14 +107,31 @@ impl Spanned for Query {
109107
.map(|i| i.span())
110108
.chain(core::iter::once(body.span()))
111109
.chain(order_by.as_ref().map(|i| i.span()))
112-
.chain(limit.as_ref().map(|i| i.span()))
113-
.chain(limit_by.iter().map(|i| i.span()))
114-
.chain(offset.as_ref().map(|i| i.span()))
110+
.chain(limit_clause.as_ref().map(|i| i.span()))
115111
.chain(fetch.as_ref().map(|i| i.span())),
116112
)
117113
}
118114
}
119115

116+
impl Spanned for LimitClause {
117+
fn span(&self) -> Span {
118+
match self {
119+
LimitClause::LimitOffset {
120+
limit,
121+
offset,
122+
limit_by,
123+
} => union_spans(
124+
limit
125+
.iter()
126+
.map(|i| i.span())
127+
.chain(offset.as_ref().map(|i| i.span()))
128+
.chain(limit_by.iter().map(|i| i.span())),
129+
),
130+
LimitClause::OffsetCommaLimit { offset, limit } => offset.span().union(&limit.span()),
131+
}
132+
}
133+
}
134+
120135
impl Spanned for Offset {
121136
fn span(&self) -> Span {
122137
let Offset {

src/ast/visitor.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ where
523523
/// // Remove all select limits in sub-queries
524524
/// visit_expressions_mut(&mut statements, |expr| {
525525
/// if let Expr::Subquery(q) = expr {
526-
/// q.limit = None
526+
/// q.limit_clause = None;
527527
/// }
528528
/// ControlFlow::<()>::Continue(())
529529
/// });
@@ -647,7 +647,7 @@ where
647647
/// // Remove all select limits in outer statements (not in sub-queries)
648648
/// visit_statements_mut(&mut statements, |stmt| {
649649
/// if let Statement::Query(q) = stmt {
650-
/// q.limit = None
650+
/// q.limit_clause = None;
651651
/// }
652652
/// ControlFlow::<()>::Continue(())
653653
/// });

src/parser/mod.rs

+55-49
Original file line numberDiff line numberDiff line change
@@ -9490,6 +9490,55 @@ impl<'a> Parser<'a> {
94909490
}
94919491
}
94929492

9493+
pub fn parse_optional_limit_clause(&mut self) -> Result<Option<LimitClause>, ParserError> {
9494+
let mut offset = if self.parse_keyword(Keyword::OFFSET) {
9495+
Some(self.parse_offset()?)
9496+
} else {
9497+
None
9498+
};
9499+
9500+
let (limit, limit_by) = if self.parse_keyword(Keyword::LIMIT) {
9501+
let expr = self.parse_limit()?;
9502+
9503+
if self.dialect.supports_limit_comma()
9504+
&& offset.is_none()
9505+
&& expr.is_some() // ALL not supported with comma
9506+
&& self.consume_token(&Token::Comma)
9507+
{
9508+
return Ok(Some(LimitClause::OffsetCommaLimit {
9509+
offset: expr.unwrap(), // Just checked `expr.is_some()`
9510+
limit: self.parse_expr()?,
9511+
}));
9512+
}
9513+
9514+
let limit_by = if dialect_of!(self is ClickHouseDialect | GenericDialect)
9515+
&& self.parse_keyword(Keyword::BY)
9516+
{
9517+
Some(self.parse_comma_separated(Parser::parse_expr)?)
9518+
} else {
9519+
None
9520+
};
9521+
9522+
(Some(expr), limit_by)
9523+
} else {
9524+
(None, None)
9525+
};
9526+
9527+
if offset.is_none() && limit.is_some() && self.parse_keyword(Keyword::OFFSET) {
9528+
offset = Some(self.parse_offset()?);
9529+
}
9530+
9531+
if offset.is_some() || (limit.is_some() && limit != Some(None)) || limit_by.is_some() {
9532+
Ok(Some(LimitClause::LimitOffset {
9533+
limit: limit.unwrap_or_default(),
9534+
offset,
9535+
limit_by: limit_by.unwrap_or_default(),
9536+
}))
9537+
} else {
9538+
Ok(None)
9539+
}
9540+
}
9541+
94939542
/// Parse a table object for insertion
94949543
/// e.g. `some_database.some_table` or `FUNCTION some_table_func(...)`
94959544
pub fn parse_table_object(&mut self) -> Result<TableObject, ParserError> {
@@ -10230,10 +10279,8 @@ impl<'a> Parser<'a> {
1023010279
Ok(Query {
1023110280
with,
1023210281
body: self.parse_insert_setexpr_boxed()?,
10233-
limit: None,
10234-
limit_by: vec![],
1023510282
order_by: None,
10236-
offset: None,
10283+
limit_clause: None,
1023710284
fetch: None,
1023810285
locks: vec![],
1023910286
for_clause: None,
@@ -10245,10 +10292,8 @@ impl<'a> Parser<'a> {
1024510292
Ok(Query {
1024610293
with,
1024710294
body: self.parse_update_setexpr_boxed()?,
10248-
limit: None,
10249-
limit_by: vec![],
1025010295
order_by: None,
10251-
offset: None,
10296+
limit_clause: None,
1025210297
fetch: None,
1025310298
locks: vec![],
1025410299
for_clause: None,
@@ -10260,10 +10305,8 @@ impl<'a> Parser<'a> {
1026010305
Ok(Query {
1026110306
with,
1026210307
body: self.parse_delete_setexpr_boxed()?,
10263-
limit: None,
10264-
limit_by: vec![],
10308+
limit_clause: None,
1026510309
order_by: None,
10266-
offset: None,
1026710310
fetch: None,
1026810311
locks: vec![],
1026910312
for_clause: None,
@@ -10276,40 +10319,7 @@ impl<'a> Parser<'a> {
1027610319

1027710320
let order_by = self.parse_optional_order_by()?;
1027810321

10279-
let mut limit = None;
10280-
let mut offset = None;
10281-
10282-
for _x in 0..2 {
10283-
if limit.is_none() && self.parse_keyword(Keyword::LIMIT) {
10284-
limit = self.parse_limit()?
10285-
}
10286-
10287-
if offset.is_none() && self.parse_keyword(Keyword::OFFSET) {
10288-
offset = Some(self.parse_offset()?)
10289-
}
10290-
10291-
if self.dialect.supports_limit_comma()
10292-
&& limit.is_some()
10293-
&& offset.is_none()
10294-
&& self.consume_token(&Token::Comma)
10295-
{
10296-
// MySQL style LIMIT x,y => LIMIT y OFFSET x.
10297-
// Check <https://dev.mysql.com/doc/refman/8.0/en/select.html> for more details.
10298-
offset = Some(Offset {
10299-
value: limit.unwrap(),
10300-
rows: OffsetRows::None,
10301-
});
10302-
limit = Some(self.parse_expr()?);
10303-
}
10304-
}
10305-
10306-
let limit_by = if dialect_of!(self is ClickHouseDialect | GenericDialect)
10307-
&& self.parse_keyword(Keyword::BY)
10308-
{
10309-
self.parse_comma_separated(Parser::parse_expr)?
10310-
} else {
10311-
vec![]
10312-
};
10322+
let limit_clause = self.parse_optional_limit_clause()?;
1031310323

1031410324
let settings = self.parse_settings()?;
1031510325

@@ -10346,9 +10356,7 @@ impl<'a> Parser<'a> {
1034610356
with,
1034710357
body,
1034810358
order_by,
10349-
limit,
10350-
limit_by,
10351-
offset,
10359+
limit_clause,
1035210360
fetch,
1035310361
locks,
1035410362
for_clause,
@@ -11703,9 +11711,7 @@ impl<'a> Parser<'a> {
1170311711
with: None,
1170411712
body: Box::new(values),
1170511713
order_by: None,
11706-
limit: None,
11707-
limit_by: vec![],
11708-
offset: None,
11714+
limit_clause: None,
1170911715
fetch: None,
1171011716
locks: vec![],
1171111717
for_clause: None,

tests/sqlparser_clickhouse.rs

+14-1
Original file line numberDiff line numberDiff line change
@@ -944,6 +944,12 @@ fn parse_limit_by() {
944944
clickhouse_and_generic().verified_stmt(
945945
r#"SELECT * FROM default.last_asset_runs_mv ORDER BY created_at DESC LIMIT 1 BY asset, toStartOfDay(created_at)"#,
946946
);
947+
clickhouse_and_generic().parse_sql_statements(
948+
r#"SELECT * FROM default.last_asset_runs_mv ORDER BY created_at DESC BY asset, toStartOfDay(created_at)"#,
949+
).expect_err("BY without LIMIT");
950+
clickhouse_and_generic()
951+
.parse_sql_statements("SELECT * FROM T OFFSET 5 BY foo")
952+
.expect_err("BY with OFFSET but without LIMIT");
947953
}
948954

949955
#[test]
@@ -1107,7 +1113,14 @@ fn parse_select_order_by_with_fill_interpolate() {
11071113
},
11081114
select.order_by.expect("ORDER BY expected")
11091115
);
1110-
assert_eq!(Some(Expr::value(number("2"))), select.limit);
1116+
assert_eq!(
1117+
select.limit_clause,
1118+
Some(LimitClause::LimitOffset {
1119+
limit: Some(Expr::value(number("2"))),
1120+
offset: None,
1121+
limit_by: vec![]
1122+
})
1123+
);
11111124
}
11121125

11131126
#[test]

0 commit comments

Comments
 (0)