Skip to content

Commit fb578bb

Browse files
authored
Preserve MySQL-style LIMIT <offset>, <limit> syntax (#1765)
1 parent 85f8551 commit fb578bb

10 files changed

+327
-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

+60-49
Original file line numberDiff line numberDiff line change
@@ -9491,6 +9491,60 @@ impl<'a> Parser<'a> {
94919491
}
94929492
}
94939493

9494+
fn parse_optional_limit_clause(&mut self) -> Result<Option<LimitClause>, ParserError> {
9495+
let mut offset = if self.parse_keyword(Keyword::OFFSET) {
9496+
Some(self.parse_offset()?)
9497+
} else {
9498+
None
9499+
};
9500+
9501+
let (limit, limit_by) = if self.parse_keyword(Keyword::LIMIT) {
9502+
let expr = self.parse_limit()?;
9503+
9504+
if self.dialect.supports_limit_comma()
9505+
&& offset.is_none()
9506+
&& expr.is_some() // ALL not supported with comma
9507+
&& self.consume_token(&Token::Comma)
9508+
{
9509+
let offset = expr.ok_or_else(|| {
9510+
ParserError::ParserError(
9511+
"Missing offset for LIMIT <offset>, <limit>".to_string(),
9512+
)
9513+
})?;
9514+
return Ok(Some(LimitClause::OffsetCommaLimit {
9515+
offset,
9516+
limit: self.parse_expr()?,
9517+
}));
9518+
}
9519+
9520+
let limit_by = if dialect_of!(self is ClickHouseDialect | GenericDialect)
9521+
&& self.parse_keyword(Keyword::BY)
9522+
{
9523+
Some(self.parse_comma_separated(Parser::parse_expr)?)
9524+
} else {
9525+
None
9526+
};
9527+
9528+
(Some(expr), limit_by)
9529+
} else {
9530+
(None, None)
9531+
};
9532+
9533+
if offset.is_none() && limit.is_some() && self.parse_keyword(Keyword::OFFSET) {
9534+
offset = Some(self.parse_offset()?);
9535+
}
9536+
9537+
if offset.is_some() || (limit.is_some() && limit != Some(None)) || limit_by.is_some() {
9538+
Ok(Some(LimitClause::LimitOffset {
9539+
limit: limit.unwrap_or_default(),
9540+
offset,
9541+
limit_by: limit_by.unwrap_or_default(),
9542+
}))
9543+
} else {
9544+
Ok(None)
9545+
}
9546+
}
9547+
94949548
/// Parse a table object for insertion
94959549
/// e.g. `some_database.some_table` or `FUNCTION some_table_func(...)`
94969550
pub fn parse_table_object(&mut self) -> Result<TableObject, ParserError> {
@@ -10231,10 +10285,8 @@ impl<'a> Parser<'a> {
1023110285
Ok(Query {
1023210286
with,
1023310287
body: self.parse_insert_setexpr_boxed()?,
10234-
limit: None,
10235-
limit_by: vec![],
1023610288
order_by: None,
10237-
offset: None,
10289+
limit_clause: None,
1023810290
fetch: None,
1023910291
locks: vec![],
1024010292
for_clause: None,
@@ -10246,10 +10298,8 @@ impl<'a> Parser<'a> {
1024610298
Ok(Query {
1024710299
with,
1024810300
body: self.parse_update_setexpr_boxed()?,
10249-
limit: None,
10250-
limit_by: vec![],
1025110301
order_by: None,
10252-
offset: None,
10302+
limit_clause: None,
1025310303
fetch: None,
1025410304
locks: vec![],
1025510305
for_clause: None,
@@ -10261,10 +10311,8 @@ impl<'a> Parser<'a> {
1026110311
Ok(Query {
1026210312
with,
1026310313
body: self.parse_delete_setexpr_boxed()?,
10264-
limit: None,
10265-
limit_by: vec![],
10314+
limit_clause: None,
1026610315
order_by: None,
10267-
offset: None,
1026810316
fetch: None,
1026910317
locks: vec![],
1027010318
for_clause: None,
@@ -10277,40 +10325,7 @@ impl<'a> Parser<'a> {
1027710325

1027810326
let order_by = self.parse_optional_order_by()?;
1027910327

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

1031510330
let settings = self.parse_settings()?;
1031610331

@@ -10347,9 +10362,7 @@ impl<'a> Parser<'a> {
1034710362
with,
1034810363
body,
1034910364
order_by,
10350-
limit,
10351-
limit_by,
10352-
offset,
10365+
limit_clause,
1035310366
fetch,
1035410367
locks,
1035510368
for_clause,
@@ -11809,9 +11822,7 @@ impl<'a> Parser<'a> {
1180911822
with: None,
1181011823
body: Box::new(values),
1181111824
order_by: None,
11812-
limit: None,
11813-
limit_by: vec![],
11814-
offset: None,
11825+
limit_clause: None,
1181511826
fetch: None,
1181611827
locks: vec![],
1181711828
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)