diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 32c023e05..4c36f7059 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -32,8 +32,8 @@ use crate::{ use super::{ display_comma_separated, helpers::attached_token::AttachedToken, query::InputFormatClause, Assignment, Expr, FromTable, Ident, InsertAliases, MysqlInsertPriority, ObjectName, OnInsert, - OrderByExpr, Query, SelectInto, SelectItem, Setting, SqliteOnConflict, TableFactor, - TableObject, TableWithJoins, UpdateTableFromKind, Values, + OptimizerHint, OrderByExpr, Query, SelectInto, SelectItem, Setting, SqliteOnConflict, + TableFactor, TableObject, TableWithJoins, UpdateTableFromKind, Values, }; /// INSERT statement. @@ -43,6 +43,11 @@ use super::{ pub struct Insert { /// Token for the `INSERT` keyword (or its substitutes) pub insert_token: AttachedToken, + /// A query optimizer hint + /// + /// [MySQL](https://dev.mysql.com/doc/refman/8.4/en/optimizer-hints.html) + /// [Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Comments.html#GUID-D316D545-89E2-4D54-977F-FC97815CD62E) + pub optimizer_hint: Option, /// Only for Sqlite pub or: Option, /// Only for mysql @@ -102,7 +107,11 @@ impl Display for Insert { }; if let Some(on_conflict) = self.or { - write!(f, "INSERT {on_conflict} INTO {table_name} ")?; + f.write_str("INSERT")?; + if let Some(hint) = self.optimizer_hint.as_ref() { + write!(f, " {hint}")?; + } + write!(f, " {on_conflict} INTO {table_name} ")?; } else { write!( f, @@ -111,8 +120,11 @@ impl Display for Insert { "REPLACE" } else { "INSERT" - }, + } )?; + if let Some(hint) = self.optimizer_hint.as_ref() { + write!(f, " {hint}")?; + } if let Some(priority) = self.priority { write!(f, " {priority}",)?; } @@ -188,6 +200,11 @@ impl Display for Insert { pub struct Delete { /// Token for the `DELETE` keyword pub delete_token: AttachedToken, + /// A query optimizer hint + /// + /// [MySQL](https://dev.mysql.com/doc/refman/8.4/en/optimizer-hints.html) + /// [Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Comments.html#GUID-D316D545-89E2-4D54-977F-FC97815CD62E) + pub optimizer_hint: Option, /// Multi tables delete are supported in mysql pub tables: Vec, /// FROM @@ -207,6 +224,10 @@ pub struct Delete { impl Display for Delete { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("DELETE")?; + if let Some(hint) = self.optimizer_hint.as_ref() { + f.write_str(" ")?; + hint.fmt(f)?; + } if !self.tables.is_empty() { indented_list(f, &self.tables)?; } @@ -257,6 +278,11 @@ impl Display for Delete { pub struct Update { /// Token for the `UPDATE` keyword pub update_token: AttachedToken, + /// A query optimizer hint + /// + /// [MySQL](https://dev.mysql.com/doc/refman/8.4/en/optimizer-hints.html) + /// [Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Comments.html#GUID-D316D545-89E2-4D54-977F-FC97815CD62E) + pub optimizer_hint: Option, /// TABLE pub table: TableWithJoins, /// Column assignments @@ -276,6 +302,10 @@ pub struct Update { impl Display for Update { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("UPDATE ")?; + if let Some(hint) = self.optimizer_hint.as_ref() { + hint.fmt(f)?; + f.write_str(" ")?; + } if let Some(or) = &self.or { or.fmt(f)?; f.write_str(" ")?; @@ -322,6 +352,10 @@ impl Display for Update { pub struct Merge { /// The `MERGE` token that starts the statement. pub merge_token: AttachedToken, + /// A query optimizer hint + /// + /// [Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Comments.html#GUID-D316D545-89E2-4D54-977F-FC97815CD62E) + pub optimizer_hint: Option, /// optional INTO keyword pub into: bool, /// Specifies the table to merge @@ -338,12 +372,18 @@ pub struct Merge { impl Display for Merge { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("MERGE")?; + if let Some(hint) = self.optimizer_hint.as_ref() { + write!(f, " {hint}")?; + } + if self.into { + write!(f, " INTO")?; + } write!( f, - "MERGE{int} {table} USING {source} ", - int = if self.into { " INTO" } else { "" }, + " {table} USING {source} ", table = self.table, - source = self.source, + source = self.source )?; write!(f, "ON {on} ", on = self.on)?; write!(f, "{}", display_separated(&self.clauses, " "))?; diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 35a62ab76..69693451e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -11666,6 +11666,57 @@ pub struct ResetStatement { pub reset: Reset, } +/// Query optimizer hints are optionally supported comments after the +/// `SELECT`, `INSERT`, `UPDATE`, `REPLACE`, `MERGE`, and `DELETE` keywords in +/// the corresponding statements. +/// +/// See [Select::optimizer_hint] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct OptimizerHint { + /// the raw test of the optimizer hint without its markers + pub text: String, + /// the style of the comment which `text` was extracted from, + /// e.g. `/*+...*/` or `--+...` + /// + /// Not all dialects support all styles, though. + pub style: OptimizerHintStyle, +} + +/// The commentary style of an [optimizer hint](OptimizerHint) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum OptimizerHintStyle { + /// A hint corresponding to a single line comment, + /// e.g. `--+ LEADING(v.e v.d t)` + SingleLine { + /// the comment prefix, e.g. `--` + prefix: String, + }, + /// A hint corresponding to a multi line comment, + /// e.g. `/*+ LEADING(v.e v.d t) */` + MultiLine, +} + +impl fmt::Display for OptimizerHint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.style { + OptimizerHintStyle::SingleLine { prefix } => { + f.write_str(prefix)?; + f.write_str("+")?; + f.write_str(&self.text) + } + OptimizerHintStyle::MultiLine => { + f.write_str("/*+")?; + f.write_str(&self.text)?; + f.write_str("*/") + } + } + } +} + impl fmt::Display for ResetStatement { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self.reset { diff --git a/src/ast/query.rs b/src/ast/query.rs index d8af243f7..6d4d28dbf 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -343,6 +343,11 @@ pub enum SelectFlavor { pub struct Select { /// Token for the `SELECT` keyword pub select_token: AttachedToken, + /// A query optimizer hint + /// + /// [MySQL](https://dev.mysql.com/doc/refman/8.4/en/optimizer-hints.html) + /// [Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Comments.html#GUID-D316D545-89E2-4D54-977F-FC97815CD62E) + pub optimizer_hint: Option, /// `SELECT [DISTINCT] ...` pub distinct: Option, /// MSSQL syntax: `TOP () [ PERCENT ] [ WITH TIES ]` @@ -410,6 +415,11 @@ impl fmt::Display for Select { } } + if let Some(hint) = self.optimizer_hint.as_ref() { + f.write_str(" ")?; + hint.fmt(f)?; + } + if let Some(value_table_mode) = self.value_table_mode { f.write_str(" ")?; value_table_mode.fmt(f)?; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 488c88624..c83db8155 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -894,6 +894,7 @@ impl Spanned for Delete { fn span(&self) -> Span { let Delete { delete_token, + optimizer_hint: _, tables, from, using, @@ -927,6 +928,7 @@ impl Spanned for Update { fn span(&self) -> Span { let Update { update_token, + optimizer_hint: _, table, assignments, from, @@ -1288,6 +1290,7 @@ impl Spanned for Insert { fn span(&self) -> Span { let Insert { insert_token, + optimizer_hint: _, or: _, // enum, sqlite specific ignore: _, // bool into: _, // bool @@ -2229,6 +2232,7 @@ impl Spanned for Select { fn span(&self) -> Span { let Select { select_token, + optimizer_hint: _, distinct: _, // todo top: _, // todo, mysql specific projection, @@ -2815,6 +2819,7 @@ WHERE id = 1 // ~ individual tokens within the statement let Statement::Merge(Merge { merge_token, + optimizer_hint: _, into: _, table: _, source: _, diff --git a/src/parser/merge.rs b/src/parser/merge.rs index 81798c456..723567552 100644 --- a/src/parser/merge.rs +++ b/src/parser/merge.rs @@ -41,6 +41,8 @@ impl Parser<'_> { /// Parse a `MERGE` statement pub fn parse_merge(&mut self, merge_token: TokenWithSpan) -> Result { + let optimizer_hint = self.parse_optional_optimizer_hint()?; + let into = self.parse_keyword(Keyword::INTO); let table = self.parse_table_factor()?; @@ -57,6 +59,7 @@ impl Parser<'_> { Ok(Statement::Merge(Merge { merge_token: merge_token.into(), + optimizer_hint, into, table, source, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 64b653910..b2c132b50 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4283,6 +4283,11 @@ impl<'a> Parser<'a> { }) } + /// Return nth token, possibly whitespace, that has not yet been processed. + fn peek_nth_token_no_skip_ref(&self, n: usize) -> &TokenWithSpan { + self.tokens.get(self.index + n).unwrap_or(&EOF_TOKEN) + } + /// Return true if the next tokens exactly `expected` /// /// Does not advance the current token. @@ -12969,6 +12974,7 @@ impl<'a> Parser<'a> { /// Parse a `DELETE` statement and return `Statement::Delete`. pub fn parse_delete(&mut self, delete_token: TokenWithSpan) -> Result { + let optimizer_hint = self.parse_optional_optimizer_hint()?; let (tables, with_from_keyword) = if !self.parse_keyword(Keyword::FROM) { // `FROM` keyword is optional in BigQuery SQL. // https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#delete_statement @@ -13012,6 +13018,7 @@ impl<'a> Parser<'a> { Ok(Statement::Delete(Delete { delete_token: delete_token.into(), + optimizer_hint, tables, from: if with_from_keyword { FromTable::WithFromKeyword(from) @@ -13784,6 +13791,7 @@ impl<'a> Parser<'a> { if !self.peek_keyword(Keyword::SELECT) { return Ok(Select { select_token: AttachedToken(from_token), + optimizer_hint: None, distinct: None, top: None, top_before_distinct: false, @@ -13811,6 +13819,7 @@ impl<'a> Parser<'a> { } let select_token = self.expect_keyword(Keyword::SELECT)?; + let optimizer_hint = self.parse_optional_optimizer_hint()?; let value_table_mode = self.parse_value_table_mode()?; let mut top_before_distinct = false; @@ -13966,6 +13975,7 @@ impl<'a> Parser<'a> { Ok(Select { select_token: AttachedToken(select_token), + optimizer_hint, distinct, top, top_before_distinct, @@ -13994,6 +14004,59 @@ impl<'a> Parser<'a> { }) } + /// Parses an optional optimizer hint at the current token position + /// + /// [MySQL](https://dev.mysql.com/doc/refman/8.4/en/optimizer-hints.html#optimizer-hints-overview) + /// [Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Comments.html#GUID-D316D545-89E2-4D54-977F-FC97815CD62E) + fn parse_optional_optimizer_hint(&mut self) -> Result, ParserError> { + let supports_multiline = dialect_of!(self is MySqlDialect | OracleDialect | GenericDialect); + let supports_singleline = dialect_of!(self is OracleDialect | GenericDialect); + if !supports_multiline && !supports_singleline { + return Ok(None); + } + loop { + let t = self.peek_nth_token_no_skip_ref(0); + match &t.token { + // ~ only the very first comment + Token::Whitespace(ws) => { + match ws { + Whitespace::SingleLineComment { comment, prefix } => { + return Ok(if supports_singleline && comment.starts_with("+") { + let text = comment.split_at(1).1.into(); + let prefix = prefix.clone(); + self.next_token_no_skip(); // ~ consume the token + Some(OptimizerHint { + text, + style: OptimizerHintStyle::SingleLine { prefix }, + }) + } else { + None + }); + } + Whitespace::MultiLineComment(comment) => { + return Ok(if supports_multiline && comment.starts_with("+") { + let text = comment.split_at(1).1.into(); + self.next_token_no_skip(); // ~ consume the token + Some(OptimizerHint { + text, + style: OptimizerHintStyle::MultiLine, + }) + } else { + None + }); + } + // ~ but skip (pure) whitespace + Whitespace::Space | Whitespace::Tab | Whitespace::Newline => { + // ~ consume the token and try with the next whitespace (if any) + self.next_token_no_skip(); + } + } + } + _ => return Ok(None), + } + } + } + fn parse_value_table_mode(&mut self) -> Result, ParserError> { if !dialect_of!(self is BigQueryDialect) { return Ok(None); @@ -16677,6 +16740,7 @@ impl<'a> Parser<'a> { /// Parse an INSERT statement pub fn parse_insert(&mut self, insert_token: TokenWithSpan) -> Result { + let optimizer_hint = self.parse_optional_optimizer_hint()?; let or = self.parse_conflict_clause(); let priority = if !dialect_of!(self is MySqlDialect | GenericDialect) { None @@ -16846,6 +16910,7 @@ impl<'a> Parser<'a> { Ok(Statement::Insert(Insert { insert_token: insert_token.into(), + optimizer_hint, or, table: table_object, table_alias, @@ -16948,6 +17013,7 @@ impl<'a> Parser<'a> { /// Parse an `UPDATE` statement and return `Statement::Update`. pub fn parse_update(&mut self, update_token: TokenWithSpan) -> Result { + let optimizer_hint = self.parse_optional_optimizer_hint()?; let or = self.parse_conflict_clause(); let table = self.parse_table_and_joins()?; let from_before_set = if self.parse_keyword(Keyword::FROM) { @@ -16983,6 +17049,7 @@ impl<'a> Parser<'a> { }; Ok(Update { update_token: update_token.into(), + optimizer_hint, table, assignments, from, diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index d8c3ada1d..fb28b4d21 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -2681,6 +2681,7 @@ fn test_export_data() { }), Span::empty() )), + optimizer_hint: None, distinct: None, top: None, top_before_distinct: false, @@ -2785,6 +2786,7 @@ fn test_export_data() { }), Span::empty() )), + optimizer_hint: None, distinct: None, top: None, top_before_distinct: false, diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 44bfcda42..ac31a2783 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, + optimizer_hint: None, select_token: AttachedToken::empty(), top: None, top_before_distinct: false, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index c7a1981e9..6010fcf7d 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -457,6 +457,7 @@ fn parse_update_set_from() { stmt, Statement::Update(Update { update_token: AttachedToken::empty(), + optimizer_hint: None, table: TableWithJoins { relation: table_from_name(ObjectName::from(vec![Ident::new("t1")])), joins: vec![], @@ -472,6 +473,7 @@ fn parse_update_set_from() { with: None, body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), + optimizer_hint: None, distinct: None, top: None, top_before_distinct: false, @@ -549,6 +551,7 @@ fn parse_update_with_table_alias() { returning, or: None, limit: None, + optimizer_hint: None, update_token: _, }) => { assert_eq!( @@ -5794,6 +5797,7 @@ fn test_parse_named_window() { let actual_select_only = dialects.verified_only_select(sql); let expected = Select { select_token: AttachedToken::empty(), + optimizer_hint: None, distinct: None, top: None, top_before_distinct: false, @@ -6523,6 +6527,7 @@ fn parse_interval_and_or_xor() { with: None, body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), + optimizer_hint: None, distinct: None, top: None, top_before_distinct: false, @@ -8897,6 +8902,7 @@ fn lateral_function() { let actual_select_only = verified_only_select(sql); let expected = Select { select_token: AttachedToken::empty(), + optimizer_hint: None, distinct: None, top: None, projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())], @@ -9897,6 +9903,7 @@ fn parse_merge() { with: None, body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), + optimizer_hint: None, distinct: None, top: None, top_before_distinct: false, @@ -12299,6 +12306,7 @@ fn parse_unload() { query: Some(Box::new(Query { body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), + optimizer_hint: None, distinct: None, top: None, top_before_distinct: false, @@ -12607,6 +12615,7 @@ fn parse_map_access_expr() { fn parse_connect_by() { let expect_query = Select { select_token: AttachedToken::empty(), + optimizer_hint: None, distinct: None, top: None, top_before_distinct: false, @@ -12689,6 +12698,7 @@ fn parse_connect_by() { all_dialects_where(|d| d.supports_connect_by()).verified_only_select(connect_by_3), Select { select_token: AttachedToken::empty(), + optimizer_hint: None, distinct: None, top: None, top_before_distinct: false, @@ -13619,6 +13629,7 @@ fn test_extract_seconds_ok() { with: None, body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), + optimizer_hint: None, distinct: None, top: None, top_before_distinct: false, @@ -15711,6 +15722,7 @@ fn test_select_from_first() { with: None, body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), + optimizer_hint: None, distinct: None, top: None, projection, diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 80a15eb11..93ef74ff8 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -266,6 +266,7 @@ fn test_select_union_by_name() { set_quantifier: *expected_quantifier, left: Box::::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), + optimizer_hint: None, distinct: None, top: None, projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())], @@ -297,6 +298,7 @@ fn test_select_union_by_name() { }))), right: Box::::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), + optimizer_hint: None, distinct: None, top: None, projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())], diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 1927b864e..7ef4ce85c 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -141,6 +141,7 @@ fn parse_create_procedure() { pipe_operators: vec![], body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), + optimizer_hint: None, distinct: None, top: None, top_before_distinct: false, @@ -1348,6 +1349,7 @@ fn parse_substring_in_select() { body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), + optimizer_hint: None, distinct: Some(Distinct::Distinct), top: None, top_before_distinct: false, @@ -1505,6 +1507,7 @@ fn parse_mssql_declare() { body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), + optimizer_hint: None, distinct: None, top: None, top_before_distinct: false, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index e847d3edb..a7f2c96fd 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1416,6 +1416,7 @@ fn parse_escaped_quote_identifiers_with_escape() { with: None, body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), + optimizer_hint: None, distinct: None, top: None, top_before_distinct: false, @@ -1471,6 +1472,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() { with: None, body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), + optimizer_hint: None, distinct: None, top: None, top_before_distinct: false, @@ -1518,7 +1520,7 @@ fn parse_escaped_backticks_with_escape() { with: None, body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), - + optimizer_hint: None, distinct: None, top: None, top_before_distinct: false, @@ -1570,7 +1572,7 @@ fn parse_escaped_backticks_with_no_escape() { with: None, body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), - + optimizer_hint: None, distinct: None, top: None, top_before_distinct: false, @@ -2390,7 +2392,7 @@ fn parse_select_with_numeric_prefix_column_name() { q.body, Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), - + optimizer_hint: None, distinct: None, top: None, top_before_distinct: false, @@ -2565,6 +2567,7 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { q.body, Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), + optimizer_hint: None, distinct: None, top: None, top_before_distinct: false, @@ -2632,6 +2635,7 @@ fn parse_update_with_joins() { returning, or: None, limit: None, + optimizer_hint: None, update_token: _, }) => { assert_eq!( @@ -3197,6 +3201,7 @@ fn parse_substring_in_select() { with: None, body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), + optimizer_hint: None, distinct: Some(Distinct::Distinct), top: None, top_before_distinct: false, @@ -3520,6 +3525,7 @@ fn parse_hex_string_introducer() { with: None, body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), + optimizer_hint: None, distinct: None, top: None, top_before_distinct: false, @@ -4354,3 +4360,48 @@ fn test_create_index_options() { "CREATE INDEX idx_name ON t(c1, c2) USING BTREE LOCK = EXCLUSIVE ALGORITHM = DEFAULT", ); } + +#[test] +fn test_optimizer_hints() { + let mysql_dialect = mysql_and_generic(); + + // ~ selects + mysql_dialect.verified_stmt( + "\ + SELECT /*+ SET_VAR(optimizer_switch = 'mrr_cost_based=off') \ + SET_VAR(max_heap_table_size = 1G) */ 1", + ); + + mysql_dialect.verified_stmt( + "\ + SELECT /*+ SET_VAR(target_partitions=1) */ * FROM \ + (SELECT /*+ SET_VAR(target_partitions=8) */ * FROM t1 LIMIT 1) AS dt", + ); + + // ~ inserts / replace + mysql_dialect.verified_stmt( + "\ + INSERT /*+ RESOURCE_GROUP(Batch) */ \ + INTO t2 VALUES (2)", + ); + + mysql_dialect.verified_stmt( + "\ + REPLACE /*+ foobar */ INTO test \ + VALUES (1, 'Old', '2014-08-20 18:47:00')", + ); + + // ~ updates + mysql_dialect.verified_stmt( + "\ + UPDATE /*+ quux */ table_name \ + SET column1 = 1 \ + WHERE 1 = 1", + ); + + // ~ deletes + mysql_dialect.verified_stmt( + "\ + DELETE /*+ foobar */ FROM table_name", + ); +} diff --git a/tests/sqlparser_oracle.rs b/tests/sqlparser_oracle.rs index 683660369..1c12f868f 100644 --- a/tests/sqlparser_oracle.rs +++ b/tests/sqlparser_oracle.rs @@ -333,3 +333,58 @@ fn parse_national_quote_delimited_string_but_is_a_word() { expr_from_projection(&select.projection[2]) ); } + +#[test] +fn test_optimizer_hints() { + let oracle_dialect = oracle(); + + // ~ selects + let select = oracle_dialect.verified_only_select_with_canonical( + "SELECT /*+one two three*/ /*+not a hint!*/ 1 FROM dual", + "SELECT /*+one two three*/ 1 FROM dual", + ); + assert_eq!( + select + .optimizer_hint + .as_ref() + .map(|hint| hint.text.as_str()), + Some("one two three") + ); + + let select = oracle_dialect.verified_only_select_with_canonical( + "SELECT /*one two three*/ /*+not a hint!*/ 1 FROM dual", + "SELECT 1 FROM dual", + ); + assert_eq!(select.optimizer_hint, None); + + let select = oracle_dialect.verified_only_select_with_canonical( + "SELECT --+ one two three /* asdf */\n 1 FROM dual", + "SELECT --+ one two three /* asdf */\n 1 FROM dual", + ); + assert_eq!( + select + .optimizer_hint + .as_ref() + .map(|hint| hint.text.as_str()), + Some(" one two three /* asdf */\n") + ); + + // ~ inserts + oracle_dialect.verified_stmt("INSERT /*+ append */ INTO t1 SELECT * FROM all_objects"); + + // ~ updates + oracle_dialect.verified_stmt("UPDATE /*+ DISABLE_PARALLEL_DML */ table_name SET column1 = 1"); + + // ~ deletes + oracle_dialect.verified_stmt("DELETE --+ ENABLE_PARALLEL_DML\n FROM table_name"); + + // ~ merges + oracle_dialect.verified_stmt( + "MERGE /*+ CLUSTERING */ INTO people_target pt \ + USING people_source ps \ + ON (pt.person_id = ps.person_id) \ + WHEN NOT MATCHED THEN INSERT \ + (pt.person_id, pt.first_name, pt.last_name, pt.title) \ + VALUES (ps.person_id, ps.first_name, ps.last_name, ps.title)", + ); +} diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 325e3939e..e80bd3102 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1282,6 +1282,7 @@ fn parse_copy_to() { with: None, body: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), + optimizer_hint: None, distinct: None, top: None, top_before_distinct: false, @@ -3059,6 +3060,7 @@ fn parse_array_subquery_expr() { set_quantifier: SetQuantifier::None, left: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), + optimizer_hint: None, distinct: None, top: None, top_before_distinct: false, @@ -3085,6 +3087,7 @@ fn parse_array_subquery_expr() { }))), right: Box::new(SetExpr::Select(Box::new(Select { select_token: AttachedToken::empty(), + optimizer_hint: None, distinct: None, top: None, top_before_distinct: false, @@ -5314,6 +5317,7 @@ fn test_simple_postgres_insert_with_alias() { statement, Statement::Insert(Insert { insert_token: AttachedToken::empty(), + optimizer_hint: None, or: None, ignore: false, into: true, @@ -5385,6 +5389,7 @@ fn test_simple_postgres_insert_with_alias() { statement, Statement::Insert(Insert { insert_token: AttachedToken::empty(), + optimizer_hint: None, or: None, ignore: false, into: true, @@ -5458,6 +5463,7 @@ fn test_simple_insert_with_quoted_alias() { statement, Statement::Insert(Insert { insert_token: AttachedToken::empty(), + optimizer_hint: None, or: None, ignore: false, into: true, diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 321cfef07..da311ac06 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -477,6 +477,7 @@ fn parse_update_tuple_row_values() { assert_eq!( sqlite().verified_stmt("UPDATE x SET (a, b) = (1, 2)"), Statement::Update(Update { + optimizer_hint: None, or: None, assignments: vec![Assignment { target: AssignmentTarget::Tuple(vec![