From a437a325f650d6bd485a54cbd518087dbf6caa64 Mon Sep 17 00:00:00 2001 From: Michael Victor Zink Date: Thu, 15 Jan 2026 21:00:11 -0800 Subject: [PATCH] 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 --- src/ast/mod.rs | 17 +-- src/ast/query.rs | 70 +++++++++++++ src/ast/spans.rs | 5 +- src/dialect/mod.rs | 13 +++ src/dialect/mysql.rs | 4 + src/keywords.rs | 6 ++ src/parser/mod.rs | 97 ++++++++++++++++- tests/sqlparser_bigquery.rs | 2 + tests/sqlparser_clickhouse.rs | 1 + tests/sqlparser_common.rs | 30 +++++- tests/sqlparser_duckdb.rs | 2 + tests/sqlparser_mssql.rs | 3 + tests/sqlparser_mysql.rs | 191 ++++++++++++++++++++++++++++++++++ tests/sqlparser_postgres.rs | 3 + 14 files changed, 427 insertions(+), 17 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d77186bc7..7f00cdd2e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -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::{ diff --git a/src/ast/query.rs b/src/ast/query.rs index a1fc33b6a..f7dbd16d4 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -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. + 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`. @@ -345,6 +409,10 @@ pub struct Select { pub select_token: AttachedToken, /// `SELECT [DISTINCT] ...` pub distinct: Option, + /// MySQL-specific SELECT modifiers. + /// + /// See [MySQL SELECT](https://dev.mysql.com/doc/refman/8.4/en/select.html). + pub select_modifiers: SelectModifiers, /// MSSQL syntax: `TOP () [ PERCENT ] [ WITH TIES ]` pub top: Option, /// Whether the top was located before `ALL`/`DISTINCT` @@ -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(" ")?; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 488c88624..91c850c90 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -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, @@ -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(); diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index cd7fdee12..07cf88901 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -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. diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs index 81aa9d445..2d721deb7 100644 --- a/src/dialect/mysql.rs +++ b/src/dialect/mysql.rs @@ -156,6 +156,10 @@ impl Dialect for MySqlDialect { true } + fn supports_select_modifiers(&self) -> bool { + true + } + fn supports_set_names(&self) -> bool { true } diff --git a/src/keywords.rs b/src/keywords.rs index 964e4b388..2e26bda8e 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -333,6 +333,7 @@ define_keywords!( DISCARD, DISCONNECT, DISTINCT, + DISTINCTROW, DISTRIBUTE, DIV, DO, @@ -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, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 882803a5a..87ab98541 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -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, 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", + self.peek_token(), + ); } let on = self.parse_keyword(Keyword::ON); if !on { @@ -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![], @@ -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()?); } @@ -14005,6 +14022,7 @@ impl<'a> Parser<'a> { Ok(Select { select_token: AttachedToken(select_token), distinct, + select_modifiers, top, top_before_distinct, projection, @@ -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. + fn parse_select_modifiers( + &mut self, + ) -> Result<(SelectModifiers, Option), ParserError> { + let mut modifiers = SelectModifiers::default(); + let mut distinct: Option = None; + let mut has_all = false; + + 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, + 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, ParserError> { if !dialect_of!(self is BigQueryDialect) { return Ok(None); diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index d8c3ada1d..fe5d90ebc 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -2682,6 +2682,7 @@ fn test_export_data() { Span::empty() )), distinct: None, + select_modifiers: SelectModifiers::default(), top: None, top_before_distinct: false, projection: vec![ @@ -2786,6 +2787,7 @@ fn test_export_data() { Span::empty() )), distinct: None, + select_modifiers: SelectModifiers::default(), top: None, top_before_distinct: false, projection: vec![ diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 44bfcda42..02923b973 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -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, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index f892bf7a9..a80f564ec 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -473,6 +473,7 @@ fn parse_update_set_from() { body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), distinct: None, + select_modifiers: SelectModifiers::default(), top: None, top_before_distinct: false, projection: vec![ @@ -1040,18 +1041,18 @@ fn parse_outer_join_operator() { #[test] fn parse_select_distinct_on() { let sql = "SELECT DISTINCT ON (album_id) name FROM track ORDER BY album_id, milliseconds"; - let select = verified_only_select(sql); + let select = all_dialects_but_mysql().verified_only_select(sql); assert_eq!( &Some(Distinct::On(vec![Expr::Identifier(Ident::new("album_id"))])), &select.distinct ); let sql = "SELECT DISTINCT ON () name FROM track ORDER BY milliseconds"; - let select = verified_only_select(sql); + let select = all_dialects_but_mysql().verified_only_select(sql); assert_eq!(&Some(Distinct::On(vec![])), &select.distinct); let sql = "SELECT DISTINCT ON (album_id, milliseconds) name FROM track"; - let select = verified_only_select(sql); + let select = all_dialects_but_mysql().verified_only_select(sql); assert_eq!( &Some(Distinct::On(vec![ Expr::Identifier(Ident::new("album_id")), @@ -1079,7 +1080,9 @@ fn parse_select_all() { fn parse_select_all_distinct() { let result = parse_sql_statements("SELECT ALL DISTINCT name FROM customer"); assert_eq!( - ParserError::ParserError("Cannot specify both ALL and DISTINCT".to_string()), + ParserError::ParserError( + "Expected: ALL alone without DISTINCT or DISTINCTROW, found: DISTINCT".to_string() + ), result.unwrap_err(), ); } @@ -2348,6 +2351,16 @@ pub fn all_dialects_but_pg() -> TestedDialects { ) } +pub fn all_dialects_but_mysql() -> TestedDialects { + TestedDialects::new( + all_dialects() + .dialects + .into_iter() + .filter(|x| !x.is::()) + .collect(), + ) +} + #[test] fn parse_bitwise_ops() { let bitwise_ops = &[ @@ -5795,6 +5808,7 @@ fn test_parse_named_window() { let expected = Select { select_token: AttachedToken::empty(), distinct: None, + select_modifiers: SelectModifiers::default(), top: None, top_before_distinct: false, projection: vec![ @@ -6524,6 +6538,7 @@ fn parse_interval_and_or_xor() { body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), distinct: None, + select_modifiers: SelectModifiers::default(), top: None, top_before_distinct: false, projection: vec![UnnamedExpr(Expr::Identifier(Ident { @@ -8898,6 +8913,7 @@ fn lateral_function() { let expected = Select { select_token: AttachedToken::empty(), distinct: None, + select_modifiers: SelectModifiers::default(), top: None, projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())], exclude: None, @@ -9898,6 +9914,7 @@ fn parse_merge() { body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), distinct: None, + select_modifiers: SelectModifiers::default(), top: None, top_before_distinct: false, projection: vec![SelectItem::Wildcard( @@ -12300,6 +12317,7 @@ fn parse_unload() { body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), distinct: None, + select_modifiers: SelectModifiers::default(), top: None, top_before_distinct: false, projection: vec![UnnamedExpr(Expr::Identifier(Ident::new("cola"))),], @@ -12608,6 +12626,7 @@ fn parse_connect_by() { let expect_query = Select { select_token: AttachedToken::empty(), distinct: None, + select_modifiers: SelectModifiers::default(), top: None, top_before_distinct: false, projection: vec![ @@ -12690,6 +12709,7 @@ fn parse_connect_by() { Select { select_token: AttachedToken::empty(), distinct: None, + select_modifiers: SelectModifiers::default(), top: None, top_before_distinct: false, projection: vec![ @@ -13620,6 +13640,7 @@ fn test_extract_seconds_ok() { body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), distinct: None, + select_modifiers: SelectModifiers::default(), top: None, top_before_distinct: false, projection: vec![UnnamedExpr(Expr::Extract { @@ -15757,6 +15778,7 @@ fn test_select_from_first() { body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), distinct: None, + select_modifiers: SelectModifiers::default(), top: None, projection, exclude: None, diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 80a15eb11..82ff833d5 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -267,6 +267,7 @@ fn test_select_union_by_name() { left: Box::::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), distinct: None, + select_modifiers: SelectModifiers::default(), top: None, projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())], exclude: None, @@ -298,6 +299,7 @@ fn test_select_union_by_name() { right: Box::::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), distinct: None, + select_modifiers: SelectModifiers::default(), top: None, projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())], exclude: None, diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 1927b864e..e751933c1 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -142,6 +142,7 @@ fn parse_create_procedure() { body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), distinct: None, + select_modifiers: SelectModifiers::default(), top: None, top_before_distinct: false, projection: vec![SelectItem::UnnamedExpr(Expr::Value( @@ -1349,6 +1350,7 @@ fn parse_substring_in_select() { body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), distinct: Some(Distinct::Distinct), + select_modifiers: SelectModifiers::default(), top: None, top_before_distinct: false, projection: vec![SelectItem::UnnamedExpr(Expr::Substring { @@ -1506,6 +1508,7 @@ fn parse_mssql_declare() { body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), distinct: None, + select_modifiers: SelectModifiers::default(), top: None, top_before_distinct: false, projection: vec![SelectItem::UnnamedExpr(Expr::BinaryOp { diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index e847d3edb..438a1ce6c 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1417,6 +1417,7 @@ fn parse_escaped_quote_identifiers_with_escape() { body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), distinct: None, + select_modifiers: SelectModifiers::default(), top: None, top_before_distinct: false, projection: vec![SelectItem::UnnamedExpr(Expr::Identifier(Ident { @@ -1472,6 +1473,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() { body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), distinct: None, + select_modifiers: SelectModifiers::default(), top: None, top_before_distinct: false, projection: vec![SelectItem::UnnamedExpr(Expr::Identifier(Ident { @@ -1520,6 +1522,7 @@ fn parse_escaped_backticks_with_escape() { select_token: AttachedToken::empty(), distinct: None, + select_modifiers: SelectModifiers::default(), top: None, top_before_distinct: false, projection: vec![SelectItem::UnnamedExpr(Expr::Identifier(Ident { @@ -1572,6 +1575,7 @@ fn parse_escaped_backticks_with_no_escape() { select_token: AttachedToken::empty(), distinct: None, + select_modifiers: SelectModifiers::default(), top: None, top_before_distinct: false, projection: vec![SelectItem::UnnamedExpr(Expr::Identifier(Ident { @@ -2392,6 +2396,7 @@ fn parse_select_with_numeric_prefix_column_name() { select_token: AttachedToken::empty(), distinct: None, + select_modifiers: SelectModifiers::default(), top: None, top_before_distinct: false, projection: vec![SelectItem::UnnamedExpr(Expr::Identifier(Ident::new( @@ -2566,6 +2571,7 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), distinct: None, + select_modifiers: SelectModifiers::default(), top: None, top_before_distinct: false, projection: vec![ @@ -3198,6 +3204,7 @@ fn parse_substring_in_select() { body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), distinct: Some(Distinct::Distinct), + select_modifiers: SelectModifiers::default(), top: None, top_before_distinct: false, projection: vec![SelectItem::UnnamedExpr(Expr::Substring { @@ -3521,6 +3528,7 @@ fn parse_hex_string_introducer() { body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), distinct: None, + select_modifiers: SelectModifiers::default(), top: None, top_before_distinct: false, projection: vec![SelectItem::UnnamedExpr(Expr::Prefixed { @@ -4239,6 +4247,189 @@ fn parse_straight_join() { .verified_stmt("SELECT a.*, b.* FROM table_a STRAIGHT_JOIN table_b AS b ON a.b_id = b.id"); } +#[test] +fn parse_select_straight_join() { + let select = mysql().verified_only_select( + "SELECT STRAIGHT_JOIN * FROM employees e JOIN dept_emp d ON e.emp_no = d.emp_no WHERE d.emp_no = 10001", + ); + assert!(select.select_modifiers.straight_join); + + mysql().verified_stmt( + "SELECT STRAIGHT_JOIN e.emp_no, d.dept_no FROM employees e JOIN dept_emp d ON e.emp_no = d.emp_no", + ); + mysql().verified_stmt("SELECT STRAIGHT_JOIN DISTINCT emp_no FROM employees"); + + let select = mysql().verified_only_select("SELECT * FROM employees"); + assert!(!select.select_modifiers.straight_join); +} + +#[test] +fn parse_select_modifiers() { + let select = mysql().verified_only_select("SELECT HIGH_PRIORITY * FROM employees"); + assert!(select.select_modifiers.high_priority); + assert!(!select.select_modifiers.straight_join); + + let select = mysql().verified_only_select("SELECT SQL_SMALL_RESULT * FROM employees"); + assert!(select.select_modifiers.sql_small_result); + + let select = mysql().verified_only_select("SELECT SQL_BIG_RESULT * FROM employees"); + assert!(select.select_modifiers.sql_big_result); + + let select = mysql().verified_only_select("SELECT SQL_BUFFER_RESULT * FROM employees"); + assert!(select.select_modifiers.sql_buffer_result); + + let select = mysql().verified_only_select("SELECT SQL_NO_CACHE * FROM employees"); + assert!(select.select_modifiers.sql_no_cache); + + let select = mysql().verified_only_select("SELECT SQL_CALC_FOUND_ROWS * FROM employees"); + assert!(select.select_modifiers.sql_calc_found_rows); + + let select = mysql().verified_only_select( + "SELECT HIGH_PRIORITY STRAIGHT_JOIN SQL_SMALL_RESULT SQL_BIG_RESULT SQL_BUFFER_RESULT SQL_NO_CACHE SQL_CALC_FOUND_ROWS * FROM employees", + ); + assert!(select.select_modifiers.high_priority); + assert!(select.select_modifiers.straight_join); + assert!(select.select_modifiers.sql_small_result); + assert!(select.select_modifiers.sql_big_result); + assert!(select.select_modifiers.sql_buffer_result); + assert!(select.select_modifiers.sql_no_cache); + assert!(select.select_modifiers.sql_calc_found_rows); + + mysql().verified_stmt("SELECT HIGH_PRIORITY DISTINCT emp_no FROM employees"); + mysql().verified_stmt("SELECT SQL_CALC_FOUND_ROWS DISTINCT emp_no FROM employees"); + mysql().verified_stmt("SELECT HIGH_PRIORITY STRAIGHT_JOIN e.emp_no, d.dept_no FROM employees e JOIN dept_emp d ON e.emp_no = d.emp_no"); +} + +#[test] +fn parse_select_modifiers_any_order() { + mysql().one_statement_parses_to( + "SELECT DISTINCT HIGH_PRIORITY * FROM employees", + "SELECT HIGH_PRIORITY DISTINCT * FROM employees", + ); + mysql().one_statement_parses_to( + "SELECT SQL_CALC_FOUND_ROWS DISTINCT HIGH_PRIORITY * FROM employees", + "SELECT HIGH_PRIORITY SQL_CALC_FOUND_ROWS DISTINCT * FROM employees", + ); + mysql().one_statement_parses_to( + "SELECT HIGH_PRIORITY SQL_SMALL_RESULT DISTINCT * FROM employees", + "SELECT HIGH_PRIORITY SQL_SMALL_RESULT DISTINCT * FROM employees", + ); + + mysql().one_statement_parses_to( + "SELECT DISTINCTROW * FROM employees", + "SELECT DISTINCT * FROM employees", + ); + mysql().one_statement_parses_to( + "SELECT HIGH_PRIORITY DISTINCTROW * FROM employees", + "SELECT HIGH_PRIORITY DISTINCT * FROM employees", + ); + mysql().one_statement_parses_to( + "SELECT DISTINCTROW HIGH_PRIORITY * FROM employees", + "SELECT HIGH_PRIORITY DISTINCT * FROM employees", + ); + + mysql().one_statement_parses_to("SELECT ALL * FROM employees", "SELECT * FROM employees"); + mysql().one_statement_parses_to( + "SELECT ALL HIGH_PRIORITY * FROM employees", + "SELECT HIGH_PRIORITY * FROM employees", + ); + mysql().one_statement_parses_to( + "SELECT HIGH_PRIORITY ALL * FROM employees", + "SELECT HIGH_PRIORITY * FROM employees", + ); + + mysql().one_statement_parses_to( + "SELECT DISTINCT HIGH_PRIORITY * FROM employees", + "SELECT HIGH_PRIORITY DISTINCT * FROM employees", + ); + let select = mysql().verified_only_select("SELECT HIGH_PRIORITY DISTINCT * FROM employees"); + assert!(select.select_modifiers.high_priority); + assert!(matches!(select.distinct, Some(Distinct::Distinct))); + + mysql().one_statement_parses_to( + "SELECT SQL_CALC_FOUND_ROWS ALL HIGH_PRIORITY * FROM employees", + "SELECT HIGH_PRIORITY SQL_CALC_FOUND_ROWS * FROM employees", + ); + let select = + mysql().verified_only_select("SELECT HIGH_PRIORITY SQL_CALC_FOUND_ROWS * FROM employees"); + assert!(select.select_modifiers.sql_calc_found_rows); + assert!(select.select_modifiers.high_priority); + assert!(select.distinct.is_none()); +} + +#[test] +fn parse_select_modifiers_duplicate() { + mysql().one_statement_parses_to( + "SELECT HIGH_PRIORITY HIGH_PRIORITY * FROM employees", + "SELECT HIGH_PRIORITY * FROM employees", + ); + mysql().one_statement_parses_to( + "SELECT SQL_CALC_FOUND_ROWS SQL_CALC_FOUND_ROWS * FROM employees", + "SELECT SQL_CALC_FOUND_ROWS * FROM employees", + ); + mysql().one_statement_parses_to( + "SELECT STRAIGHT_JOIN STRAIGHT_JOIN * FROM employees", + "SELECT STRAIGHT_JOIN * FROM employees", + ); + mysql().one_statement_parses_to( + "SELECT SQL_NO_CACHE SQL_NO_CACHE * FROM employees", + "SELECT SQL_NO_CACHE * FROM employees", + ); + mysql().one_statement_parses_to( + "SELECT HIGH_PRIORITY DISTINCT HIGH_PRIORITY * FROM employees", + "SELECT HIGH_PRIORITY DISTINCT * FROM employees", + ); + mysql().one_statement_parses_to( + "SELECT SQL_CALC_FOUND_ROWS DISTINCT SQL_CALC_FOUND_ROWS * FROM employees", + "SELECT SQL_CALC_FOUND_ROWS DISTINCT * FROM employees", + ); +} + +#[test] +fn parse_select_modifiers_ordering() { + mysql().one_statement_parses_to( + "SELECT SQL_CALC_FOUND_ROWS SQL_NO_CACHE SQL_BUFFER_RESULT SQL_BIG_RESULT SQL_SMALL_RESULT STRAIGHT_JOIN HIGH_PRIORITY * FROM employees", + "SELECT HIGH_PRIORITY STRAIGHT_JOIN SQL_SMALL_RESULT SQL_BIG_RESULT SQL_BUFFER_RESULT SQL_NO_CACHE SQL_CALC_FOUND_ROWS * FROM employees", + ); + mysql().one_statement_parses_to( + "SELECT SQL_NO_CACHE DISTINCT SQL_CALC_FOUND_ROWS * FROM employees", + "SELECT SQL_NO_CACHE SQL_CALC_FOUND_ROWS DISTINCT * FROM employees", + ); + mysql().one_statement_parses_to( + "SELECT HIGH_PRIORITY STRAIGHT_JOIN DISTINCT SQL_SMALL_RESULT * FROM employees", + "SELECT HIGH_PRIORITY STRAIGHT_JOIN SQL_SMALL_RESULT DISTINCT * FROM employees", + ); + mysql().one_statement_parses_to( + "SELECT HIGH_PRIORITY ALL STRAIGHT_JOIN * FROM employees", + "SELECT HIGH_PRIORITY STRAIGHT_JOIN * FROM employees", + ); +} + +#[test] +fn parse_select_modifiers_errors() { + assert!(mysql() + .parse_sql_statements("SELECT DISTINCT DISTINCT * FROM t") + .is_err()); + assert!(mysql() + .parse_sql_statements("SELECT DISTINCTROW DISTINCTROW * FROM t") + .is_err()); + assert!(mysql() + .parse_sql_statements("SELECT DISTINCT DISTINCTROW * FROM t") + .is_err()); + assert!(mysql() + .parse_sql_statements("SELECT ALL DISTINCT * FROM t") + .is_err()); + assert!(mysql() + .parse_sql_statements("SELECT DISTINCT ALL * FROM t") + .is_err()); + assert!(mysql() + .parse_sql_statements("SELECT ALL DISTINCTROW * FROM t") + .is_err()); + assert!(mysql() + .parse_sql_statements("SELECT ALL ALL * FROM t") + .is_err()); +} + #[test] fn mysql_foreign_key_with_index_name() { mysql().verified_stmt( diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 57bddc656..36d07b438 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1283,6 +1283,7 @@ fn parse_copy_to() { body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), distinct: None, + select_modifiers: SelectModifiers::default(), top: None, top_before_distinct: false, projection: vec![ @@ -3060,6 +3061,7 @@ fn parse_array_subquery_expr() { left: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), distinct: None, + select_modifiers: SelectModifiers::default(), top: None, top_before_distinct: false, projection: vec![SelectItem::UnnamedExpr(Expr::Value( @@ -3086,6 +3088,7 @@ fn parse_array_subquery_expr() { right: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), distinct: None, + select_modifiers: SelectModifiers::default(), top: None, top_before_distinct: false, projection: vec![SelectItem::UnnamedExpr(Expr::Value(