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(