Skip to content

Commit a2459ae

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 5ed5af1 commit a2459ae

14 files changed

Lines changed: 437 additions & 15 deletions

src/ast/mod.rs

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

110111
pub use self::trigger::{

src/ast/query.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,108 @@ 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 in any order after
340+
/// SELECT and before the column list, can be repeated, and can be interleaved with
341+
/// 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+
///
360+
/// <https://dev.mysql.com/doc/refman/8.4/en/select.html>
361+
pub high_priority: bool,
362+
/// `STRAIGHT_JOIN` forces the optimizer to join tables in the order listed in the FROM clause.
363+
///
364+
/// <https://dev.mysql.com/doc/refman/8.4/en/select.html>
365+
pub straight_join: bool,
366+
/// `SQL_SMALL_RESULT` hints that the result set is small, using in-memory temp tables.
367+
///
368+
/// <https://dev.mysql.com/doc/refman/8.4/en/select.html>
369+
pub sql_small_result: bool,
370+
/// `SQL_BIG_RESULT` hints that the result set is large, using disk-based temp tables.
371+
///
372+
/// <https://dev.mysql.com/doc/refman/8.4/en/select.html>
373+
pub sql_big_result: bool,
374+
/// `SQL_BUFFER_RESULT` forces the result to be put into a temporary table to release locks early.
375+
///
376+
/// <https://dev.mysql.com/doc/refman/8.4/en/select.html>
377+
pub sql_buffer_result: bool,
378+
/// `SQL_NO_CACHE` tells MySQL not to cache the query result. (Deprecated in 8.4+.)
379+
///
380+
/// <https://dev.mysql.com/doc/refman/8.4/en/select.html>
381+
pub sql_no_cache: bool,
382+
/// `SQL_CALC_FOUND_ROWS` tells MySQL to calculate the total number of rows. (Deprecated in 8.0.17+.)
383+
///
384+
/// - [MySQL SELECT modifiers](https://dev.mysql.com/doc/refman/8.4/en/select.html)
385+
/// - [`FOUND_ROWS()`](https://dev.mysql.com/doc/refman/8.4/en/information-functions.html#function_found-rows)
386+
pub sql_calc_found_rows: bool,
387+
}
388+
389+
impl fmt::Display for SelectModifiers {
390+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
391+
if self.high_priority {
392+
f.write_str(" HIGH_PRIORITY")?;
393+
}
394+
if self.straight_join {
395+
f.write_str(" STRAIGHT_JOIN")?;
396+
}
397+
if self.sql_small_result {
398+
f.write_str(" SQL_SMALL_RESULT")?;
399+
}
400+
if self.sql_big_result {
401+
f.write_str(" SQL_BIG_RESULT")?;
402+
}
403+
if self.sql_buffer_result {
404+
f.write_str(" SQL_BUFFER_RESULT")?;
405+
}
406+
if self.sql_no_cache {
407+
f.write_str(" SQL_NO_CACHE")?;
408+
}
409+
if self.sql_calc_found_rows {
410+
f.write_str(" SQL_CALC_FOUND_ROWS")?;
411+
}
412+
Ok(())
413+
}
414+
}
415+
416+
impl SelectModifiers {
417+
/// Returns true if any of the modifiers are set.
418+
pub fn is_any_set(&self) -> bool {
419+
// Using irrefutable destructuring to catch fields added in the future
420+
let Self {
421+
high_priority,
422+
straight_join,
423+
sql_small_result,
424+
sql_big_result,
425+
sql_buffer_result,
426+
sql_no_cache,
427+
sql_calc_found_rows,
428+
} = self;
429+
*high_priority
430+
|| *straight_join
431+
|| *sql_small_result
432+
|| *sql_big_result
433+
|| *sql_buffer_result
434+
|| *sql_no_cache
435+
|| *sql_calc_found_rows
436+
}
437+
}
438+
337439
/// A restricted variant of `SELECT` (without CTEs/`ORDER BY`), which may
338440
/// appear either as the only body item of a `Query`, or as an operand
339441
/// to a set operation like `UNION`.
@@ -350,6 +452,10 @@ pub struct Select {
350452
pub optimizer_hint: Option<OptimizerHint>,
351453
/// `SELECT [DISTINCT] ...`
352454
pub distinct: Option<Distinct>,
455+
/// MySQL-specific SELECT modifiers.
456+
///
457+
/// See [MySQL SELECT](https://dev.mysql.com/doc/refman/8.4/en/select.html).
458+
pub select_modifiers: Option<SelectModifiers>,
353459
/// MSSQL syntax: `TOP (<N>) [ PERCENT ] [ WITH TIES ]`
354460
pub top: Option<Top>,
355461
/// Whether the top was located before `ALL`/`DISTINCT`
@@ -442,6 +548,10 @@ impl fmt::Display for Select {
442548
}
443549
}
444550

