This is an automated email from the ASF dual-hosted git repository.

github-bot pushed a commit to branch 
gh-readonly-queue/main/pr-2162-2d47fec0ab0632d2c7453387a226a4c4d293f93a
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git

commit be460b2b4db3ff01aa18fdbe400bc24270377fa6
Author: xitep <[email protected]>
AuthorDate: Thu Jan 29 14:52:59 2026 +0100

    [MySQL, Oracle] Parse optimizer hints (#2162)
    
    Co-authored-by: Ifeanyi Ubah <[email protected]>
---
 src/ast/dml.rs                | 54 ++++++++++++++++++++++++++++++++-----
 src/ast/mod.rs                | 51 +++++++++++++++++++++++++++++++++++
 src/ast/query.rs              | 10 +++++++
 src/ast/spans.rs              |  5 ++++
 src/dialect/generic.rs        |  4 +++
 src/dialect/mod.rs            | 10 +++++++
 src/dialect/mysql.rs          |  4 +++
 src/dialect/oracle.rs         |  4 +++
 src/parser/merge.rs           |  2 ++
 src/parser/mod.rs             | 63 +++++++++++++++++++++++++++++++++++++++++++
 tests/sqlparser_bigquery.rs   |  2 ++
 tests/sqlparser_clickhouse.rs |  1 +
 tests/sqlparser_common.rs     | 12 +++++++++
 tests/sqlparser_duckdb.rs     |  2 ++
 tests/sqlparser_mssql.rs      |  3 +++
 tests/sqlparser_mysql.rs      | 57 ++++++++++++++++++++++++++++++++++++---
 tests/sqlparser_oracle.rs     | 55 +++++++++++++++++++++++++++++++++++++
 tests/sqlparser_postgres.rs   |  6 +++++
 tests/sqlparser_sqlite.rs     |  1 +
 19 files changed, 336 insertions(+), 10 deletions(-)

diff --git a/src/ast/dml.rs b/src/ast/dml.rs
index 32c023e0..4c36f705 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<OptimizerHint>,
     /// Only for Sqlite
     pub or: Option<SqliteOnConflict>,
     /// 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<OptimizerHint>,
     /// Multi tables delete are supported in mysql
     pub tables: Vec<ObjectName>,
     /// 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<OptimizerHint>,
     /// 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<OptimizerHint>,
     /// 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 33f99bc2..f255e5f3 100644
--- a/src/ast/mod.rs
+++ b/src/ast/mod.rs
@@ -11688,6 +11688,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 7ea4de19..08448cab 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<OptimizerHint>,
     /// `SELECT [DISTINCT] ...`
     pub distinct: Option<Distinct>,
     /// MSSQL syntax: `TOP (<N>) [ 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 126e587a..60c983fa 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,
@@ -1290,6 +1292,7 @@ impl Spanned for Insert {
     fn span(&self) -> Span {
         let Insert {
             insert_token,
+            optimizer_hint: _,
             or: _,     // enum, sqlite specific
             ignore: _, // bool
             into: _,   // bool
@@ -2233,6 +2236,7 @@ impl Spanned for Select {
     fn span(&self) -> Span {
         let Select {
             select_token,
+            optimizer_hint: _,
             distinct: _, // todo
             top: _,      // todo, mysql specific
             projection,
@@ -2819,6 +2823,7 @@ WHERE id = 1
         // ~ individual tokens within the statement
         let Statement::Merge(Merge {
             merge_token,
+            optimizer_hint: _,
             into: _,
             table: _,
             source: _,
diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs
index d460c523..345d63fe 100644
--- a/src/dialect/generic.rs
+++ b/src/dialect/generic.rs
@@ -271,4 +271,8 @@ impl Dialect for GenericDialect {
     fn supports_select_format(&self) -> bool {
         true
     }
+
+    fn supports_comment_optimizer_hint(&self) -> bool {
+        true
+    }
 }
diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs
index 98ec93da..8cff4d23 100644
--- a/src/dialect/mod.rs
+++ b/src/dialect/mod.rs
@@ -1322,6 +1322,16 @@ pub trait Dialect: Debug + Any {
         false
     }
 
+    /// Returns `true` if the dialect supports query optimizer hints in the
+    /// format of single and multi line comments immediately following a
+    /// `SELECT`, `INSERT`, `REPLACE`, `DELETE`, or `MERGE` keyword.
+    ///
+    /// [MySQL](https://dev.mysql.com/doc/refman/8.4/en/optimizer-hints.html)
+    /// 
[Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/Comments.html#SQLRF-GUID-D316D545-89E2-4D54-977F-FC97815CD62E)
+    fn supports_comment_optimizer_hint(&self) -> bool {
+        false
+    }
+
     /// Returns true if the dialect considers the `&&` operator as a boolean 
AND operator.
     fn supports_double_ampersand_operator(&self) -> bool {
         false
diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs
index 81aa9d44..b44001fe 100644
--- a/src/dialect/mysql.rs
+++ b/src/dialect/mysql.rs
@@ -182,6 +182,10 @@ impl Dialect for MySqlDialect {
     fn supports_binary_kw_as_cast(&self) -> bool {
         true
     }
+
+    fn supports_comment_optimizer_hint(&self) -> bool {
+        true
+    }
 }
 
 /// `LOCK TABLES`
diff --git a/src/dialect/oracle.rs b/src/dialect/oracle.rs
index 54c2ace5..7ff93262 100644
--- a/src/dialect/oracle.rs
+++ b/src/dialect/oracle.rs
@@ -99,4 +99,8 @@ impl Dialect for OracleDialect {
     fn supports_quote_delimited_string(&self) -> bool {
         true
     }
+
+    fn supports_comment_optimizer_hint(&self) -> bool {
+        true
+    }
 }
diff --git a/src/parser/merge.rs b/src/parser/merge.rs
index 62da68a2..31f435f8 100644
--- a/src/parser/merge.rs
+++ b/src/parser/merge.rs
@@ -43,6 +43,7 @@ impl Parser<'_> {
 
     /// Parse a `MERGE` statement
     pub fn parse_merge(&mut self, merge_token: TokenWithSpan) -> Result<Merge, 
ParserError> {
+        let optimizer_hint = self.maybe_parse_optimizer_hint()?;
         let into = self.parse_keyword(Keyword::INTO);
 
         let table = self.parse_table_factor()?;
@@ -59,6 +60,7 @@ impl Parser<'_> {
 
         Ok(Merge {
             merge_token: merge_token.into(),
+            optimizer_hint,
             into,
             table,
             source,
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index 5847f779..23a961d3 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -4325,6 +4325,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.
@@ -13026,6 +13031,7 @@ impl<'a> Parser<'a> {
 
     /// Parse a `DELETE` statement and return `Statement::Delete`.
     pub fn parse_delete(&mut self, delete_token: TokenWithSpan) -> 
Result<Statement, ParserError> {
+        let optimizer_hint = self.maybe_parse_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
@@ -13069,6 +13075,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)
@@ -13839,6 +13846,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,
@@ -13866,6 +13874,7 @@ impl<'a> Parser<'a> {
         }
 
         let select_token = self.expect_keyword(Keyword::SELECT)?;
+        let optimizer_hint = self.maybe_parse_optimizer_hint()?;
         let value_table_mode = self.parse_value_table_mode()?;
 
         let mut top_before_distinct = false;
@@ -14020,6 +14029,7 @@ impl<'a> Parser<'a> {
 
         Ok(Select {
             select_token: AttachedToken(select_token),
+            optimizer_hint,
             distinct,
             top,
             top_before_distinct,
@@ -14048,6 +14058,55 @@ 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 maybe_parse_optimizer_hint(&mut self) -> Result<Option<OptimizerHint>, 
ParserError> {
+        let supports_hints = self.dialect.supports_comment_optimizer_hint();
+        if !supports_hints {
+            return Ok(None);
+        }
+        loop {
+            let t = self.peek_nth_token_no_skip_ref(0);
+            match &t.token {
+                Token::Whitespace(ws) => {
+                    match ws {
+                        Whitespace::SingleLineComment { comment, .. }
+                        | Whitespace::MultiLineComment(comment) => {
+                            return Ok(match comment.strip_prefix("+") {
+                                None => None,
+                                Some(text) => {
+                                    let hint = OptimizerHint {
+                                        text: text.into(),
+                                        style: if let 
Whitespace::SingleLineComment {
+                                            prefix, ..
+                                        } = ws
+                                        {
+                                            OptimizerHintStyle::SingleLine {
+                                                prefix: prefix.clone(),
+                                            }
+                                        } else {
+                                            OptimizerHintStyle::MultiLine
+                                        },
+                                    };
+                                    // Consume the comment token
+                                    self.next_token_no_skip();
+                                    Some(hint)
+                                }
+                            });
+                        }
+                        Whitespace::Space | Whitespace::Tab | 
Whitespace::Newline => {
+                            // Consume the token and try with the next 
whitespace or comment
+                            self.next_token_no_skip();
+                        }
+                    }
+                }
+                _ => return Ok(None),
+            }
+        }
+    }
+
     fn parse_value_table_mode(&mut self) -> Result<Option<ValueTableMode>, 
ParserError> {
         if !dialect_of!(self is BigQueryDialect) {
             return Ok(None);
@@ -16742,6 +16801,7 @@ impl<'a> Parser<'a> {
 
     /// Parse an INSERT statement
     pub fn parse_insert(&mut self, insert_token: TokenWithSpan) -> 
Result<Statement, ParserError> {
+        let optimizer_hint = self.maybe_parse_optimizer_hint()?;
         let or = self.parse_conflict_clause();
         let priority = if !dialect_of!(self is MySqlDialect | GenericDialect) {
             None
@@ -16911,6 +16971,7 @@ impl<'a> Parser<'a> {
 
             Ok(Insert {
                 insert_token: insert_token.into(),
+                optimizer_hint,
                 or,
                 table: table_object,
                 table_alias,
@@ -17014,6 +17075,7 @@ impl<'a> Parser<'a> {
 
     /// Parse an `UPDATE` statement and return `Statement::Update`.
     pub fn parse_update(&mut self, update_token: TokenWithSpan) -> 
Result<Statement, ParserError> {
+        let optimizer_hint = self.maybe_parse_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) {
@@ -17049,6 +17111,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 d8c3ada1..fb28b4d2 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 44bfcda4..ac31a278 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 87c15e2d..2796f258 100644
--- a/tests/sqlparser_common.rs
+++ b/tests/sqlparser_common.rs
@@ -455,6 +455,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![],
@@ -470,6 +471,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,
@@ -548,6 +550,7 @@ fn parse_update_with_table_alias() {
             returning,
             or: None,
             limit: None,
+            optimizer_hint: None,
             update_token: _,
         }) => {
             assert_eq!(
@@ -5804,6 +5807,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,
@@ -6534,6 +6538,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,
@@ -8910,6 +8915,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())],
@@ -9911,6 +9917,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,
@@ -12314,6 +12321,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,
@@ -12622,6 +12630,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,
@@ -12704,6 +12713,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,
@@ -13637,6 +13647,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,
@@ -15776,6 +15787,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 bdfe4f50..7cc710de 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::<SetExpr>::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::<SetExpr>::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 1927b864..7ef4ce85 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 4a620538..80aed5bf 100644
--- a/tests/sqlparser_mysql.rs
+++ b/tests/sqlparser_mysql.rs
@@ -1435,6 +1435,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,
@@ -1490,6 +1491,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,
@@ -1537,7 +1539,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,
@@ -1589,7 +1591,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,
@@ -2409,7 +2411,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,
@@ -2584,6 +2586,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,
@@ -2651,6 +2654,7 @@ fn parse_update_with_joins() {
             returning,
             or: None,
             limit: None,
+            optimizer_hint: None,
             update_token: _,
         }) => {
             assert_eq!(
@@ -3216,6 +3220,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,
@@ -3539,6 +3544,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,
@@ -4381,3 +4387,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 68366036..1c12f868 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 7bd7f43c..a449eebc 100644
--- a/tests/sqlparser_postgres.rs
+++ b/tests/sqlparser_postgres.rs
@@ -1284,6 +1284,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,
@@ -3064,6 +3065,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,
@@ -3090,6 +3092,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,
@@ -5384,6 +5387,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,
@@ -5455,6 +5459,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,
@@ -5528,6 +5533,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 321cfef0..da311ac0 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![


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to