Skip to content

Commit 5e5c16c

Browse files
authored
MySQL: Add support for SELECT modifiers (apache#2172)
1 parent 3ac5670 commit 5e5c16c

File tree

14 files changed

+480
-30
lines changed

14 files changed

+480
-30
lines changed

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: 118 additions & 1 deletion
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
}
@@ -3351,8 +3461,14 @@ impl fmt::Display for NonBlock {
33513461
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
33523462
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
33533463
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
3354-
/// `DISTINCT` or `DISTINCT ON (...)` modifiers for `SELECT` lists.
3464+
/// `ALL`, `DISTINCT`, or `DISTINCT ON (...)` modifiers for `SELECT` lists.
33553465
pub enum Distinct {
3466+
/// `ALL` (keep duplicate rows)
3467+
///
3468+
/// Generally this is the default if omitted, but omission should be represented as
3469+
/// `None::<Option<Distinct>>`
3470+
All,
3471+
33563472
/// `DISTINCT` (remove duplicate rows)
33573473
Distinct,
33583474

@@ -3363,6 +3479,7 @@ pub enum Distinct {
33633479
impl fmt::Display for Distinct {
33643480
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
33653481
match self {
3482+
Distinct::All => write!(f, "ALL"),
33663483
Distinct::Distinct => write!(f, "DISTINCT"),
33673484
Distinct::On(col_names) => {
33683485
let col_names = display_comma_separated(col_names);

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: 99 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4927,16 +4927,27 @@ impl<'a> Parser<'a> {
49274927
/// and results in a [`ParserError`] if both `ALL` and `DISTINCT` are found.
49284928
pub fn parse_all_or_distinct(&mut self) -> Result<Option<Distinct>, ParserError> {
49294929
let loc = self.peek_token().span.start;
4930-
let all = self.parse_keyword(Keyword::ALL);
4931-
let distinct = self.parse_keyword(Keyword::DISTINCT);
4932-
if !distinct {
4933-
return Ok(None);
4934-
}
4935-
if all {
4936-
return parser_err!("Cannot specify both ALL and DISTINCT".to_string(), loc);
4937-
}
4938-
let on = self.parse_keyword(Keyword::ON);
4939-
if !on {
4930+
let distinct = match self.parse_one_of_keywords(&[Keyword::ALL, Keyword::DISTINCT]) {
4931+
Some(Keyword::ALL) => {
4932+
if self.peek_keyword(Keyword::DISTINCT) {
4933+
return parser_err!("Cannot specify ALL then DISTINCT".to_string(), loc);
4934+
}
4935+
Some(Distinct::All)
4936+
}
4937+
Some(Keyword::DISTINCT) => {
4938+
if self.peek_keyword(Keyword::ALL) {
4939+
return parser_err!("Cannot specify DISTINCT then ALL".to_string(), loc);
4940+
}
4941+
Some(Distinct::Distinct)
4942+
}
4943+
None => return Ok(None),
4944+
_ => return parser_err!("ALL or DISTINCT", loc),
4945+
};
4946+
4947+
let Some(Distinct::Distinct) = distinct else {
4948+
return Ok(distinct);
4949+
};
4950+
if !self.parse_keyword(Keyword::ON) {
49404951
return Ok(Some(Distinct::Distinct));
49414952
}
49424953

@@ -13861,6 +13872,7 @@ impl<'a> Parser<'a> {
1386113872
select_token: AttachedToken(from_token),
1386213873
optimizer_hint: None,
1386313874
distinct: None,
13875+
select_modifiers: None,
1386413876
top: None,
1386513877
top_before_distinct: false,
1386613878
projection: vec![],
@@ -13890,13 +13902,26 @@ impl<'a> Parser<'a> {
1389013902
let optimizer_hint = self.maybe_parse_optimizer_hint()?;
1389113903
let value_table_mode = self.parse_value_table_mode()?;
1389213904

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+
1389313912
let mut top_before_distinct = false;
1389413913
let mut top = None;
1389513914
if self.dialect.supports_top_before_distinct() && self.parse_keyword(Keyword::TOP) {
1389613915
top = Some(self.parse_top()?);
1389713916
top_before_distinct = true;
1389813917
}
13899-
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+
1390013925
if !self.dialect.supports_top_before_distinct() && self.parse_keyword(Keyword::TOP) {
1390113926
top = Some(self.parse_top()?);
1390213927
}
@@ -14044,6 +14069,7 @@ impl<'a> Parser<'a> {
1404414069
select_token: AttachedToken(select_token),
1404514070
optimizer_hint,
1404614071
distinct,
14072+
select_modifiers,
1404714073
top,
1404814074
top_before_distinct,
1404914075
projection,
@@ -14120,6 +14146,68 @@ impl<'a> Parser<'a> {
1412014146
}
1412114147
}
1412214148

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+
1412314211
fn parse_value_table_mode(&mut self) -> Result<Option<ValueTableMode>, ParserError> {
1412414212
if !dialect_of!(self is BigQueryDialect) {
1412514213
return Ok(None);

0 commit comments

Comments
 (0)