551+
if let Some(ref select_modifiers) = self.select_modifiers {
552+
select_modifiers.fmt(f)?;
553+
}
554+
445555
if !self.projection.is_empty() {
446556
indented_list(f, &self.projection)?;
447557
}

src/ast/spans.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2238,7 +2238,8 @@ impl Spanned for Select {
22382238
select_token,
22392239
optimizer_hint: _,
22402240
distinct: _, // todo
2241-
top: _, // todo, mysql specific
2241+
select_modifiers: _,
2242+
top: _, // todo, mysql specific
22422243
projection,
22432244
exclude: _,
22442245
into,

src/dialect/mod.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,19 @@ pub trait Dialect: Debug + Any {
698698
false
699699
}
700700

701+
/// Returns true if the dialect supports MySQL-specific SELECT modifiers
702+
/// like `HIGH_PRIORITY`, `STRAIGHT_JOIN`, `SQL_SMALL_RESULT`, etc.
703+
///
704+
/// For example:
705+
/// ```sql
706+
/// SELECT HIGH_PRIORITY STRAIGHT_JOIN SQL_SMALL_RESULT * FROM t1 JOIN t2 ON ...
707+
/// ```
708+
///
709+
/// [MySQL](https://dev.mysql.com/doc/refman/8.4/en/select.html)
710+
fn supports_select_modifiers(&self) -> bool {
711+
false
712+
}
713+
701714
/// Dialect-specific infix parser override
702715
///
703716
/// 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: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13872,6 +13872,7 @@ impl<'a> Parser<'a> {
1387213872
select_token: AttachedToken(from_token),
1387313873
optimizer_hint: None,
1387413874
distinct: None,
13875+
select_modifiers: None,
1387513876
top: None,
1387613877
top_before_distinct: false,
1387713878
projection: vec![],
@@ -13901,13 +13902,26 @@ impl<'a> Parser<'a> {
1390113902
let optimizer_hint = self.maybe_parse_optimizer_hint()?;
1390213903
let value_table_mode = self.parse_value_table_mode()?;
1390313904

13905+
let (select_modifiers, distinct_select_modifier) =
13906+
if self.dialect.supports_select_modifiers() {
13907+
self.parse_select_modifiers()?
13908+
} else {
13909+
(None, None)
13910+
};
13911+
1390413912
let mut top_before_distinct = false;
1390513913
let mut top = None;
1390613914
if self.dialect.supports_top_before_distinct() && self.parse_keyword(Keyword::TOP) {
1390713915
top = Some(self.parse_top()?);
1390813916
top_before_distinct = true;
1390913917
}
13910-
let distinct = self.parse_all_or_distinct()?;
13918+
13919+
let distinct = if distinct_select_modifier.is_some() {
13920+
distinct_select_modifier
13921+
} else {
13922+
self.parse_all_or_distinct()?
13923+
};
13924+
1391113925
if !self.dialect.supports_top_before_distinct() && self.parse_keyword(Keyword::TOP) {
1391213926
top = Some(self.parse_top()?);
1391313927
}
@@ -14055,6 +14069,7 @@ impl<'a> Parser<'a> {
1405514069
select_token: AttachedToken(select_token),
1405614070
optimizer_hint,
1405714071
distinct,
14072+
select_modifiers,
1405814073
top,
1405914074
top_before_distinct,
1406014075
projection,
@@ -14131,6 +14146,68 @@ impl<'a> Parser<'a> {
1413114146
}
1413214147
}
1413314148

14149+
/// Parses MySQL SELECT modifiers and DISTINCT/ALL in any order.
14150+
///
14151+
/// Manual testing shows odifiers can appear in any order, and modifiers other than DISTINCT/ALL
14152+
/// can be repeated.
14153+
///
14154+
/// <https://dev.mysql.com/doc/refman/8.4/en/select.html>
14155+
fn parse_select_modifiers(
14156+
&mut self,
14157+
) -> Result<(Option<SelectModifiers>, Option<Distinct>), ParserError> {
14158+
let mut modifiers = SelectModifiers::default();
14159+
let mut distinct = None;
14160+
14161+
let keywords = &[
14162+
Keyword::ALL,
14163+
Keyword::DISTINCT,
14164+
Keyword::DISTINCTROW,
14165+
Keyword::HIGH_PRIORITY,
14166+
Keyword::STRAIGHT_JOIN,
14167+
Keyword::SQL_SMALL_RESULT,
14168+
Keyword::SQL_BIG_RESULT,
14169+
Keyword::SQL_BUFFER_RESULT,
14170+
Keyword::SQL_NO_CACHE,
14171+
Keyword::SQL_CALC_FOUND_ROWS,
14172+
];
14173+
14174+
while let Some(keyword) = self.parse_one_of_keywords(keywords) {
14175+
match keyword {
14176+
Keyword::ALL | Keyword::DISTINCT if distinct.is_none() => {
14177+
self.prev_token();
14178+
distinct = self.parse_all_or_distinct()?;
14179+
}
14180+
// DISTINCTROW is a MySQL-specific legacy (but not deprecated) alias for DISTINCT
14181+
Keyword::DISTINCTROW if distinct.is_none() => {
14182+
distinct = Some(Distinct::Distinct);
14183+
}
14184+
Keyword::HIGH_PRIORITY => modifiers.high_priority = true,
14185+
Keyword::STRAIGHT_JOIN => modifiers.straight_join = true,
14186+
Keyword::SQL_SMALL_RESULT => modifiers.sql_small_result = true,
14187+
Keyword::SQL_BIG_RESULT => modifiers.sql_big_result = true,
14188+
Keyword::SQL_BUFFER_RESULT => modifiers.sql_buffer_result = true,
14189+
Keyword::SQL_NO_CACHE => modifiers.sql_no_cache = true,
14190+
Keyword::SQL_CALC_FOUND_ROWS => modifiers.sql_calc_found_rows = true,
14191+
_ => {
14192+
self.prev_token();
14193+
return self.expected(
14194+
"HIGH_PRIORITY, STRAIGHT_JOIN, or other MySQL select modifier",
14195+
self.peek_token(),
14196+
);
14197+
}
14198+
}
14199+
}
14200+
14201+
// Avoid polluting the AST with `Some(SelectModifiers::default())` empty value unless there
14202+
// actually were some modifiers set.
14203+
let select_modifiers = if modifiers.is_any_set() {
14204+
Some(modifiers)
14205+
} else {
14206+
None
14207+
};
14208+
Ok((select_modifiers, distinct))
14209+
}
14210+
1413414211
fn parse_value_table_mode(&mut self) -> Result<Option<ValueTableMode>, ParserError> {
1413514212
if !dialect_of!(self is BigQueryDialect) {
1413614213
return Ok(None);

tests/sqlparser_bigquery.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2683,6 +2683,7 @@ fn test_export_data() {
26832683
)),
26842684
optimizer_hint: None,
26852685
distinct: None,
2686+
select_modifiers: None,
26862687
top: None,
26872688
top_before_distinct: false,
26882689
projection: vec![
@@ -2788,6 +2789,7 @@ fn test_export_data() {
27882789
)),
27892790
optimizer_hint: None,
27902791
distinct: None,
2792+
select_modifiers: None,
27912793
top: None,
27922794
top_before_distinct: false,
27932795
projection: vec![

tests/sqlparser_clickhouse.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,10 @@ fn parse_map_access_expr() {
4040
let select = clickhouse().verified_only_select(sql);
4141
assert_eq!(
4242
Select {
43-
distinct: None,
44-
optimizer_hint: None,
4543
select_token: AttachedToken::empty(),
44+
optimizer_hint: None,
45+
distinct: None,
46+
select_modifiers: None,
4647
top: None,
4748
top_before_distinct: false,
4849
projection: vec![UnnamedExpr(Expr::CompoundFieldAccess {

0 commit comments

Comments
 (0)