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

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

commit e81eb14a147f32e68942aea668ab56c1548394b2
Author: Marcelo Altmann <[email protected]>
AuthorDate: Fri Feb 20 13:01:06 2026 -0300

    Allow custom OptimizerHints (#2216)
---
 src/ast/dml.rs                |  31 ++++++-------
 src/ast/mod.rs                |  16 +++++--
 src/ast/query.rs              |   6 +--
 src/ast/spans.rs              |  10 ++---
 src/dialect/snowflake.rs      |   2 +-
 src/parser/merge.rs           |   4 +-
 src/parser/mod.rs             | 100 +++++++++++++++++++++++-------------------
 tests/sqlparser_bigquery.rs   |   4 +-
 tests/sqlparser_clickhouse.rs |   2 +-
 tests/sqlparser_common.rs     |  30 ++++++-------
 tests/sqlparser_duckdb.rs     |   4 +-
 tests/sqlparser_mssql.rs      |   6 +--
 tests/sqlparser_mysql.rs      |  50 ++++++++++++++++-----
 tests/sqlparser_oracle.rs     |  43 ++++++++++--------
 tests/sqlparser_postgres.rs   |  12 ++---
 tests/sqlparser_sqlite.rs     |   2 +-
 16 files changed, 191 insertions(+), 131 deletions(-)

diff --git a/src/ast/dml.rs b/src/ast/dml.rs
index f9c8823a..a0be916d 100644
--- a/src/ast/dml.rs
+++ b/src/ast/dml.rs
@@ -43,11 +43,11 @@ use super::{
 pub struct Insert {
     /// Token for the `INSERT` keyword (or its substitutes)
     pub insert_token: AttachedToken,
-    /// A query optimizer hint
+    /// Query optimizer hints
     ///
     /// [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>,
+    pub optimizer_hints: Vec<OptimizerHint>,
     /// Only for Sqlite
     pub or: Option<SqliteOnConflict>,
     /// Only for mysql
@@ -133,7 +133,7 @@ impl Display for Insert {
 
         if let Some(on_conflict) = self.or {
             f.write_str("INSERT")?;
-            if let Some(hint) = self.optimizer_hint.as_ref() {
+            for hint in &self.optimizer_hints {
                 write!(f, " {hint}")?;
             }
             write!(f, " {on_conflict} INTO {table_name} ")?;
@@ -147,7 +147,7 @@ impl Display for Insert {
                     "INSERT"
                 }
             )?;
-            if let Some(hint) = self.optimizer_hint.as_ref() {
+            for hint in &self.optimizer_hints {
                 write!(f, " {hint}")?;
             }
             if let Some(priority) = self.priority {
@@ -267,11 +267,11 @@ impl Display for Insert {
 pub struct Delete {
     /// Token for the `DELETE` keyword
     pub delete_token: AttachedToken,
-    /// A query optimizer hint
+    /// Query optimizer hints
     ///
     /// [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>,
+    pub optimizer_hints: Vec<OptimizerHint>,
     /// Multi tables delete are supported in mysql
     pub tables: Vec<ObjectName>,
     /// FROM
@@ -291,7 +291,7 @@ 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() {
+        for hint in &self.optimizer_hints {
             f.write_str(" ")?;
             hint.fmt(f)?;
         }
@@ -345,11 +345,11 @@ impl Display for Delete {
 pub struct Update {
     /// Token for the `UPDATE` keyword
     pub update_token: AttachedToken,
-    /// A query optimizer hint
+    /// Query optimizer hints
     ///
     /// [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>,
+    pub optimizer_hints: Vec<OptimizerHint>,
     /// TABLE
     pub table: TableWithJoins,
     /// Column assignments
@@ -368,11 +368,12 @@ 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("UPDATE")?;
+        for hint in &self.optimizer_hints {
             f.write_str(" ")?;
+            hint.fmt(f)?;
         }
+        f.write_str(" ")?;
         if let Some(or) = &self.or {
             or.fmt(f)?;
             f.write_str(" ")?;
@@ -419,10 +420,10 @@ impl Display for Update {
 pub struct Merge {
     /// The `MERGE` token that starts the statement.
     pub merge_token: AttachedToken,
-    /// A query optimizer hint
+    /// Query optimizer hints
     ///
     /// 
[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>,
+    pub optimizer_hints: Vec<OptimizerHint>,
     /// optional INTO keyword
     pub into: bool,
     /// Specifies the table to merge
@@ -440,7 +441,7 @@ 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() {
+        for hint in &self.optimizer_hints {
             write!(f, " {hint}")?;
         }
         if self.into {
diff --git a/src/ast/mod.rs b/src/ast/mod.rs
index a06526ec..61b0f65b 100644
--- a/src/ast/mod.rs
+++ b/src/ast/mod.rs
@@ -11674,12 +11674,19 @@ pub struct ResetStatement {
 /// `SELECT`, `INSERT`, `UPDATE`, `REPLACE`, `MERGE`, and `DELETE` keywords in
 /// the corresponding statements.
 ///
-/// See [Select::optimizer_hint]
+/// See [Select::optimizer_hints]
 #[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
+    /// An optional prefix between the comment marker and `+`.
+    ///
+    /// Standard optimizer hints like `/*+ ... */` have an empty prefix,
+    /// while system-specific hints like `/*abc+ ... */` have `prefix = "abc"`.
+    /// The prefix is any sequence of ASCII alphanumeric characters
+    /// immediately before the `+` marker.
+    pub prefix: String,
+    /// the raw text of the optimizer hint without its markers
     pub text: String,
     /// the style of the comment which `text` was extracted from,
     /// e.g. `/*+...*/` or `--+...`
@@ -11709,11 +11716,14 @@ impl fmt::Display for OptimizerHint {
         match &self.style {
             OptimizerHintStyle::SingleLine { prefix } => {
                 f.write_str(prefix)?;
+                f.write_str(&self.prefix)?;
                 f.write_str("+")?;
                 f.write_str(&self.text)
             }
             OptimizerHintStyle::MultiLine => {
-                f.write_str("/*+")?;
+                f.write_str("/*")?;
+                f.write_str(&self.prefix)?;
+                f.write_str("+")?;
                 f.write_str(&self.text)?;
                 f.write_str("*/")
             }
diff --git a/src/ast/query.rs b/src/ast/query.rs
index 6d95216d..ff617a38 100644
--- a/src/ast/query.rs
+++ b/src/ast/query.rs
@@ -445,11 +445,11 @@ impl SelectModifiers {
 pub struct Select {
     /// Token for the `SELECT` keyword
     pub select_token: AttachedToken,
-    /// A query optimizer hint
+    /// Query optimizer hints
     ///
     /// [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>,
+    pub optimizer_hints: Vec<OptimizerHint>,
     /// `SELECT [DISTINCT] ...`
     pub distinct: Option<Distinct>,
     /// MySQL-specific SELECT modifiers.
@@ -521,7 +521,7 @@ impl fmt::Display for Select {
             }
         }
 
-        if let Some(hint) = self.optimizer_hint.as_ref() {
+        for hint in &self.optimizer_hints {
             f.write_str(" ")?;
             hint.fmt(f)?;
         }
diff --git a/src/ast/spans.rs b/src/ast/spans.rs
index 74f19e83..128fe01b 100644
--- a/src/ast/spans.rs
+++ b/src/ast/spans.rs
@@ -900,7 +900,7 @@ impl Spanned for Delete {
     fn span(&self) -> Span {
         let Delete {
             delete_token,
-            optimizer_hint: _,
+            optimizer_hints: _,
             tables,
             from,
             using,
@@ -934,7 +934,7 @@ impl Spanned for Update {
     fn span(&self) -> Span {
         let Update {
             update_token,
-            optimizer_hint: _,
+            optimizer_hints: _,
             table,
             assignments,
             from,
@@ -1298,7 +1298,7 @@ impl Spanned for Insert {
     fn span(&self) -> Span {
         let Insert {
             insert_token,
-            optimizer_hint: _,
+            optimizer_hints: _,
             or: _,     // enum, sqlite specific
             ignore: _, // bool
             into: _,   // bool
@@ -2246,7 +2246,7 @@ impl Spanned for Select {
     fn span(&self) -> Span {
         let Select {
             select_token,
-            optimizer_hint: _,
+            optimizer_hints: _,
             distinct: _, // todo
             select_modifiers: _,
             top: _, // todo, mysql specific
@@ -2840,7 +2840,7 @@ WHERE id = 1
         // ~ individual tokens within the statement
         let Statement::Merge(Merge {
             merge_token,
-            optimizer_hint: _,
+            optimizer_hints: _,
             into: _,
             table: _,
             source: _,
diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs
index 8af1367f..984e384f 100644
--- a/src/dialect/snowflake.rs
+++ b/src/dialect/snowflake.rs
@@ -1765,7 +1765,7 @@ fn parse_multi_table_insert(
 
     Ok(Statement::Insert(Insert {
         insert_token: insert_token.into(),
-        optimizer_hint: None,
+        optimizer_hints: vec![],
         or: None,
         ignore: false,
         into: false,
diff --git a/src/parser/merge.rs b/src/parser/merge.rs
index 31f435f8..a927bc4b 100644
--- a/src/parser/merge.rs
+++ b/src/parser/merge.rs
@@ -43,7 +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 optimizer_hints = self.maybe_parse_optimizer_hints()?;
         let into = self.parse_keyword(Keyword::INTO);
 
         let table = self.parse_table_factor()?;
@@ -60,7 +60,7 @@ impl Parser<'_> {
 
         Ok(Merge {
             merge_token: merge_token.into(),
-            optimizer_hint,
+            optimizer_hints,
             into,
             table,
             source,
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index a7ee5415..16eb7a8b 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -13272,7 +13272,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 optimizer_hints = self.maybe_parse_optimizer_hints()?;
         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
@@ -13316,7 +13316,7 @@ impl<'a> Parser<'a> {
 
         Ok(Statement::Delete(Delete {
             delete_token: delete_token.into(),
-            optimizer_hint,
+            optimizer_hints,
             tables,
             from: if with_from_keyword {
                 FromTable::WithFromKeyword(from)
@@ -14088,7 +14088,7 @@ impl<'a> Parser<'a> {
             if !self.peek_keyword(Keyword::SELECT) {
                 return Ok(Select {
                     select_token: AttachedToken(from_token),
-                    optimizer_hint: None,
+                    optimizer_hints: vec![],
                     distinct: None,
                     select_modifiers: None,
                     top: None,
@@ -14117,7 +14117,7 @@ impl<'a> Parser<'a> {
         }
 
         let select_token = self.expect_keyword(Keyword::SELECT)?;
-        let optimizer_hint = self.maybe_parse_optimizer_hint()?;
+        let optimizer_hints = self.maybe_parse_optimizer_hints()?;
         let value_table_mode = self.parse_value_table_mode()?;
 
         let (select_modifiers, distinct_select_modifier) =
@@ -14276,7 +14276,7 @@ impl<'a> Parser<'a> {
 
         Ok(Select {
             select_token: AttachedToken(select_token),
-            optimizer_hint,
+            optimizer_hints,
             distinct,
             select_modifiers,
             top,
@@ -14306,53 +14306,65 @@ impl<'a> Parser<'a> {
         })
     }
 
-    /// Parses an optional optimizer hint at the current token position
+    /// Parses optimizer hints at the current token position.
+    ///
+    /// Collects all `/*prefix+...*/` and `--prefix+...` patterns.
+    /// The `prefix` is any run of ASCII alphanumeric characters between the
+    /// comment marker and `+` (e.g. `""` for `/*+...*/`, `"abc"` for 
`/*abc+...*/`).
     ///
     /// 
[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> {
+    fn maybe_parse_optimizer_hints(&mut self) -> Result<Vec<OptimizerHint>, 
ParserError> {
         let supports_hints = self.dialect.supports_comment_optimizer_hint();
         if !supports_hints {
-            return Ok(None);
+            return Ok(vec![]);
         }
+        let mut hints = vec![];
         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();
-                        }
+            let Token::Whitespace(ws) = &t.token else {
+                break;
+            };
+            match ws {
+                Whitespace::SingleLineComment { comment, prefix } => {
+                    if let Some((hint_prefix, text)) = 
Self::extract_hint_prefix_and_text(comment) {
+                        hints.push(OptimizerHint {
+                            prefix: hint_prefix,
+                            text,
+                            style: OptimizerHintStyle::SingleLine {
+                                prefix: prefix.clone(),
+                            },
+                        });
+                    }
+                    self.next_token_no_skip();
+                }
+                Whitespace::MultiLineComment(comment) => {
+                    if let Some((hint_prefix, text)) = 
Self::extract_hint_prefix_and_text(comment) {
+                        hints.push(OptimizerHint {
+                            prefix: hint_prefix,
+                            text,
+                            style: OptimizerHintStyle::MultiLine,
+                        });
                     }
+                    self.next_token_no_skip();
+                }
+                Whitespace::Space | Whitespace::Tab | Whitespace::Newline => {
+                    self.next_token_no_skip();
                 }
-                _ => return Ok(None),
             }
         }
+        Ok(hints)
+    }
+
+    /// Checks if a comment's content starts with `[ASCII-alphanumeric]*+`
+    /// and returns `(prefix, text_after_plus)` if so.
+    fn extract_hint_prefix_and_text(comment: &str) -> Option<(String, String)> 
{
+        let (before_plus, text) = comment.split_once('+')?;
+        if before_plus.chars().all(|c| c.is_ascii_alphanumeric()) {
+            Some((before_plus.to_string(), text.to_string()))
+        } else {
+            None
+        }
     }
 
     /// Parses MySQL SELECT modifiers and DISTINCT/ALL in any order.
@@ -17173,7 +17185,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 optimizer_hints = self.maybe_parse_optimizer_hints()?;
         let or = self.parse_conflict_clause();
         let priority = if !dialect_of!(self is MySqlDialect | GenericDialect) {
             None
@@ -17343,7 +17355,7 @@ impl<'a> Parser<'a> {
 
             Ok(Insert {
                 insert_token: insert_token.into(),
-                optimizer_hint,
+                optimizer_hints,
                 or,
                 table: table_object,
                 table_alias,
@@ -17451,7 +17463,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 optimizer_hints = self.maybe_parse_optimizer_hints()?;
         let or = self.parse_conflict_clause();
         let table = self.parse_table_and_joins()?;
         let from_before_set = if self.parse_keyword(Keyword::FROM) {
@@ -17487,7 +17499,7 @@ impl<'a> Parser<'a> {
         };
         Ok(Update {
             update_token: update_token.into(),
-            optimizer_hint,
+            optimizer_hints,
             table,
             assignments,
             from,
diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs
index cf843ea2..ce962cb8 100644
--- a/tests/sqlparser_bigquery.rs
+++ b/tests/sqlparser_bigquery.rs
@@ -2681,7 +2681,7 @@ fn test_export_data() {
                         }),
                         Span::empty()
                     )),
-                    optimizer_hint: None,
+                    optimizer_hints: vec![],
                     distinct: None,
                     select_modifiers: None,
                     top: None,
@@ -2787,7 +2787,7 @@ fn test_export_data() {
                         }),
                         Span::empty()
                     )),
-                    optimizer_hint: None,
+                    optimizer_hints: vec![],
                     distinct: None,
                     select_modifiers: None,
                     top: None,
diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs
index b8b4e337..82f79577 100644
--- a/tests/sqlparser_clickhouse.rs
+++ b/tests/sqlparser_clickhouse.rs
@@ -41,7 +41,7 @@ fn parse_map_access_expr() {
     assert_eq!(
         Select {
             select_token: AttachedToken::empty(),
-            optimizer_hint: None,
+            optimizer_hints: vec![],
             distinct: None,
             select_modifiers: None,
             top: None,
diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs
index c37cfa44..a3b5404d 100644
--- a/tests/sqlparser_common.rs
+++ b/tests/sqlparser_common.rs
@@ -455,7 +455,7 @@ fn parse_update_set_from() {
         stmt,
         Statement::Update(Update {
             update_token: AttachedToken::empty(),
-            optimizer_hint: None,
+            optimizer_hints: vec![],
             table: TableWithJoins {
                 relation: 
table_from_name(ObjectName::from(vec![Ident::new("t1")])),
                 joins: vec![],
@@ -471,7 +471,7 @@ fn parse_update_set_from() {
                         with: None,
                         body: Box::new(SetExpr::Select(Box::new(Select {
                             select_token: AttachedToken::empty(),
-                            optimizer_hint: None,
+                            optimizer_hints: vec![],
                             distinct: None,
                             select_modifiers: None,
                             top: None,
@@ -551,9 +551,9 @@ fn parse_update_with_table_alias() {
             returning,
             or: None,
             limit: None,
-            optimizer_hint: None,
+            optimizer_hints,
             update_token: _,
-        }) => {
+        }) if optimizer_hints.is_empty() => {
             assert_eq!(
                 TableWithJoins {
                     relation: TableFactor::Table {
@@ -5819,7 +5819,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,
+        optimizer_hints: vec![],
         distinct: None,
         select_modifiers: None,
         top: None,
@@ -6551,7 +6551,7 @@ fn parse_interval_and_or_xor() {
         with: None,
         body: Box::new(SetExpr::Select(Box::new(Select {
             select_token: AttachedToken::empty(),
-            optimizer_hint: None,
+            optimizer_hints: vec![],
             distinct: None,
             select_modifiers: None,
             top: None,
@@ -8929,7 +8929,7 @@ fn lateral_function() {
     let actual_select_only = verified_only_select(sql);
     let expected = Select {
         select_token: AttachedToken::empty(),
-        optimizer_hint: None,
+        optimizer_hints: vec![],
         distinct: None,
         select_modifiers: None,
         top: None,
@@ -9932,7 +9932,7 @@ fn parse_merge() {
                         with: None,
                         body: Box::new(SetExpr::Select(Box::new(Select {
                             select_token: AttachedToken::empty(),
-                            optimizer_hint: None,
+                            optimizer_hints: vec![],
                             distinct: None,
                             select_modifiers: None,
                             top: None,
@@ -12356,7 +12356,7 @@ fn parse_unload() {
             query: Some(Box::new(Query {
                 body: Box::new(SetExpr::Select(Box::new(Select {
                     select_token: AttachedToken::empty(),
-                    optimizer_hint: None,
+                    optimizer_hints: vec![],
                     distinct: None,
                     select_modifiers: None,
                     top: None,
@@ -12677,7 +12677,7 @@ fn parse_connect_by() {
         dialects.verified_only_select(connect_by_1),
         Select {
             select_token: AttachedToken::empty(),
-            optimizer_hint: None,
+            optimizer_hints: vec![],
             distinct: None,
             select_modifiers: None,
             top: None,
@@ -12744,7 +12744,7 @@ fn parse_connect_by() {
         dialects.verified_only_select(connect_by_2),
         Select {
             select_token: AttachedToken::empty(),
-            optimizer_hint: None,
+            optimizer_hints: vec![],
             distinct: None,
             select_modifiers: None,
             top: None,
@@ -12812,7 +12812,7 @@ fn parse_connect_by() {
         dialects.verified_only_select(connect_by_3),
         Select {
             select_token: AttachedToken::empty(),
-            optimizer_hint: None,
+            optimizer_hints: vec![],
             distinct: None,
             select_modifiers: None,
             top: None,
@@ -12900,7 +12900,7 @@ fn parse_connect_by() {
         dialects.verified_only_select(connect_by_5),
         Select {
             select_token: AttachedToken::empty(),
-            optimizer_hint: None,
+            optimizer_hints: vec![],
             distinct: None,
             select_modifiers: None,
             top: None,
@@ -13863,7 +13863,7 @@ fn test_extract_seconds_ok() {
         with: None,
         body: Box::new(SetExpr::Select(Box::new(Select {
             select_token: AttachedToken::empty(),
-            optimizer_hint: None,
+            optimizer_hints: vec![],
             distinct: None,
             select_modifiers: None,
             top: None,
@@ -16028,7 +16028,7 @@ fn test_select_from_first() {
             with: None,
             body: Box::new(SetExpr::Select(Box::new(Select {
                 select_token: AttachedToken::empty(),
-                optimizer_hint: None,
+                optimizer_hints: vec![],
                 distinct: None,
                 select_modifiers: None,
                 top: None,
diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs
index 91eb2799..e0e3f143 100644
--- a/tests/sqlparser_duckdb.rs
+++ b/tests/sqlparser_duckdb.rs
@@ -266,7 +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,
+                optimizer_hints: vec![],
                 distinct: None,
                 select_modifiers: None,
                 top: None,
@@ -299,7 +299,7 @@ fn test_select_union_by_name() {
             }))),
             right: Box::<SetExpr>::new(SetExpr::Select(Box::new(Select {
                 select_token: AttachedToken::empty(),
-                optimizer_hint: None,
+                optimizer_hints: vec![],
                 distinct: None,
                 select_modifiers: None,
                 top: None,
diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs
index d7d11ba6..b5fd1e77 100644
--- a/tests/sqlparser_mssql.rs
+++ b/tests/sqlparser_mssql.rs
@@ -141,7 +141,7 @@ fn parse_create_procedure() {
                     pipe_operators: vec![],
                     body: Box::new(SetExpr::Select(Box::new(Select {
                         select_token: AttachedToken::empty(),
-                        optimizer_hint: None,
+                        optimizer_hints: vec![],
                         distinct: None,
                         select_modifiers: None,
                         top: None,
@@ -1350,7 +1350,7 @@ fn parse_substring_in_select() {
 
                     body: Box::new(SetExpr::Select(Box::new(Select {
                         select_token: AttachedToken::empty(),
-                        optimizer_hint: None,
+                        optimizer_hints: vec![],
                         distinct: Some(Distinct::Distinct),
                         select_modifiers: None,
                         top: None,
@@ -1509,7 +1509,7 @@ fn parse_mssql_declare() {
 
                 body: Box::new(SetExpr::Select(Box::new(Select {
                     select_token: AttachedToken::empty(),
-                    optimizer_hint: None,
+                    optimizer_hints: vec![],
                     distinct: None,
                     select_modifiers: None,
                     top: None,
diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs
index 4ad0404b..30405623 100644
--- a/tests/sqlparser_mysql.rs
+++ b/tests/sqlparser_mysql.rs
@@ -1435,7 +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,
+                optimizer_hints: vec![],
                 distinct: None,
                 select_modifiers: None,
                 top: None,
@@ -1492,7 +1492,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,
+                optimizer_hints: vec![],
                 distinct: None,
                 select_modifiers: None,
                 top: None,
@@ -1541,7 +1541,7 @@ fn parse_escaped_backticks_with_escape() {
             with: None,
             body: Box::new(SetExpr::Select(Box::new(Select {
                 select_token: AttachedToken::empty(),
-                optimizer_hint: None,
+                optimizer_hints: vec![],
                 distinct: None,
                 select_modifiers: None,
                 top: None,
@@ -1594,7 +1594,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,
+                optimizer_hints: vec![],
                 distinct: None,
                 select_modifiers: None,
                 top: None,
@@ -2415,7 +2415,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,
+                    optimizer_hints: vec![],
                     distinct: None,
                     select_modifiers: None,
                     top: None,
@@ -2591,7 +2591,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,
+                    optimizer_hints: vec![],
                     distinct: None,
                     select_modifiers: None,
                     top: None,
@@ -2660,9 +2660,9 @@ fn parse_update_with_joins() {
             returning,
             or: None,
             limit: None,
-            optimizer_hint: None,
+            optimizer_hints,
             update_token: _,
-        }) => {
+        }) if optimizer_hints.is_empty() => {
             assert_eq!(
                 TableWithJoins {
                     relation: TableFactor::Table {
@@ -3226,7 +3226,7 @@ fn parse_substring_in_select() {
                     with: None,
                     body: Box::new(SetExpr::Select(Box::new(Select {
                         select_token: AttachedToken::empty(),
-                        optimizer_hint: None,
+                        optimizer_hints: vec![],
                         distinct: Some(Distinct::Distinct),
                         select_modifiers: None,
                         top: None,
@@ -3572,7 +3572,7 @@ fn parse_hex_string_introducer() {
             with: None,
             body: Box::new(SetExpr::Select(Box::new(Select {
                 select_token: AttachedToken::empty(),
-                optimizer_hint: None,
+                optimizer_hints: vec![],
                 distinct: None,
                 select_modifiers: None,
                 top: None,
@@ -4649,6 +4649,36 @@ fn test_optimizer_hints() {
         "\
        DELETE /*+ foobar */ FROM table_name",
     );
+
+    // prefixed hints: any alphanumeric prefix before `+` is captured
+    let select = mysql_dialect.verified_only_select("SELECT /*abc+ text */ 1");
+    assert_eq!(select.optimizer_hints.len(), 1);
+    assert_eq!(select.optimizer_hints[0].prefix, "abc");
+    assert_eq!(select.optimizer_hints[0].text, " text ");
+
+    // multiple hints with different prefixes
+    let select = mysql_dialect.verified_only_select("SELECT /*+ A */ /*x2+ B 
*/ 1");
+    assert_eq!(select.optimizer_hints.len(), 2);
+    assert_eq!(select.optimizer_hints[0].prefix, "");
+    assert_eq!(select.optimizer_hints[0].text, " A ");
+    assert_eq!(select.optimizer_hints[1].prefix, "x2");
+    assert_eq!(select.optimizer_hints[1].text, " B ");
+
+    // hints mixed with regular comments: regular comments are skipped
+    let select = mysql_dialect.verified_only_select_with_canonical(
+        "SELECT /*+ A */ /* Regular comment */ /*x2+ B */ 1",
+        "SELECT /*+ A */ /*x2+ B */ 1",
+    );
+    assert_eq!(select.optimizer_hints.len(), 2);
+    assert_eq!(select.optimizer_hints[0].prefix, "");
+    assert_eq!(select.optimizer_hints[0].text, " A ");
+    assert_eq!(select.optimizer_hints[1].prefix, "x2");
+    assert_eq!(select.optimizer_hints[1].text, " B ");
+
+    // prefixed hints in INSERT/UPDATE/DELETE
+    mysql_dialect.verified_stmt("INSERT /*abc+ append */ INTO t2 VALUES (2)");
+    mysql_dialect.verified_stmt("UPDATE /*abc+ PARALLEL */ table_name SET 
column1 = 1");
+    mysql_dialect.verified_stmt("DELETE /*abc+ ENABLE_DML */ FROM table_name");
 }
 
 #[test]
diff --git a/tests/sqlparser_oracle.rs b/tests/sqlparser_oracle.rs
index 0dbccdb5..8f7bb867 100644
--- a/tests/sqlparser_oracle.rs
+++ b/tests/sqlparser_oracle.rs
@@ -338,36 +338,34 @@ fn parse_national_quote_delimited_string_but_is_a_word() {
 fn test_optimizer_hints() {
     let oracle_dialect = oracle();
 
-    // selects
+    // selects: all `/*+...*/` comments are collected as hints
     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")
+        "SELECT /*+one two three*/ /*+four five six*/ 1 FROM dual",
+        "SELECT /*+one two three*/ /*+four five six*/ 1 FROM dual",
     );
+    assert_eq!(select.optimizer_hints.len(), 2);
+    assert_eq!(select.optimizer_hints[0].text, "one two three");
+    assert_eq!(select.optimizer_hints[0].prefix, "");
+    assert_eq!(select.optimizer_hints[1].text, "four five six");
 
+    // regular comments are skipped, hints after them are still collected
     let select = oracle_dialect.verified_only_select_with_canonical(
-        "SELECT /*one two three*/ /*+not a hint!*/ 1 FROM dual",
-        "SELECT 1 FROM dual",
+        "SELECT /*one two three*/ /*+four five six*/ 1 FROM dual",
+        "SELECT /*+four five six*/ 1 FROM dual",
     );
-    assert_eq!(select.optimizer_hint, None);
+    assert_eq!(select.optimizer_hints.len(), 1);
+    assert_eq!(select.optimizer_hints[0].text, "four five six");
 
     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_hints.len(), 1);
     assert_eq!(
-        select
-            .optimizer_hint
-            .as_ref()
-            .map(|hint| hint.text.as_str()),
-        Some(" one two three /* asdf */\n")
+        select.optimizer_hints[0].text,
+        " one two three /* asdf */\n"
     );
+    assert_eq!(select.optimizer_hints[0].prefix, "");
 
     // inserts
     oracle_dialect.verified_stmt("INSERT /*+ append */ INTO t1 SELECT * FROM 
all_objects");
@@ -387,6 +385,15 @@ fn test_optimizer_hints() {
                (pt.person_id, pt.first_name, pt.last_name, pt.title) \
                VALUES (ps.person_id, ps.first_name, ps.last_name, ps.title)",
     );
+
+    // single-line prefixed hint (Oracle supports `--` without trailing 
whitespace)
+    let select = oracle_dialect.verified_only_select_with_canonical(
+        "SELECT --abc+ text\n 1 FROM dual",
+        "SELECT --abc+ text\n 1 FROM dual",
+    );
+    assert_eq!(select.optimizer_hints.len(), 1);
+    assert_eq!(select.optimizer_hints[0].prefix, "abc");
+    assert_eq!(select.optimizer_hints[0].text, " text\n");
 }
 
 #[test]
diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs
index d79e2b83..03517876 100644
--- a/tests/sqlparser_postgres.rs
+++ b/tests/sqlparser_postgres.rs
@@ -1330,7 +1330,7 @@ fn parse_copy_to() {
                 with: None,
                 body: Box::new(SetExpr::Select(Box::new(Select {
                     select_token: AttachedToken::empty(),
-                    optimizer_hint: None,
+                    optimizer_hints: vec![],
                     distinct: None,
                     select_modifiers: None,
                     top: None,
@@ -3112,7 +3112,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,
+                        optimizer_hints: vec![],
                         distinct: None,
                         select_modifiers: None,
                         top: None,
@@ -3140,7 +3140,7 @@ fn parse_array_subquery_expr() {
                     }))),
                     right: Box::new(SetExpr::Select(Box::new(Select {
                         select_token: AttachedToken::empty(),
-                        optimizer_hint: None,
+                        optimizer_hints: vec![],
                         distinct: None,
                         select_modifiers: None,
                         top: None,
@@ -5436,7 +5436,7 @@ fn test_simple_postgres_insert_with_alias() {
         statement,
         Statement::Insert(Insert {
             insert_token: AttachedToken::empty(),
-            optimizer_hint: None,
+            optimizer_hints: vec![],
             or: None,
             ignore: false,
             into: true,
@@ -5512,7 +5512,7 @@ fn test_simple_postgres_insert_with_alias() {
         statement,
         Statement::Insert(Insert {
             insert_token: AttachedToken::empty(),
-            optimizer_hint: None,
+            optimizer_hints: vec![],
             or: None,
             ignore: false,
             into: true,
@@ -5590,7 +5590,7 @@ fn test_simple_insert_with_quoted_alias() {
         statement,
         Statement::Insert(Insert {
             insert_token: AttachedToken::empty(),
-            optimizer_hint: None,
+            optimizer_hints: vec![],
             or: None,
             ignore: false,
             into: true,
diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs
index ffe94ab8..a8fa8db2 100644
--- a/tests/sqlparser_sqlite.rs
+++ b/tests/sqlparser_sqlite.rs
@@ -477,7 +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,
+            optimizer_hints: vec![],
             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