From 4e2c3acc4aeb5b388761b5e8e501c400dbd5fc41 Mon Sep 17 00:00:00 2001 From: Marcelo Altmann Date: Thu, 12 Feb 2026 08:28:01 -0300 Subject: [PATCH] Collect multiple optimizer hints per statement and extract comment prefix Two orthogonal improvements to optimizer hint parsing: 1. `Option` -> `Vec`: the old Option silently dropped all but the first hint-style comment. Vec preserves all hint comments the parser encounters, letting consumers decide which to use. This is backwards compatible: `optimizer_hint: None` becomes `optimizer_hints: vec![]`, and `optimizer_hint.unwrap()` becomes `optimizer_hints[0]`. 2. Generic prefix extraction: the `/*+...*/` pattern is an established convention. Various systems extend it with `/*prefix+...*/` where the prefix is opaque alphanumeric text before `+`. Rather than adding a new dialect flag or struct for each system, the parser now captures any `[a-zA-Z0-9]*` run before `+` as a `prefix` field. Standard hints have `prefix: ""`. No new dialect surface -- same `supports_comment_optimizer_hint()` gate. This makes OptimizerHint a generic extension point: downstream consumers can define their own prefixed hint conventions and filter hints by prefix, without requiring any changes to the parser or dialect configuration. --- 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 | 102 +++++++++++++++++++++------------- 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 | 39 +++++++++---- tests/sqlparser_oracle.rs | 35 +++++++----- tests/sqlparser_postgres.rs | 12 ++-- tests/sqlparser_sqlite.rs | 2 +- 16 files changed, 181 insertions(+), 124 deletions(-) diff --git a/src/ast/dml.rs b/src/ast/dml.rs index f9c8823a22..a0be916dee 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, + pub optimizer_hints: Vec, /// Only for Sqlite pub or: Option, /// 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, + pub optimizer_hints: Vec, /// Multi tables delete are supported in mysql pub tables: Vec, /// 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, + pub optimizer_hints: Vec, /// 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, + pub optimizer_hints: Vec, /// 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 601af1bd51..92a4149bcd 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -11579,12 +11579,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 `--+...` @@ -11614,11 +11621,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 b8f605be54..d98141f718 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, + pub optimizer_hints: Vec, /// `SELECT [DISTINCT] ...` pub distinct: Option, /// 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 f4bdf85a39..9b4696a4cd 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -897,7 +897,7 @@ impl Spanned for Delete { fn span(&self) -> Span { let Delete { delete_token, - optimizer_hint: _, + optimizer_hints: _, tables, from, using, @@ -931,7 +931,7 @@ impl Spanned for Update { fn span(&self) -> Span { let Update { update_token, - optimizer_hint: _, + optimizer_hints: _, table, assignments, from, @@ -1295,7 +1295,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 @@ -2243,7 +2243,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 @@ -2837,7 +2837,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 14e4ad45a1..dac9884a49 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -1752,7 +1752,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 31f435f8f9..a927bc4b1c 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.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 7a2bda8aca..2344a1f065 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13134,7 +13134,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.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 @@ -13178,7 +13178,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) @@ -13950,7 +13950,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, @@ -13979,7 +13979,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) = @@ -14138,7 +14138,7 @@ impl<'a> Parser<'a> { Ok(Select { select_token: AttachedToken(select_token), - optimizer_hint, + optimizer_hints, distinct, select_modifiers, top, @@ -14168,53 +14168,75 @@ 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, ParserError> { + fn maybe_parse_optimizer_hints(&mut self) -> Result, 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) - } + Token::Whitespace(ws) => 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(); + } else { + break; } - Whitespace::Space | Whitespace::Tab | Whitespace::Newline => { - // Consume the token and try with the next whitespace or comment + } + 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(); + } else { + break; } } - } - _ => return Ok(None), + Whitespace::Space | Whitespace::Tab | Whitespace::Newline => { + self.next_token_no_skip(); + } + }, + _ => break, } } + 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 plus_pos = comment.find('+')?; + let before_plus = &comment[..plus_pos]; + if before_plus.chars().all(|c| c.is_ascii_alphanumeric()) { + let text = &comment[plus_pos + 1..]; + Some((before_plus.to_string(), text.to_string())) + } else { + None + } } /// Parses MySQL SELECT modifiers and DISTINCT/ALL in any order. @@ -16985,7 +17007,7 @@ impl<'a> Parser<'a> { /// Parse an INSERT statement pub fn parse_insert(&mut self, insert_token: TokenWithSpan) -> Result { - 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 @@ -17155,7 +17177,7 @@ impl<'a> Parser<'a> { Ok(Insert { insert_token: insert_token.into(), - optimizer_hint, + optimizer_hints, or, table: table_object, table_alias, @@ -17263,7 +17285,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.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) { @@ -17299,7 +17321,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 cf843ea2b3..ce962cb807 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 b8b4e33708..82f79577b9 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 899dba8dde..7dd3a76488 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 { @@ -5818,7 +5818,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, @@ -6550,7 +6550,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, @@ -8928,7 +8928,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, @@ -9931,7 +9931,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, @@ -12343,7 +12343,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, @@ -12664,7 +12664,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, @@ -12731,7 +12731,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, @@ -12799,7 +12799,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, @@ -12887,7 +12887,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, @@ -13850,7 +13850,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, @@ -15991,7 +15991,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 b9ae26491f..fb024bfee0 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::::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::::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 82e6f46213..7bf8e6eade 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 1b9d12f8cf..4130285485 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, @@ -4641,6 +4641,25 @@ 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 "); + + // ~ 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 0dbccdb5eb..959ae169b8 100644 --- a/tests/sqlparser_oracle.rs +++ b/tests/sqlparser_oracle.rs @@ -338,36 +338,32 @@ 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*/ /*+not a hint!*/ 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, "not a hint!"); 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); + assert!(select.optimizer_hints.is_empty()); 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 +383,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 f8c7381366..3bf31bf51e 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1291,7 +1291,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, @@ -3073,7 +3073,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, @@ -3101,7 +3101,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, @@ -5397,7 +5397,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, @@ -5473,7 +5473,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, @@ -5551,7 +5551,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 ffe94ab8ad..a8fa8db223 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![