This is an automated email from the ASF dual-hosted git repository.
github-bot pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git
The following commit(s) were added to refs/heads/main by this push:
new e81eb14a Allow custom OptimizerHints (#2216)
e81eb14a is described below
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]