Skip to content

Commit 18774e5

Browse files
mvzinkayman-sigma
authored andcommitted
Preserve MySQL-style LIMIT <offset>, <limit> syntax (apache#1765)
1 parent 004c0d7 commit 18774e5

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
@@ -9501,6 +9501,60 @@ impl<'a> Parser<'a> {
95019501
}
95029502
}
95039503

9504+
fn parse_optional_limit_clause(&mut self) -> Result<Option<LimitClause>, ParserError> {
9505+
let mut offset = if self.parse_keyword(Keyword::OFFSET) {
9506+
Some(self.parse_offset()?)
9507+
} else {
9508+
None
9509+
};
9510+
9511+
let (limit, limit_by) = if self.parse_keyword(Keyword::LIMIT) {
9512+
let expr = self.parse_limit()?;
9513+
9514+
if self.dialect.supports_limit_comma()
9515+
&& offset.is_none()
9516+
&& expr.is_some() // ALL not supported with comma
9517+
&& self.consume_token(&Token::Comma)
9518+
{
9519+
let offset = expr.ok_or_else(|| {
9520+
ParserError::ParserError(
9521+
"Missing offset for LIMIT <offset>, <limit>".to_string(),
9522+
)
9523+
})?;
9524+
return Ok(Some(LimitClause::OffsetCommaLimit {
9525+
offset,
9526+
limit: self.parse_expr()?,
9527+
}));
9528+
}
9529+
9530+
let limit_by = if dialect_of!(self is ClickHouseDialect | GenericDialect)
9531+
&& self.parse_keyword(Keyword::BY)
9532+
{
9533+
Some(self.parse_comma_separated(Parser::parse_expr)?)
9534+
} else {
9535+
None
9536+
};
9537+
9538+
(Some(expr), limit_by)
9539+
} else {
9540+
(None, None)
9541+
};
9542+
9543+
if offset.is_none() && limit.is_some() && self.parse_keyword(Keyword::OFFSET) {
9544+
offset = Some(self.parse_offset()?);
9545+
}
9546+
9547+
if offset.is_some() || (limit.is_some() && limit != Some(None)) || limit_by.is_some() {
9548+
Ok(Some(LimitClause::LimitOffset {
9549+
limit: limit.unwrap_or_default(),
9550+
offset,
9551+
limit_by: limit_by.unwrap_or_default(),
9552+
}))
9553+
} else {
9554+
Ok(None)
9555+
}
9556+
}
9557+
95049558
/// Parse a table object for insertion
95059559
/// e.g. `some_database.some_table` or `FUNCTION some_table_func(...)`
95069560
pub fn parse_table_object(&mut self) -> Result<TableObject, ParserError> {
@@ -10241,10 +10295,8 @@ impl<'a> Parser<'a> {
1024110295
Ok(Query {
1024210296
with,
1024310297
body: self.parse_insert_setexpr_boxed()?,
10244-
limit: None,
10245-
limit_by: vec![],
1024610298
order_by: None,
10247-
offset: None,
10299+
limit_clause: None,
1024810300
fetch: None,
1024910301
locks: vec![],
1025010302
for_clause: None,
@@ -10256,10 +10308,8 @@ impl<'a> Parser<'a> {
1025610308
Ok(Query {
1025710309
with,
1025810310
body: self.parse_update_setexpr_boxed()?,
10259-
limit: None,
10260-
limit_by: vec![],
1026110311
order_by: None,
10262-
offset: None,
10312+
limit_clause: None,
1026310313
fetch: None,
1026410314
locks: vec![],
1026510315
for_clause: None,
@@ -10271,10 +10321,8 @@ impl<'a> Parser<'a> {
1027110321
Ok(Query {
1027210322
with,
1027310323
body: self.parse_delete_setexpr_boxed()?,
10274-
limit: None,
10275-
limit_by: vec![],
10324+
limit_clause: None,
1027610325
order_by: None,
10277-
offset: None,
1027810326
fetch: None,
1027910327
locks: vec![],
1028010328
for_clause: None,
@@ -10287,40 +10335,7 @@ impl<'a> Parser<'a> {
1028710335

1028810336
let order_by = self.parse_optional_order_by()?;
1028910337

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

1032510340
let settings = self.parse_settings()?;
1032610341

@@ -10357,9 +10372,7 @@ impl<'a> Parser<'a> {
1035710372
with,
1035810373
body,
1035910374
order_by,
10360-
limit,
10361-
limit_by,
10362-
offset,
10375+
limit_clause,
1036310376
fetch,
1036410377
locks,
1036510378
for_clause,
@@ -11819,9 +11832,7 @@ impl<'a> Parser<'a> {
1181911832
with: None,
1182011833
body: Box::new(values),
1182111834
order_by: None,
11822-
limit: None,
11823-
limit_by: vec![],
11824-
offset: None,
11835+
limit_clause: None,
1182511836
fetch: None,
1182611837
locks: vec![],
1182711838
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)