Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,15 @@ pub use self::query::{
OffsetRows, OpenJsonTableColumn, OrderBy, OrderByExpr, OrderByKind, OrderByOptions,
PipeOperator, PivotValueSource, ProjectionSelect, Query, RenameSelectItem,
RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select,
SelectFlavor, SelectInto, SelectItem, SelectItemQualifiedWildcardKind, SetExpr, SetOperator,
SetQuantifier, Setting, SymbolDefinition, Table, TableAlias, TableAliasColumnDef, TableFactor,
TableFunctionArgs, TableIndexHintForClause, TableIndexHintType, TableIndexHints,
TableIndexType, TableSample, TableSampleBucket, TableSampleKind, TableSampleMethod,
TableSampleModifier, TableSampleQuantity, TableSampleSeed, TableSampleSeedModifier,
TableSampleUnit, TableVersion, TableWithJoins, Top, TopQuantity, UpdateTableFromKind,
ValueTableMode, Values, WildcardAdditionalOptions, With, WithFill, XmlNamespaceDefinition,
XmlPassingArgument, XmlPassingClause, XmlTableColumn, XmlTableColumnOption,
SelectFlavor, SelectInto, SelectItem, SelectItemQualifiedWildcardKind, SelectModifiers,
SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, TableAlias,
TableAliasColumnDef, TableFactor, TableFunctionArgs, TableIndexHintForClause,
TableIndexHintType, TableIndexHints, TableIndexType, TableSample, TableSampleBucket,
TableSampleKind, TableSampleMethod, TableSampleModifier, TableSampleQuantity, TableSampleSeed,
TableSampleSeedModifier, TableSampleUnit, TableVersion, TableWithJoins, Top, TopQuantity,
UpdateTableFromKind, ValueTableMode, Values, WildcardAdditionalOptions, With, WithFill,
XmlNamespaceDefinition, XmlPassingArgument, XmlPassingClause, XmlTableColumn,
XmlTableColumnOption,
};

pub use self::trigger::{
Expand Down
70 changes: 70 additions & 0 deletions src/ast/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,70 @@ pub enum SelectFlavor {
FromFirstNoSelect,
}

/// MySQL-specific SELECT modifiers that appear after the SELECT keyword.
///
/// These modifiers affect query execution and optimization. They can appear
/// in any order after SELECT and before the column list, and can be
/// interleaved with DISTINCT/DISTINCTROW/ALL:
///
/// ```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] ...
/// ```
///
/// See [MySQL SELECT](https://dev.mysql.com/doc/refman/8.4/en/select.html).
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct SelectModifiers {
/// `HIGH_PRIORITY` gives the SELECT higher priority than statements that update a table.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we include the mysql links individually on the fields as well? that way if other dialects add their own variants in the future the doc would be consistent

pub high_priority: bool,
/// `STRAIGHT_JOIN` forces the optimizer to join tables in the order listed in the FROM clause.
pub straight_join: bool,
/// `SQL_SMALL_RESULT` hints that the result set is small, using in-memory temp tables.
pub sql_small_result: bool,
/// `SQL_BIG_RESULT` hints that the result set is large, using disk-based temp tables.
pub sql_big_result: bool,
/// `SQL_BUFFER_RESULT` forces the result to be put into a temporary table to release locks early.
pub sql_buffer_result: bool,
/// `SQL_NO_CACHE` tells MySQL not to cache the query result.
pub sql_no_cache: bool,
/// `SQL_CALC_FOUND_ROWS` tells MySQL to calculate the total number of rows.
pub sql_calc_found_rows: bool,
}

impl fmt::Display for SelectModifiers {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.high_priority {
f.write_str(" HIGH_PRIORITY")?;
}
if self.straight_join {
f.write_str(" STRAIGHT_JOIN")?;
}
if self.sql_small_result {
f.write_str(" SQL_SMALL_RESULT")?;
}
if self.sql_big_result {
f.write_str(" SQL_BIG_RESULT")?;
}
if self.sql_buffer_result {
f.write_str(" SQL_BUFFER_RESULT")?;
}
if self.sql_no_cache {
f.write_str(" SQL_NO_CACHE")?;
}
if self.sql_calc_found_rows {
f.write_str(" SQL_CALC_FOUND_ROWS")?;
}
Ok(())
}
}

