Skip to content

Commit a437a32

Browse files
committed
MySQL: Add support for SELECT modifiers
Adds support for MySQL-specific `SELECT` modifiers that appear after the `SELECT` keyword. Grammar from the [docs]: ```sql SELECT [ALL | DISTINCT | DISTINCTROW ] [HIGH_PRIORITY] [STRAIGHT_JOIN] [SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT] [SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS] select_expr [, select_expr] ... ``` Manual testing shows that these options can appear in any order relative to each other, so for the sake of fidelity, we parse this separately from how we parse distinct and other options for other dialects, in a new `Parser::parse_select_modifiers` method. `DISTINCTROW` is a legacy (but not deprecated) synonym for `DISTINCT`, so it just gets canonicalized as `DISTINCT`. [docs]: https://dev.mysql.com/doc/refman/8.4/en/select.html
1 parent 845e213 commit a437a32

14 files changed

Lines changed: 427 additions & 17 deletions

src/ast/mod.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,15 @@ pub use self::query::{
9595
OffsetRows, OpenJsonTableColumn, OrderBy, OrderByExpr, OrderByKind, OrderByOptions,
9696
PipeOperator, PivotValueSource, ProjectionSelect, Query, RenameSelectItem,
9797
RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select,
98-
SelectFlavor, SelectInto, SelectItem, SelectItemQualifiedWildcardKind, SetExpr, SetOperator,
99-
SetQuantifier, Setting, SymbolDefinition, Table, TableAlias, TableAliasColumnDef, TableFactor,
100-
TableFunctionArgs, TableIndexHintForClause, TableIndexHintType, TableIndexHints,
101-
TableIndexType, TableSample, TableSampleBucket, TableSampleKind, TableSampleMethod,
102-
TableSampleModifier, TableSampleQuantity, TableSampleSeed, TableSampleSeedModifier,
103-
TableSampleUnit, TableVersion, TableWithJoins, Top, TopQuantity, UpdateTableFromKind,
104-
ValueTableMode, Values, WildcardAdditionalOptions, With, WithFill, XmlNamespaceDefinition,
105-
XmlPassingArgument, XmlPassingClause, XmlTableColumn, XmlTableColumnOption,
98+
SelectFlavor, SelectInto, SelectItem, SelectItemQualifiedWildcardKind, SelectModifiers,
99+
SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, TableAlias,
100+
TableAliasColumnDef, TableFactor, TableFunctionArgs, TableIndexHintForClause,
101+
TableIndexHintType, TableIndexHints, TableIndexType, TableSample, TableSampleBucket,
102+
TableSampleKind, TableSampleMethod, TableSampleModifier, TableSampleQuantity, TableSampleSeed,
103+
TableSampleSeedModifier, TableSampleUnit, TableVersion, TableWithJoins, Top, TopQuantity,
104+
UpdateTableFromKind, ValueTableMode, Values, WildcardAdditionalOptions, With, WithFill,
105+
XmlNamespaceDefinition, XmlPassingArgument, XmlPassingClause, XmlTableColumn,
106+
XmlTableColumnOption,
106107
};
107108

