Skip to content

Commit 460f829

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 6daa46d commit 460f829

14 files changed

Lines changed: 429 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: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4878,14 +4878,17 @@ impl<'a> Parser<'a> {
48784878
/// Parse either `ALL`, `DISTINCT` or `DISTINCT ON (...)`. Returns [`None`] if `ALL` is parsed
48794879
/// and results in a [`ParserError`] if both `ALL` and `DISTINCT` are found.
48804880
pub fn parse_all_or_distinct(&mut self) -> Result<Option<Distinct>, ParserError> {
4881-
let loc = self.peek_token().span.start;
48824881
let all = self.parse_keyword(Keyword::ALL);
48834882
let distinct = self.parse_keyword(Keyword::DISTINCT);
48844883
if !distinct {
48854884
return Ok(None);
48864885
}
48874886
if all {
4888-
return parser_err!("Cannot specify both ALL and DISTINCT".to_string(), loc);
4887+
self.prev_token();
4888+
return self.expected(
4889+
"ALL alone without DISTINCT or DISTINCTROW",
4890+
self.peek_token(),
4891+
);
48894892
}
48904893
let on = self.parse_keyword(Keyword::ON);
48914894
if !on {
@@ -13794,6 +13797,7 @@ impl<'a> Parser<'a> {
1379413797
return Ok(Select {
1379513798
select_token: AttachedToken(from_token),
1379613799
distinct: None,
13800+
select_modifiers: SelectModifiers::default(),
1379713801
top: None,
1379813802
top_before_distinct: false,
1379913803
projection: vec![],
@@ -13822,13 +13826,26 @@ impl<'a> Parser<'a> {
1382213826
let select_token = self.expect_keyword(Keyword::SELECT)?;
1382313827
let value_table_mode = self.parse_value_table_mode()?;
1382413828

13829+
let (select_modifiers, distinct_select_modifier) =
13830+
if self.dialect.supports_select_modifiers() {
13831+
self.parse_select_modifiers()?
13832+
} else {
13833+
(SelectModifiers::default(), None)
13834+
};
13835+
1382513836
let mut top_before_distinct = false;
1382613837
let mut top = None;
1382713838
if self.dialect.supports_top_before_distinct() && self.parse_keyword(Keyword::TOP) {
1382813839
top = Some(self.parse_top()?);
1382913840
top_before_distinct = true;
1383013841
}
13831-
let distinct = self.parse_all_or_distinct()?;
13842+
13843+
let distinct = if distinct_select_modifier.is_some() {
13844+
distinct_select_modifier
13845+
} else {
13846+
self.parse_all_or_distinct()?
13847+
};
13848+
1383213849
if !self.dialect.supports_top_before_distinct() && self.parse_keyword(Keyword::TOP) {
1383313850
top = Some(self.parse_top()?);
1383413851
}
@@ -13976,6 +13993,7 @@ impl<'a> Parser<'a> {
1397613993
Ok(Select {
1397713994
select_token: AttachedToken(select_token),
1397813995
distinct,
13996+
select_modifiers,
1397913997
top,
1398013998
top_before_distinct,
1398113999
projection,
@@ -14003,6 +14021,81 @@ impl<'a> Parser<'a> {
1400314021
})
1400414022
}
1400514023

14024+
/// Parses SELECT modifiers and DISTINCT/ALL in any order.
14025+
/// Allows HIGH_PRIORITY, STRAIGHT_JOIN, SQL_SMALL_RESULT, SQL_BIG_RESULT,
14026+
/// SQL_BUFFER_RESULT, SQL_NO_CACHE, SQL_CALC_FOUND_ROWS and DISTINCT/DISTINCTROW/ALL
14027+
/// to appear in any order.
14028+
/// Returns (SelectModifiers, Option<Distinct>).
14029+
fn parse_select_modifiers(
14030+
&mut self,
14031+
) -> Result<(SelectModifiers, Option<Distinct>), ParserError> {
14032+
let mut modifiers = SelectModifiers::default();
14033+
let mut distinct: Option<Distinct> = None;
14034+
let mut has_all = false;
14035+
14036+
let keywords = &[
14037+
Keyword::ALL,
14038+
Keyword::DISTINCT,
14039+
Keyword::DISTINCTROW,
14040+
Keyword::HIGH_PRIORITY,
14041+
Keyword::STRAIGHT_JOIN,
14042+
Keyword::SQL_SMALL_RESULT,
14043+
Keyword::SQL_BIG_RESULT,
14044+
Keyword::SQL_BUFFER_RESULT,
14045+
Keyword::SQL_NO_CACHE,
14046+
Keyword::SQL_CALC_FOUND_ROWS,
14047+
];
14048+
14049+
while let Some(keyword) = self.parse_one_of_keywords(keywords) {
14050+
match keyword {
14051+
Keyword::ALL => {
14052+
if has_all {
14053+
self.prev_token();
14054+
return self.expected("SELECT without duplicate ALL", self.peek_token());
14055+
}
14056+
if distinct.is_some() {
14057+
self.prev_token();
14058+
return self.expected("DISTINCT alone without ALL", self.peek_token());
14059+
}
14060+
has_all = true;
14061+
}
14062+
Keyword::DISTINCT | Keyword::DISTINCTROW => {
14063+
if distinct.is_some() {
14064+
self.prev_token();
14065+
return self.expected(
14066+
"SELECT without duplicate DISTINCT or DISTINCTROW",
14067+
self.peek_token(),
14068+
);
14069+
}
14070+
if has_all {
14071+
self.prev_token();
14072+
return self.expected(
14073+
"ALL alone without DISTINCT or DISTINCTROW",
14074+
self.peek_token(),
14075+
);
14076+
}
14077+
distinct = Some(Distinct::Distinct);
14078+
}
14079+
Keyword::HIGH_PRIORITY => modifiers.high_priority = true,
14080+
Keyword::STRAIGHT_JOIN => modifiers.straight_join = true,
14081+
Keyword::SQL_SMALL_RESULT => modifiers.sql_small_result = true,
14082+
Keyword::SQL_BIG_RESULT => modifiers.sql_big_result = true,
14083+
Keyword::SQL_BUFFER_RESULT => modifiers.sql_buffer_result = true,
14084+
Keyword::SQL_NO_CACHE => modifiers.sql_no_cache = true,
14085+
Keyword::SQL_CALC_FOUND_ROWS => modifiers.sql_calc_found_rows = true,
14086+
_ => {
14087+
self.prev_token();
14088+
return self.expected(
14089+
"HIGH_PRIORITY, STRAIGHT_JOIN, or other MySQL select modifier",
14090+
self.peek_token(),
14091+
);
14092+
}
14093+
}
14094+
}
14095+
14096+
Ok((modifiers, distinct))
14097+
}
14098+
1400614099
fn parse_value_table_mode(&mut self) -> Result<Option<ValueTableMode>, ParserError> {
1400714100
if !dialect_of!(self is BigQueryDialect) {
1400814101
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)