/// A restricted variant of `SELECT` (without CTEs/`ORDER BY`), which may
/// appear either as the only body item of a `Query`, or as an operand
/// to a set operation like `UNION`.
Expand All @@ -345,6 +409,10 @@ pub struct Select {
pub select_token: AttachedToken,
/// `SELECT [DISTINCT] ...`
pub distinct: Option<Distinct>,
/// MySQL-specific SELECT modifiers.
///
/// See [MySQL SELECT](https://dev.mysql.com/doc/refman/8.4/en/select.html).
pub select_modifiers: SelectModifiers,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub select_modifiers: SelectModifiers,
pub select_modifiers: Option<SelectModifiers>,

/// MSSQL syntax: `TOP (<N>) [ PERCENT ] [ WITH TIES ]`
pub top: Option<Top>,
/// Whether the top was located before `ALL`/`DISTINCT`
Expand Down Expand Up @@ -415,6 +483,8 @@ impl fmt::Display for Select {
value_table_mode.fmt(f)?;
}

self.select_modifiers.fmt(f)?;

if let Some(ref top) = self.top {
if self.top_before_distinct {
f.write_str(" ")?;
Expand Down
5 changes: 3 additions & 2 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2230,7 +2230,8 @@ impl Spanned for Select {
let Select {
select_token,
distinct: _, // todo
top: _, // todo, mysql specific
select_modifiers: _,
top: _, // todo, mysql specific
projection,
exclude: _,
into,
Expand Down Expand Up @@ -2801,7 +2802,7 @@ WHERE id = 1
UPDATE SET target_table.description = source_table.description

WHEN MATCHED AND target_table.x != 'X' THEN DELETE
WHEN NOT MATCHED AND 1 THEN INSERT (product, quantity) ROW
WHEN NOT MATCHED AND 1 THEN INSERT (product, quantity) ROW
"#;

let r = Parser::parse_sql(&crate::dialect::GenericDialect, sql).unwrap();
Expand Down
13 changes: 13 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,19 @@ pub trait Dialect: Debug + Any {
false
}

/// Returns true if the dialect supports MySQL-specific SELECT modifiers
/// like `HIGH_PRIORITY`, `STRAIGHT_JOIN`, `SQL_SMALL_RESULT`, etc.
///
/// For example:
/// ```sql
/// SELECT HIGH_PRIORITY STRAIGHT_JOIN SQL_SMALL_RESULT * FROM t1 JOIN t2 ON ...
/// ```
///
/// [MySQL](https://dev.mysql.com/doc/refman/8.4/en/select.html)
fn supports_select_modifiers(&self) -> bool {
false
}

/// Dialect-specific infix parser override
///
/// This method is called to parse the next infix expression.
Expand Down
4 changes: 4 additions & 0 deletions src/dialect/mysql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ impl Dialect for MySqlDialect {
true
}

fn supports_select_modifiers(&self) -> bool {
true
}

fn supports_set_names(&self) -> bool {
true
}
Expand Down
6 changes: 6 additions & 0 deletions src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ define_keywords!(
DISCARD,
DISCONNECT,
DISTINCT,
DISTINCTROW,
DISTRIBUTE,
DIV,
DO,
Expand Down Expand Up @@ -956,6 +957,11 @@ define_keywords!(
SQLEXCEPTION,
SQLSTATE,
SQLWARNING,
SQL_BIG_RESULT,
SQL_BUFFER_RESULT,
SQL_CALC_FOUND_ROWS,
SQL_NO_CACHE,
SQL_SMALL_RESULT,
SQRT,
SRID,
STABLE,
Expand Down
97 changes: 94 additions & 3 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4906,14 +4906,17 @@ impl<'a> Parser<'a> {
/// Parse either `ALL`, `DISTINCT` or `DISTINCT ON (...)`. Returns [`None`] if `ALL` is parsed
/// and results in a [`ParserError`] if both `ALL` and `DISTINCT` are found.
pub fn parse_all_or_distinct(&mut self) -> Result<Option<Distinct>, ParserError> {
let loc = self.peek_token().span.start;
let all = self.parse_keyword(Keyword::ALL);
let distinct = self.parse_keyword(Keyword::DISTINCT);
if !distinct {
return Ok(None);
}
if all {
return parser_err!("Cannot specify both ALL and DISTINCT".to_string(), loc);
self.prev_token();
return self.expected(
"ALL alone without DISTINCT or DISTINCTROW",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this diff seems incorrect to mention DISTINCTROW?

self.peek_token(),
);
}
let on = self.parse_keyword(Keyword::ON);
if !on {
Expand Down Expand Up @@ -13823,6 +13826,7 @@ impl<'a> Parser<'a> {
return Ok(Select {
select_token: AttachedToken(from_token),
distinct: None,
select_modifiers: SelectModifiers::default(),
top: None,
top_before_distinct: false,
projection: vec![],
Expand Down Expand Up @@ -13851,13 +13855,26 @@ impl<'a> Parser<'a> {
let select_token = self.expect_keyword(Keyword::SELECT)?;
let value_table_mode = self.parse_value_table_mode()?;

let (select_modifiers, distinct_select_modifier) =
if self.dialect.supports_select_modifiers() {
self.parse_select_modifiers()?
} else {
(SelectModifiers::default(), None)
};

let mut top_before_distinct = false;
let mut top = None;
if self.dialect.supports_top_before_distinct() && self.parse_keyword(Keyword::TOP) {
top = Some(self.parse_top()?);
top_before_distinct = true;
}
let distinct = self.parse_all_or_distinct()?;

let distinct = if distinct_select_modifier.is_some() {
distinct_select_modifier
} else {
self.parse_all_or_distinct()?
};

if !self.dialect.supports_top_before_distinct() && self.parse_keyword(Keyword::TOP) {
top = Some(self.parse_top()?);
}
Expand Down Expand Up @@ -14005,6 +14022,7 @@ impl<'a> Parser<'a> {
Ok(Select {
select_token: AttachedToken(select_token),
distinct,
select_modifiers,
top,
top_before_distinct,
projection,
Expand Down Expand Up @@ -14032,6 +14050,79 @@ impl<'a> Parser<'a> {
})
}

/// Parses SELECT modifiers and DISTINCT/ALL in any order. Allows HIGH_PRIORITY, STRAIGHT_JOIN,
/// SQL_SMALL_RESULT, SQL_BIG_RESULT, SQL_BUFFER_RESULT, SQL_NO_CACHE, SQL_CALC_FOUND_ROWS and
/// DISTINCT/DISTINCTROW/ALL to appear in any order.
Comment on lines +14053 to +14055
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Parses SELECT modifiers and DISTINCT/ALL in any order. Allows HIGH_PRIORITY, STRAIGHT_JOIN,
/// SQL_SMALL_RESULT, SQL_BIG_RESULT, SQL_BUFFER_RESULT, SQL_NO_CACHE, SQL_CALC_FOUND_ROWS and
/// DISTINCT/DISTINCTROW/ALL to appear in any order.
/// Parses `SELECT` modifiers and `DISTINCT/ALL` in any order.

Thinking we can avoid enumerating the variants so that they don't go out of sync with the impl

fn parse_select_modifiers(
&mut self,
) -> Result<(SelectModifiers, Option<Distinct>), ParserError> {
let mut modifiers = SelectModifiers::default();
let mut distinct: Option<Distinct> = None;
let mut has_all = false;
Comment on lines +14060 to +14061
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we instead introduce a proper enum to represent the three states? the current impl I think is harder to follow which combinations are valid and if/when the specified option is ignored in the output.


let keywords = &[
Keyword::ALL,
Keyword::DISTINCT,
Keyword::DISTINCTROW,
Keyword::HIGH_PRIORITY,
Keyword::STRAIGHT_JOIN,
Keyword::SQL_SMALL_RESULT,
Keyword::SQL_BIG_RESULT,
Keyword::SQL_BUFFER_RESULT,
Keyword::SQL_NO_CACHE,
Keyword::SQL_CALC_FOUND_ROWS,
];

while let Some(keyword) = self.parse_one_of_keywords(keywords) {
match keyword {
Keyword::ALL => {
if has_all {
self.prev_token();
return self.expected("SELECT without duplicate ALL", self.peek_token());
}
if distinct.is_some() {
self.prev_token();
return self.expected("DISTINCT alone without ALL", self.peek_token());
}
has_all = true;
}
Keyword::DISTINCT | Keyword::DISTINCTROW => {
if distinct.is_some() {
self.prev_token();
return self.expected(
"SELECT without duplicate DISTINCT or DISTINCTROW",
self.peek_token(),
);
}
if has_all {
self.prev_token();
return self.expected(
"ALL alone without DISTINCT or DISTINCTROW",
self.peek_token(),
);
}
distinct = Some(Distinct::Distinct);
}
Keyword::HIGH_PRIORITY => modifiers.high_priority = true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Keyword::HIGH_PRIORITY => modifiers.high_priority = true,
Keyword::HIGH_PRIORITY if !modifiers.high_priority => modifiers.high_priority = true,

similar for the others is something like this needed if it's invalid that they occur multiple times (can we add test cases for the behavior)?

Keyword::STRAIGHT_JOIN => modifiers.straight_join = true,
Keyword::SQL_SMALL_RESULT => modifiers.sql_small_result = true,
Keyword::SQL_BIG_RESULT => modifiers.sql_big_result = true,
Keyword::SQL_BUFFER_RESULT => modifiers.sql_buffer_result = true,
Keyword::SQL_NO_CACHE => modifiers.sql_no_cache = true,
Keyword::SQL_CALC_FOUND_ROWS => modifiers.sql_calc_found_rows = true,
_ => {
self.prev_token();
return self.expected(
"HIGH_PRIORITY, STRAIGHT_JOIN, or other MySQL select modifier",
self.peek_token(),
);
}
}
}

Ok((modifiers, distinct))
}

fn parse_value_table_mode(&mut self) -> Result<Option<ValueTableMode>, ParserError> {
if !dialect_of!(self is BigQueryDialect) {
return Ok(None);
Expand Down
2 changes: 2 additions & 0 deletions tests/sqlparser_bigquery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2682,6 +2682,7 @@ fn test_export_data() {
Span::empty()
)),
distinct: None,
select_modifiers: SelectModifiers::default(),
top: None,
top_before_distinct: false,
projection: vec![
Expand Down Expand Up @@ -2786,6 +2787,7 @@ fn test_export_data() {
Span::empty()
)),
distinct: None,
select_modifiers: SelectModifiers::default(),
top: None,
top_before_distinct: false,
projection: vec![
Expand Down
1 change: 1 addition & 0 deletions tests/sqlparser_clickhouse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ fn parse_map_access_expr() {
assert_eq!(
Select {
distinct: None,
select_modifiers: SelectModifiers::default(),
select_token: AttachedToken::empty(),
top: None,
top_before_distinct: false,
Expand Down
Loading