108109
pub use self::trigger::{

src/ast/query.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,70 @@ pub enum SelectFlavor {
334334
FromFirstNoSelect,
335335
}
336336

337+
/// MySQL-specific SELECT modifiers that appear after the SELECT keyword.
338+
///
339+
/// These modifiers affect query execution and optimization. They can appear
340+
/// in any order after SELECT and before the column list, and can be
341+
/// interleaved with DISTINCT/DISTINCTROW/ALL:
342+
///
343+
/// ```sql
344+
/// SELECT
345+
/// [ALL | DISTINCT | DISTINCTROW]
346+
/// [HIGH_PRIORITY]
347+
/// [STRAIGHT_JOIN]
348+
/// [SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT]
349+
/// [SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS]
350+
/// select_expr [, select_expr] ...
351+
/// ```
352+
///
353+
/// See [MySQL SELECT](https://dev.mysql.com/doc/refman/8.4/en/select.html).
354+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Default)]
355+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
356+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
357+
pub struct SelectModifiers {
358+
/// `HIGH_PRIORITY` gives the SELECT higher priority than statements that update a table.
359+
pub high_priority: bool,
360+
/// `STRAIGHT_JOIN` forces the optimizer to join tables in the order listed in the FROM clause.
361+
pub straight_join: bool,
362+
/// `SQL_SMALL_RESULT` hints that the result set is small, using in-memory temp tables.
363+
pub sql_small_result: bool,
364+
/// `SQL_BIG_RESULT` hints that the result set is large, using disk-based temp tables.
365+
pub sql_big_result: bool,
366+
/// `SQL_BUFFER_RESULT` forces the result to be put into a temporary table to release locks early.
367+
pub sql_buffer_result: bool,
368+
/// `SQL_NO_CACHE` tells MySQL not to cache the query result.
369+
pub sql_no_cache: bool,
370+
/// `SQL_CALC_FOUND_ROWS` tells MySQL to calculate the total number of rows.
371+
pub sql_calc_found_rows: bool,
372+
}
373+
374+
impl fmt::Display for SelectModifiers {
375+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
376+
if self.high_priority {
377+
f.write_str(" HIGH_PRIORITY")?;
378+
}
379+
if self.straight_join {
380+
f.write_str(" STRAIGHT_JOIN")?;
381+
}
382+
if self.sql_small_result {
383+
f.write_str(" SQL_SMALL_RESULT")?;
384+
}
385+
if self.sql_big_result {
386+
f.write_str(" SQL_BIG_RESULT")?;
387+
}
388+
if self.sql_buffer_result {
389+
f.write_str(" SQL_BUFFER_RESULT")?;
390+
}
391+
if self.sql_no_cache {
392+
f.write_str(" SQL_NO_CACHE")?;
393+
}
394+
if self.sql_calc_found_rows {
395+
f.write_str(" SQL_CALC_FOUND_ROWS")?;
396+
}
397+
Ok(())
398+
}
399+
}
400+
337401
/// A restricted variant of `SELECT` (without CTEs/`ORDER BY`), which may
338402
/// appear either as the only body item of a `Query`, or as an operand
339403
/// to a set operation like `UNION`.
@@ -345,6 +409,10 @@ pub struct Select {
345409
pub select_token: AttachedToken,
346410
/// `SELECT [DISTINCT] ...`
347411
pub distinct: Option<Distinct>,
412+
/// MySQL-specific SELECT modifiers.
413+
///
414+
/// See [MySQL SELECT](https://dev.mysql.com/doc/refman/8.4/en/select.html).
415+
pub select_modifiers: SelectModifiers,
348416
/// MSSQL syntax: `TOP (<N>) [ PERCENT ] [ WITH TIES ]`
349417
pub top: Option<Top>,
350418
/// Whether the top was located before `ALL`/`DISTINCT`
@@ -415,6 +483,8 @@ impl fmt::Display for Select {
415483
value_table_mode.fmt(f)?;
416484
}
417485

486+
self.select_modifiers.fmt(f)?;
487+
418488
if let Some(ref top) = self.top {
419489
if self.top_before_distinct {
420490
f.write_str(" ")?;

src/ast/spans.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2230,7 +2230,8 @@ impl Spanned for Select {
22302230
let Select {
22312231
select_token,
22322232
distinct: _, // todo
2233-
top: _, // todo, mysql specific
2233+
select_modifiers: _,
2234+
top: _, // todo, mysql specific
22342235
projection,
22352236
exclude: _,
22362237
into,
@@ -2801,7 +2802,7 @@ WHERE id = 1
28012802
UPDATE SET target_table.description = source_table.description
28022803
28032804
WHEN MATCHED AND target_table.x != 'X' THEN DELETE
2804-
WHEN NOT MATCHED AND 1 THEN INSERT (product, quantity) ROW
2805+
WHEN NOT MATCHED AND 1 THEN INSERT (product, quantity) ROW
28052806
"#;
28062807

28072808
let r = Parser::parse_sql(&crate::dialect::GenericDialect, sql).unwrap();

src/dialect/mod.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,19 @@ pub trait Dialect: Debug + Any {
610610
false
611611
}
612612

613+
/// Returns true if the dialect supports MySQL-specific SELECT modifiers
614+
/// like `HIGH_PRIORITY`, `STRAIGHT_JOIN`, `SQL_SMALL_RESULT`, etc.
615+
///
616+
/// For example:
617+
/// ```sql
618+
/// SELECT HIGH_PRIORITY STRAIGHT_JOIN SQL_SMALL_RESULT * FROM t1 JOIN t2 ON ...
619+
/// ```
620+
///
621+
/// [MySQL](https://dev.mysql.com/doc/refman/8.4/en/select.html)
622+
fn supports_select_modifiers(&self) -> bool {
623+
false
624+
}
625+
613626
/// Dialect-specific infix parser override
614627
///
615628
/// This method is called to parse the next infix expression.

src/dialect/mysql.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ impl Dialect for MySqlDialect {
156156
true
157157
}
158158

159+
fn supports_select_modifiers(&self) -> bool {
160+
true
161+
}
162+
159163
fn supports_set_names(&self) -> bool {
160164
true
161165
}

src/keywords.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,7 @@ define_keywords!(
333333
DISCARD,
334334
DISCONNECT,
335335
DISTINCT,
336+
DISTINCTROW,
336337
DISTRIBUTE,
337338
DIV,
338339
DO,
@@ -956,6 +957,11 @@ define_keywords!(
956957
SQLEXCEPTION,
957958
SQLSTATE,
958959
SQLWARNING,
960+
SQL_BIG_RESULT,
961+
SQL_BUFFER_RESULT,
962+
SQL_CALC_FOUND_ROWS,
963+
SQL_NO_CACHE,
964+
SQL_SMALL_RESULT,
959965
SQRT,
960966
SRID,
961967
STABLE,

src/parser/mod.rs

Lines changed: 94 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4906,14 +4906,17 @@ impl<'a> Parser<'a> {
49064906
/// Parse either `ALL`, `DISTINCT` or `DISTINCT ON (...)`. Returns [`None`] if `ALL` is parsed
49074907
/// and results in a [`ParserError`] if both `ALL` and `DISTINCT` are found.
49084908
pub fn parse_all_or_distinct(&mut self) -> Result<Option<Distinct>, ParserError> {
4909-
let loc = self.peek_token().span.start;
49104909
let all = self.parse_keyword(Keyword::ALL);
49114910
let distinct = self.parse_keyword(Keyword::DISTINCT);
49124911
if !distinct {
49134912
return Ok(None);
49144913
}
49154914
if all {
4916-
return parser_err!("Cannot specify both ALL and DISTINCT".to_string(), loc);
4915+
self.prev_token();
4916+
return self.expected(
4917+
"ALL alone without DISTINCT or DISTINCTROW",
4918+
self.peek_token(),
4919+
);
49174920
}
49184921
let on = self.parse_keyword(Keyword::ON);
49194922
if !on {
@@ -13823,6 +13826,7 @@ impl<'a> Parser<'a> {
1382313826
return Ok(Select {
1382413827
select_token: AttachedToken(from_token),
1382513828
distinct: None,
13829+
select_modifiers: SelectModifiers::default(),
1382613830
top: None,
1382713831
top_before_distinct: false,
1382813832
projection: vec![],
@@ -13851,13 +13855,26 @@ impl<'a> Parser<'a> {
1385113855
let select_token = self.expect_keyword(Keyword::SELECT)?;
1385213856
let value_table_mode = self.parse_value_table_mode()?;
1385313857

13858+
let (select_modifiers, distinct_select_modifier) =
13859+
if self.dialect.supports_select_modifiers() {
13860+
self.parse_select_modifiers()?
13861+
} else {
13862+
(SelectModifiers::default(), None)
13863+
};
13864+
1385413865
let mut top_before_distinct = false;
1385513866
let mut top = None;
1385613867
if self.dialect.supports_top_before_distinct() && self.parse_keyword(Keyword::TOP) {
1385713868
top = Some(self.parse_top()?);
1385813869
top_before_distinct = true;
1385913870
}
13860-
let distinct = self.parse_all_or_distinct()?;
13871+
13872+
let distinct = if distinct_select_modifier.is_some() {
13873+
distinct_select_modifier
13874+
} else {
13875+
self.parse_all_or_distinct()?
13876+
};
13877+
1386113878
if !self.dialect.supports_top_before_distinct() && self.parse_keyword(Keyword::TOP) {
1386213879
top = Some(self.parse_top()?);
1386313880
}
@@ -14005,6 +14022,7 @@ impl<'a> Parser<'a> {
1400514022
Ok(Select {
1400614023
select_token: AttachedToken(select_token),
1400714024
distinct,
14025+
select_modifiers,
1400814026
top,
1400914027
top_before_distinct,
1401014028
projection,
@@ -14032,6 +14050,79 @@ impl<'a> Parser<'a> {
1403214050
})
1403314051
}
1403414052

14053+
/// Parses SELECT modifiers and DISTINCT/ALL in any order. Allows HIGH_PRIORITY, STRAIGHT_JOIN,
14054+
/// SQL_SMALL_RESULT, SQL_BIG_RESULT, SQL_BUFFER_RESULT, SQL_NO_CACHE, SQL_CALC_FOUND_ROWS and
14055+
/// DISTINCT/DISTINCTROW/ALL to appear in any order.
14056+
fn parse_select_modifiers(
14057+
&mut self,
14058+
) -> Result<(SelectModifiers, Option<Distinct>), ParserError> {
14059+
let mut modifiers = SelectModifiers::default();
14060+
let mut distinct: Option<Distinct> = None;
14061+
let mut has_all = false;
14062+
14063+
let keywords = &[
14064+
Keyword::ALL,
14065+
Keyword::DISTINCT,
14066+
Keyword::DISTINCTROW,
14067+
Keyword::HIGH_PRIORITY,
14068+
Keyword::STRAIGHT_JOIN,
14069+
Keyword::SQL_SMALL_RESULT,
14070+
Keyword::SQL_BIG_RESULT,
14071+
Keyword::SQL_BUFFER_RESULT,
14072+
Keyword::SQL_NO_CACHE,
14073+
Keyword::SQL_CALC_FOUND_ROWS,
14074+
];
14075+
14076+
while let Some(keyword) = self.parse_one_of_keywords(keywords) {
14077+
match keyword {
14078+
Keyword::ALL => {
14079+
if has_all {
14080+
self.prev_token();
14081+
return self.expected("SELECT without duplicate ALL", self.peek_token());
14082+
}
14083+
if distinct.is_some() {
14084+
self.prev_token();
14085+
return self.expected("DISTINCT alone without ALL", self.peek_token());
14086+
}
14087+
has_all = true;
14088+
}
14089+
Keyword::DISTINCT | Keyword::DISTINCTROW => {
14090+
if distinct.is_some() {
14091+
self.prev_token();
14092+
return self.expected(
14093+
"SELECT without duplicate DISTINCT or DISTINCTROW",
14094+
self.peek_token(),
14095+
);
14096+
}
14097+
if has_all {
14098+
self.prev_token();
14099+
return self.expected(
14100+
"ALL alone without DISTINCT or DISTINCTROW",
14101+
self.peek_token(),
14102+
);
14103+
}
14104+
distinct = Some(Distinct::Distinct);
14105+
}
14106+
Keyword::HIGH_PRIORITY => modifiers.high_priority = true,
14107+
Keyword::STRAIGHT_JOIN => modifiers.straight_join = true,
14108+
Keyword::SQL_SMALL_RESULT => modifiers.sql_small_result = true,
14109+
Keyword::SQL_BIG_RESULT => modifiers.sql_big_result = true,
14110+
Keyword::SQL_BUFFER_RESULT => modifiers.sql_buffer_result = true,
14111+
Keyword::SQL_NO_CACHE => modifiers.sql_no_cache = true,
14112+
Keyword::SQL_CALC_FOUND_ROWS => modifiers.sql_calc_found_rows = true,
14113+
_ => {
14114+
self.prev_token();
14115+
return self.expected(
14116+
"HIGH_PRIORITY, STRAIGHT_JOIN, or other MySQL select modifier",
14117+
self.peek_token(),
14118+
);
14119+
}
14120+
}
14121+
}
14122+
14123+
Ok((modifiers, distinct))
14124+
}
14125+
1403514126
fn parse_value_table_mode(&mut self) -> Result<Option<ValueTableMode>, ParserError> {
1403614127
if !dialect_of!(self is BigQueryDialect) {
1403714128
return Ok(None);

tests/sqlparser_bigquery.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2682,6 +2682,7 @@ fn test_export_data() {
26822682
Span::empty()
26832683
)),
26842684
distinct: None,
2685+
select_modifiers: SelectModifiers::default(),
26852686
top: None,
26862687
top_before_distinct: false,
26872688
projection: vec![
@@ -2786,6 +2787,7 @@ fn test_export_data() {
27862787
Span::empty()
27872788
)),
27882789
distinct: None,
2790+
select_modifiers: SelectModifiers::default(),
27892791
top: None,
27902792
top_before_distinct: false,
27912793
projection: vec![

tests/sqlparser_clickhouse.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ fn parse_map_access_expr() {
4141
assert_eq!(
4242
Select {
4343
distinct: None,
44+
select_modifiers: SelectModifiers::default(),
4445
select_token: AttachedToken::empty(),
4546
top: None,
4647
top_before_distinct: false,

0 commit comments

Comments
 (0)