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]
