From 6dd814a2deb05902ec16557d4273e79eb67e6e77 Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Tue, 13 Jan 2026 21:27:13 +0100 Subject: [PATCH 01/13] [MySQL, Oracle] Parse optimizer hints for SELECTs --- src/ast/mod.rs | 51 +++++++++++++++++++++++++++++ src/ast/query.rs | 10 ++++++ src/ast/spans.rs | 1 + src/parser/mod.rs | 61 +++++++++++++++++++++++++++++++++++ tests/sqlparser_bigquery.rs | 2 ++ tests/sqlparser_clickhouse.rs | 1 + tests/sqlparser_common.rs | 10 ++++++ tests/sqlparser_duckdb.rs | 2 ++ tests/sqlparser_mssql.rs | 3 ++ tests/sqlparser_mysql.rs | 26 +++++++++++++-- tests/sqlparser_oracle.rs | 35 ++++++++++++++++++++ tests/sqlparser_postgres.rs | 3 ++ 12 files changed, 202 insertions(+), 3 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 33f99bc26e..137cc1b9a8 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 std::fmt::Formatter<'_>) -> std::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 7ea4de19e4..08448cabef 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 126e587a88..2c768eaf8a 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2233,6 +2233,7 @@ impl Spanned for Select { fn span(&self) -> Span { let Select { select_token, + optimizer_hint: _, distinct: _, // todo top: _, // todo, mysql specific projection, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index e071201740..93e28b2377 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. @@ -13837,6 +13842,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, @@ -13864,6 +13870,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; @@ -14018,6 +14025,7 @@ impl<'a> Parser<'a> { Ok(Select { select_token: AttachedToken(select_token), + optimizer_hint, distinct, top, top_before_distinct, @@ -14046,6 +14054,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); diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index d8c3ada1d1..fb28b4d217 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 44bfcda426..ac31a27835 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 6da4ea534e..a71bd9bb99 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -472,6 +472,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, @@ -5806,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, @@ -6536,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, @@ -8912,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())], @@ -9913,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, @@ -12316,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, @@ -12624,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, @@ -12706,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, @@ -13639,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, @@ -15778,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 bdfe4f50a2..7cc710de28 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 1927b864e0..7ef4ce85c2 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 4a62053867..748987e26c 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, @@ -3216,6 +3219,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 +3543,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 +4386,18 @@ fn test_create_index_options() { "CREATE INDEX idx_name ON t(c1, c2) USING BTREE LOCK = EXCLUSIVE ALGORITHM = DEFAULT", ); } + +#[test] +fn test_select_optimizer_hints() { + mysql_and_generic().verified_stmt( + "\ + SELECT /*+ SET_VAR(optimizer_switch = 'mrr_cost_based=off') \ + SET_VAR(max_heap_table_size = 1G) */ 1", + ); + + mysql_and_generic().verified_stmt( + "\ + SELECT /*+ SET_VAR(target_partitions=1) */ * FROM \ + (SELECT /*+ SET_VAR(target_partitions=8) */ * FROM t1 LIMIT 1) AS dt", + ); +} diff --git a/tests/sqlparser_oracle.rs b/tests/sqlparser_oracle.rs index 683660369a..c1caca39e6 100644 --- a/tests/sqlparser_oracle.rs +++ b/tests/sqlparser_oracle.rs @@ -333,3 +333,38 @@ fn parse_national_quote_delimited_string_but_is_a_word() { expr_from_projection(&select.projection[2]) ); } + +#[test] +fn parse_optimizer_hints() { + let oracle_dialect = oracle(); + + 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") + ); +} diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 7bd7f43c6f..01d37f2007 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, From 1f0eae665243784c3a1f015444fc7192febab541 Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Wed, 14 Jan 2026 08:13:45 +0100 Subject: [PATCH 02/13] [MySQL, Oracle] Parse optimizer hints for INSERTs --- src/ast/dml.rs | 26 +++++++++++++++++--------- src/ast/spans.rs | 1 + src/parser/mod.rs | 2 ++ tests/sqlparser_mysql.rs | 18 +++++++++++++++--- tests/sqlparser_oracle.rs | 8 +++++++- tests/sqlparser_postgres.rs | 3 +++ 6 files changed, 45 insertions(+), 13 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 32c023e057..f8b40b17d7 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -25,15 +25,12 @@ use serde::{Deserialize, Serialize}; use sqlparser_derive::{Visit, VisitMut}; use crate::{ - ast::display_separated, - display_utils::{indented_list, Indent, SpaceOrNewline}, + ast::{display_separated}, + display_utils::{Indent, SpaceOrNewline, indented_list}, }; 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, + Assignment, Expr, FromTable, Ident, InsertAliases, MysqlInsertPriority, ObjectName, OnInsert, OptimizerHint, OrderByExpr, Query, SelectInto, SelectItem, Setting, SqliteOnConflict, TableFactor, TableObject, TableWithJoins, UpdateTableFromKind, Values, display_comma_separated, helpers::attached_token::AttachedToken, query::InputFormatClause }; /// INSERT statement. @@ -43,6 +40,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 +104,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 +117,10 @@ 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}",)?; } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 2c768eaf8a..2ed235f475 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1290,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 diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 93e28b2377..811065a304 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -16801,6 +16801,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 @@ -16970,6 +16971,7 @@ impl<'a> Parser<'a> { Ok(Insert { insert_token: insert_token.into(), + optimizer_hint, or, table: table_object, table_alias, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 748987e26c..5206c4116a 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -4388,16 +4388,28 @@ fn test_create_index_options() { } #[test] -fn test_select_optimizer_hints() { - mysql_and_generic().verified_stmt( +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_and_generic().verified_stmt( + 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')"); } diff --git a/tests/sqlparser_oracle.rs b/tests/sqlparser_oracle.rs index c1caca39e6..c9bdce3919 100644 --- a/tests/sqlparser_oracle.rs +++ b/tests/sqlparser_oracle.rs @@ -335,9 +335,10 @@ fn parse_national_quote_delimited_string_but_is_a_word() { } #[test] -fn parse_optimizer_hints() { +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", @@ -367,4 +368,9 @@ fn parse_optimizer_hints() { .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"); + } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 01d37f2007..a449eebc06 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5387,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, @@ -5458,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, @@ -5531,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, From 2a14c5adc2755ea793ca0f3f61b1449b620d9c04 Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Wed, 14 Jan 2026 09:15:24 +0100 Subject: [PATCH 03/13] [MySQL, Oracle] Parse optimizer hints for UPDATEs --- src/ast/dml.rs | 9 +++++++++ src/ast/spans.rs | 1 + src/parser/mod.rs | 2 ++ tests/sqlparser_common.rs | 2 ++ tests/sqlparser_mysql.rs | 7 +++++++ tests/sqlparser_oracle.rs | 3 +++ tests/sqlparser_sqlite.rs | 1 + 7 files changed, 25 insertions(+) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index f8b40b17d7..3375b6c9d6 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -265,6 +265,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 @@ -284,6 +289,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(" ")?; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 2ed235f475..0cdf9d5fb8 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -927,6 +927,7 @@ impl Spanned for Update { fn span(&self) -> Span { let Update { update_token, + optimizer_hint: _, table, assignments, from, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 811065a304..d6df741689 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -17075,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 { + 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) { @@ -17110,6 +17111,7 @@ impl<'a> Parser<'a> { }; Ok(Update { update_token: update_token.into(), + optimizer_hint, table, assignments, from, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a71bd9bb99..bdca204076 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![], @@ -551,6 +552,7 @@ fn parse_update_with_table_alias() { returning, or: None, limit: None, + optimizer_hint: None, update_token: _, }) => { assert_eq!( diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 5206c4116a..5451bb7a18 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -2654,6 +2654,7 @@ fn parse_update_with_joins() { returning, or: None, limit: None, + optimizer_hint: None, update_token: _, }) => { assert_eq!( @@ -4412,4 +4413,10 @@ fn test_optimizer_hints() { 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"); } diff --git a/tests/sqlparser_oracle.rs b/tests/sqlparser_oracle.rs index c9bdce3919..f6737321bc 100644 --- a/tests/sqlparser_oracle.rs +++ b/tests/sqlparser_oracle.rs @@ -373,4 +373,7 @@ fn test_optimizer_hints() { 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"); } diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 321cfef073..da311ac064 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![ From 9e55b6d4a0f9691edec06b7e0d6f0d5f0e0e8beb Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Wed, 14 Jan 2026 09:21:13 +0100 Subject: [PATCH 04/13] [MySQL, Oracle] Parse optimizer hints for DELETEs --- src/ast/dml.rs | 9 +++++++++ src/ast/spans.rs | 1 + src/parser/mod.rs | 2 ++ tests/sqlparser_mysql.rs | 4 ++++ tests/sqlparser_oracle.rs | 4 ++++ 5 files changed, 20 insertions(+) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 3375b6c9d6..94071eebfb 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -196,6 +196,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 @@ -215,6 +220,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)?; } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 0cdf9d5fb8..faa79d8b99 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, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d6df741689..1bfc184dfd 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13029,6 +13029,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 @@ -13072,6 +13073,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) diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 5451bb7a18..bd93d6b131 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -4419,4 +4419,8 @@ fn test_optimizer_hints() { 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 f6737321bc..387261e960 100644 --- a/tests/sqlparser_oracle.rs +++ b/tests/sqlparser_oracle.rs @@ -376,4 +376,8 @@ fn test_optimizer_hints() { // ~ 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"); } From dc492871ef961410bedacb63dc433b3843e488e0 Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Wed, 14 Jan 2026 09:43:17 +0100 Subject: [PATCH 05/13] [MySQL, Oracle] Parse optimizer hints for MERGEs --- src/ast/dml.rs | 19 +++++++++++++------ src/ast/spans.rs | 1 + src/parser/merge.rs | 2 ++ tests/sqlparser_oracle.rs | 9 +++++++++ 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 94071eebfb..5cca998243 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -348,6 +348,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 @@ -364,13 +368,16 @@ pub struct Merge { impl Display for Merge { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "MERGE{int} {table} USING {source} ", - int = if self.into { " INTO" } else { "" }, + f.write_str("MERGE")?; + if let Some(hint) = self.optimizer_hint.as_ref() { + write!(f, " {hint}")?; + } + if self.into { + write!(f, " INTO")?; + } + write!(f, " {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, " "))?; if let Some(ref output) = self.output { diff --git a/src/ast/spans.rs b/src/ast/spans.rs index faa79d8b99..60c983fa1e 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2823,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/parser/merge.rs b/src/parser/merge.rs index 62da68a201..b2f5f8c1d6 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 { + let optimizer_hint = self.parse_optional_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/tests/sqlparser_oracle.rs b/tests/sqlparser_oracle.rs index 387261e960..f137c21166 100644 --- a/tests/sqlparser_oracle.rs +++ b/tests/sqlparser_oracle.rs @@ -380,4 +380,13 @@ fn test_optimizer_hints() { // ~ 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)"); } From 1c15d27b87105ddcda94341a9115fe34e41e46b2 Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Wed, 14 Jan 2026 09:43:51 +0100 Subject: [PATCH 06/13] Cargo fmt --- src/ast/dml.rs | 19 +++++++++++++------ tests/sqlparser_mysql.rs | 24 ++++++++++++++++-------- tests/sqlparser_oracle.rs | 12 +++++------- 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index 5cca998243..4c36f70599 100644 --- a/src/ast/dml.rs +++ b/src/ast/dml.rs @@ -25,12 +25,15 @@ use serde::{Deserialize, Serialize}; use sqlparser_derive::{Visit, VisitMut}; use crate::{ - ast::{display_separated}, - display_utils::{Indent, SpaceOrNewline, indented_list}, + ast::display_separated, + display_utils::{indented_list, Indent, SpaceOrNewline}, }; use super::{ - Assignment, Expr, FromTable, Ident, InsertAliases, MysqlInsertPriority, ObjectName, OnInsert, OptimizerHint, OrderByExpr, Query, SelectInto, SelectItem, Setting, SqliteOnConflict, TableFactor, TableObject, TableWithJoins, UpdateTableFromKind, Values, display_comma_separated, helpers::attached_token::AttachedToken, query::InputFormatClause + display_comma_separated, helpers::attached_token::AttachedToken, query::InputFormatClause, + Assignment, Expr, FromTable, Ident, InsertAliases, MysqlInsertPriority, ObjectName, OnInsert, + OptimizerHint, OrderByExpr, Query, SelectInto, SelectItem, Setting, SqliteOnConflict, + TableFactor, TableObject, TableWithJoins, UpdateTableFromKind, Values, }; /// INSERT statement. @@ -117,7 +120,8 @@ impl Display for Insert { "REPLACE" } else { "INSERT" - })?; + } + )?; if let Some(hint) = self.optimizer_hint.as_ref() { write!(f, " {hint}")?; } @@ -375,9 +379,12 @@ impl Display for Merge { if self.into { write!(f, " INTO")?; } - write!(f, " {table} USING {source} ", + write!( + f, + " {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, " "))?; if let Some(ref output) = self.output { diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index bd93d6b131..80aed5bfee 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -4406,21 +4406,29 @@ fn test_optimizer_hints() { ); // ~ inserts / replace - mysql_dialect.verified_stmt("\ + mysql_dialect.verified_stmt( + "\ INSERT /*+ RESOURCE_GROUP(Batch) */ \ - INTO t2 VALUES (2)"); + INTO t2 VALUES (2)", + ); - mysql_dialect.verified_stmt("\ + mysql_dialect.verified_stmt( + "\ REPLACE /*+ foobar */ INTO test \ - VALUES (1, 'Old', '2014-08-20 18:47:00')"); + VALUES (1, 'Old', '2014-08-20 18:47:00')", + ); // ~ updates - mysql_dialect.verified_stmt("\ + mysql_dialect.verified_stmt( + "\ UPDATE /*+ quux */ table_name \ SET column1 = 1 \ - WHERE 1 = 1"); + WHERE 1 = 1", + ); // ~ deletes - mysql_dialect.verified_stmt("\ - DELETE /*+ foobar */ FROM table_name"); + mysql_dialect.verified_stmt( + "\ + DELETE /*+ foobar */ FROM table_name", + ); } diff --git a/tests/sqlparser_oracle.rs b/tests/sqlparser_oracle.rs index f137c21166..1c12f868f4 100644 --- a/tests/sqlparser_oracle.rs +++ b/tests/sqlparser_oracle.rs @@ -370,16 +370,13 @@ fn test_optimizer_hints() { ); // ~ inserts - oracle_dialect.verified_stmt( - "INSERT /*+ append */ INTO t1 SELECT * FROM all_objects"); + 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"); + 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"); + oracle_dialect.verified_stmt("DELETE --+ ENABLE_PARALLEL_DML\n FROM table_name"); // ~ merges oracle_dialect.verified_stmt( @@ -388,5 +385,6 @@ fn test_optimizer_hints() { 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)"); + VALUES (ps.person_id, ps.first_name, ps.last_name, ps.title)", + ); } From f72cc93f6d785354d34dccc0f1915aba7e72cf0f Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Wed, 14 Jan 2026 11:26:50 +0100 Subject: [PATCH 07/13] Fix no_std compilation --- src/ast/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 137cc1b9a8..f255e5f3f7 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -11723,7 +11723,7 @@ pub enum OptimizerHintStyle { } impl fmt::Display for OptimizerHint { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.style { OptimizerHintStyle::SingleLine { prefix } => { f.write_str(prefix)?; From feff0e30aeddde1c6c10eb8a7565eb9781ea185d Mon Sep 17 00:00:00 2001 From: xitep Date: Mon, 26 Jan 2026 11:08:12 +0100 Subject: [PATCH 08/13] Update src/parser/mod.rs Co-authored-by: Ifeanyi Ubah --- src/parser/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 1bfc184dfd..fd80c7576e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14097,9 +14097,9 @@ impl<'a> Parser<'a> { None }); } - // ~ but skip (pure) whitespace + // but skip (pure) whitespace Whitespace::Space | Whitespace::Tab | Whitespace::Newline => { - // ~ consume the token and try with the next whitespace (if any) + // consume the token and try with the next whitespace (if any) self.next_token_no_skip(); } } From f0f1e708281c863f7567eb7680327bb03befc28f Mon Sep 17 00:00:00 2001 From: xitep Date: Mon, 26 Jan 2026 11:08:31 +0100 Subject: [PATCH 09/13] Update src/parser/mod.rs Co-authored-by: Ifeanyi Ubah --- src/parser/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index fd80c7576e..b3bddf26f7 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14076,7 +14076,7 @@ impl<'a> Parser<'a> { 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 + self.next_token_no_skip(); // Consume the comment token Some(OptimizerHint { text, style: OptimizerHintStyle::SingleLine { prefix }, From 52fecfc02bd069b76a9e2fedb8cf3803a8507b34 Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Mon, 26 Jan 2026 11:23:41 +0100 Subject: [PATCH 10/13] Revise comments --- src/parser/mod.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b3bddf26f7..7d30ba2f13 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14069,7 +14069,6 @@ impl<'a> Parser<'a> { 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 } => { @@ -14088,7 +14087,7 @@ impl<'a> Parser<'a> { 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 + self.next_token_no_skip(); // Consume the comment token Some(OptimizerHint { text, style: OptimizerHintStyle::MultiLine, @@ -14097,9 +14096,8 @@ impl<'a> Parser<'a> { None }); } - // but skip (pure) whitespace Whitespace::Space | Whitespace::Tab | Whitespace::Newline => { - // consume the token and try with the next whitespace (if any) + // Consume the token and try with the next whitespace or comment self.next_token_no_skip(); } } From f261c898155c46f718111c47b97142fe401cf2cb Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Mon, 26 Jan 2026 11:26:38 +0100 Subject: [PATCH 11/13] Rename method --- src/parser/merge.rs | 2 +- src/parser/mod.rs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/parser/merge.rs b/src/parser/merge.rs index b2f5f8c1d6..31f435f8f9 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 { - let optimizer_hint = self.parse_optional_optimizer_hint()?; + let optimizer_hint = self.maybe_parse_optimizer_hint()?; let into = self.parse_keyword(Keyword::INTO); let table = self.parse_table_factor()?; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7d30ba2f13..39f63e467b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13029,7 +13029,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 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 @@ -13872,7 +13872,7 @@ impl<'a> Parser<'a> { } let select_token = self.expect_keyword(Keyword::SELECT)?; - let optimizer_hint = self.parse_optional_optimizer_hint()?; + let optimizer_hint = self.maybe_parse_optimizer_hint()?; let value_table_mode = self.parse_value_table_mode()?; let mut top_before_distinct = false; @@ -14060,7 +14060,7 @@ impl<'a> Parser<'a> { /// /// [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> { + fn maybe_parse_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 { @@ -16801,7 +16801,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 optimizer_hint = self.maybe_parse_optimizer_hint()?; let or = self.parse_conflict_clause(); let priority = if !dialect_of!(self is MySqlDialect | GenericDialect) { None @@ -17075,7 +17075,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 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) { From f80afd0db060b9e5e5a235d9aea90c877bb6339a Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Mon, 26 Jan 2026 11:47:05 +0100 Subject: [PATCH 12/13] Guard optimizer hints parsing by an explicit dialect method --- src/dialect/generic.rs | 4 ++++ src/dialect/mod.rs | 10 ++++++++++ src/dialect/mysql.rs | 4 ++++ src/dialect/oracle.rs | 4 ++++ src/parser/mod.rs | 45 ++++++++++++++++++------------------------ 5 files changed, 41 insertions(+), 26 deletions(-) diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index d460c5237c..345d63fe40 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 98ec93da41..8cff4d23ec 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 81aa9d445a..b44001fe12 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 54c2ace5fb..7ff9326282 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/mod.rs b/src/parser/mod.rs index 39f63e467b..4317dfa6cd 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14061,9 +14061,8 @@ impl<'a> Parser<'a> { /// [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, 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 { + let supports_hints = self.dialect.supports_comment_optimizer_hint(); + if !supports_hints { return Ok(None); } loop { @@ -14071,29 +14070,23 @@ impl<'a> Parser<'a> { match &t.token { 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 comment 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 comment token - Some(OptimizerHint { - text, - style: OptimizerHintStyle::MultiLine, - }) - } else { - None + 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 => { From df67cd67f588948b23953b9778d83a219a23c94b Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Mon, 26 Jan 2026 12:45:40 +0100 Subject: [PATCH 13/13] Cargo fmt --- src/parser/mod.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 4317dfa6cd..10e22256d1 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14077,11 +14077,16 @@ impl<'a> Parser<'a> { Some(text) => { let hint = OptimizerHint { text: text.into(), - style: if let Whitespace::SingleLineComment { prefix, .. } = ws { - OptimizerHintStyle::SingleLine { prefix: prefix.clone() } + style: if let Whitespace::SingleLineComment { + prefix, .. + } = ws + { + OptimizerHintStyle::SingleLine { + prefix: prefix.clone(), + } } else { OptimizerHintStyle::MultiLine - } + }, }; // Consume the comment token self.next_token_no_skip();