Skip to content

Commit 6dd814a

Browse files
committed
[MySQL, Oracle] Parse optimizer hints for SELECTs
1 parent 802c7d3 commit 6dd814a

12 files changed

Lines changed: 202 additions & 3 deletions

src/ast/mod.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11688,6 +11688,57 @@ pub struct ResetStatement {
1168811688
pub reset: Reset,
1168911689
}
1169011690

11691+
/// Query optimizer hints are optionally supported comments after the
11692+
/// `SELECT`, `INSERT`, `UPDATE`, `REPLACE`, `MERGE`, and `DELETE` keywords in
11693+
/// the corresponding statements.
11694+
///
11695+
/// See [Select::optimizer_hint]
11696+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
11697+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
11698+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
11699+
pub struct OptimizerHint {
11700+
/// the raw test of the optimizer hint without its markers
11701+
pub text: String,
11702+
/// the style of the comment which `text` was extracted from,
11703+
/// e.g. `/*+...*/` or `--+...`
11704+
///
11705+
/// Not all dialects support all styles, though.
11706+
pub style: OptimizerHintStyle,
11707+
}
11708+
11709+
/// The commentary style of an [optimizer hint](OptimizerHint)
11710+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
11711+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
11712+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
11713+
pub enum OptimizerHintStyle {
11714+
/// A hint corresponding to a single line comment,
11715+
/// e.g. `--+ LEADING(v.e v.d t)`
11716+
SingleLine {
11717+
/// the comment prefix, e.g. `--`
11718+
prefix: String,
11719+
},
11720+
/// A hint corresponding to a multi line comment,
11721+
/// e.g. `/*+ LEADING(v.e v.d t) */`
11722+
MultiLine,
11723+
}
11724+
11725+
impl fmt::Display for OptimizerHint {
11726+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
11727+
match &self.style {
11728+
OptimizerHintStyle::SingleLine { prefix } => {
11729+
f.write_str(prefix)?;
11730+
f.write_str("+")?;
11731+
f.write_str(&self.text)
11732+
}
11733+
OptimizerHintStyle::MultiLine => {
11734+
f.write_str("/*+")?;
11735+
f.write_str(&self.text)?;
11736+
f.write_str("*/")
11737+
}
11738+
}
11739+
}
11740+
}
11741+
1169111742
impl fmt::Display for ResetStatement {
1169211743
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1169311744
match &self.reset {

src/ast/query.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,11 @@ pub enum SelectFlavor {
343343
pub struct Select {
344344
/// Token for the `SELECT` keyword
345345
pub select_token: AttachedToken,
346+
/// A query optimizer hint
347+
///
348+
/// [MySQL](https://dev.mysql.com/doc/refman/8.4/en/optimizer-hints.html)
349+
/// [Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Comments.html#GUID-D316D545-89E2-4D54-977F-FC97815CD62E)
350+
pub optimizer_hint: Option<OptimizerHint>,
346351
/// `SELECT [DISTINCT] ...`
347352
pub distinct: Option<Distinct>,
348353
/// MSSQL syntax: `TOP (<N>) [ PERCENT ] [ WITH TIES ]`
@@ -410,6 +415,11 @@ impl fmt::Display for Select {
410415
}
411416
}
412417

418+
if let Some(hint) = self.optimizer_hint.as_ref() {
419+
f.write_str(" ")?;
420+
hint.fmt(f)?;
421+
}
422+
413423
if let Some(value_table_mode) = self.value_table_mode {
414424
f.write_str(" ")?;
415425
value_table_mode.fmt(f)?;

src/ast/spans.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2233,6 +2233,7 @@ impl Spanned for Select {
22332233
fn span(&self) -> Span {
22342234
let Select {
22352235
select_token,
2236+
optimizer_hint: _,
22362237
distinct: _, // todo
22372238
top: _, // todo, mysql specific
22382239
projection,

src/parser/mod.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4325,6 +4325,11 @@ impl<'a> Parser<'a> {
43254325
})
43264326
}
43274327

4328+
/// Return nth token, possibly whitespace, that has not yet been processed.
4329+
fn peek_nth_token_no_skip_ref(&self, n: usize) -> &TokenWithSpan {
4330+
self.tokens.get(self.index + n).unwrap_or(&EOF_TOKEN)
4331+
}
4332+
43284333
/// Return true if the next tokens exactly `expected`
43294334
///
43304335
/// Does not advance the current token.
@@ -13837,6 +13842,7 @@ impl<'a> Parser<'a> {
1383713842
if !self.peek_keyword(Keyword::SELECT) {
1383813843
return Ok(Select {
1383913844
select_token: AttachedToken(from_token),
13845+
optimizer_hint: None,
1384013846
distinct: None,
1384113847
top: None,
1384213848
top_before_distinct: false,
@@ -13864,6 +13870,7 @@ impl<'a> Parser<'a> {
1386413870
}
1386513871

1386613872
let select_token = self.expect_keyword(Keyword::SELECT)?;
13873+
let optimizer_hint = self.parse_optional_optimizer_hint()?;
1386713874
let value_table_mode = self.parse_value_table_mode()?;
1386813875

1386913876
let mut top_before_distinct = false;
@@ -14018,6 +14025,7 @@ impl<'a> Parser<'a> {
1401814025

1401914026
Ok(Select {
1402014027
select_token: AttachedToken(select_token),
14028+
optimizer_hint,
1402114029
distinct,
1402214030
top,
1402314031
top_before_distinct,
@@ -14046,6 +14054,59 @@ impl<'a> Parser<'a> {
1404614054
})
1404714055
}
1404814056

14057+
/// Parses an optional optimizer hint at the current token position
14058+
///
14059+
/// [MySQL](https://dev.mysql.com/doc/refman/8.4/en/optimizer-hints.html#optimizer-hints-overview)
14060+
/// [Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Comments.html#GUID-D316D545-89E2-4D54-977F-FC97815CD62E)
14061+
fn parse_optional_optimizer_hint(&mut self) -> Result<Option<OptimizerHint>, ParserError> {
14062+
let supports_multiline = dialect_of!(self is MySqlDialect | OracleDialect | GenericDialect);
14063+
let supports_singleline = dialect_of!(self is OracleDialect | GenericDialect);
14064+
if !supports_multiline && !supports_singleline {
14065+
return Ok(None);
14066+
}
14067+
loop {
14068+
let t = self.peek_nth_token_no_skip_ref(0);
14069+
match &t.token {
14070+
// ~ only the very first comment
14071+
Token::Whitespace(ws) => {
14072+
match ws {
14073+
Whitespace::SingleLineComment { comment, prefix } => {
14074+
return Ok(if supports_singleline && comment.starts_with("+") {
14075+
let text = comment.split_at(1).1.into();
14076+
let prefix = prefix.clone();
14077+
self.next_token_no_skip(); // ~ consume the token
14078+
Some(OptimizerHint {
14079+
text,
14080+
style: OptimizerHintStyle::SingleLine { prefix },
14081+
})
14082+
} else {
14083+
None
14084+
});
14085+
}
14086+
Whitespace::MultiLineComment(comment) => {
14087+
return Ok(if supports_multiline && comment.starts_with("+") {
14088+
let text = comment.split_at(1).1.into();
14089+
self.next_token_no_skip(); // ~ consume the token
14090+
Some(OptimizerHint {
14091+
text,
14092+
style: OptimizerHintStyle::MultiLine,
14093+
})
14094+
} else {
14095+
None
14096+
});
14097+
}
14098+
// ~ but skip (pure) whitespace
14099+
Whitespace::Space | Whitespace::Tab | Whitespace::Newline => {
14100+
// ~ consume the token and try with the next whitespace (if any)
14101+
self.next_token_no_skip();
14102+
}
14103+
}
14104+
}
14105+
_ => return Ok(None),
14106+
}
14107+
}
14108+
}
14109+
1404914110
fn parse_value_table_mode(&mut self) -> Result<Option<ValueTableMode>, ParserError> {
1405014111
if !dialect_of!(self is BigQueryDialect) {
1405114112
return Ok(None);

tests/sqlparser_bigquery.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2681,6 +2681,7 @@ fn test_export_data() {
26812681
}),
26822682
Span::empty()
26832683
)),
2684+
optimizer_hint: None,
26842685
distinct: None,
26852686
top: None,
26862687
top_before_distinct: false,
@@ -2785,6 +2786,7 @@ fn test_export_data() {
27852786
}),
27862787
Span::empty()
27872788
)),
2789+
optimizer_hint: None,
27882790
distinct: None,
27892791
top: None,
27902792
top_before_distinct: false,

tests/sqlparser_clickhouse.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ fn parse_map_access_expr() {
4141
assert_eq!(
4242
Select {
4343
distinct: None,
44+
optimizer_hint: None,
4445
select_token: AttachedToken::empty(),
4546
top: None,
4647
top_before_distinct: false,

tests/sqlparser_common.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,7 @@ fn parse_update_set_from() {
472472
with: None,
473473
body: Box::new(SetExpr::Select(Box::new(Select {
474474
select_token: AttachedToken::empty(),
475+
optimizer_hint: None,
475476
distinct: None,
476477
top: None,
477478
top_before_distinct: false,
@@ -5806,6 +5807,7 @@ fn test_parse_named_window() {
58065807
let actual_select_only = dialects.verified_only_select(sql);
58075808
let expected = Select {
58085809
select_token: AttachedToken::empty(),
5810+
optimizer_hint: None,
58095811
distinct: None,
58105812
top: None,
58115813
top_before_distinct: false,
@@ -6536,6 +6538,7 @@ fn parse_interval_and_or_xor() {
65366538
with: None,
65376539
body: Box::new(SetExpr::Select(Box::new(Select {
65386540
select_token: AttachedToken::empty(),
6541+
optimizer_hint: None,
65396542
distinct: None,
65406543
top: None,
65416544
top_before_distinct: false,
@@ -8912,6 +8915,7 @@ fn lateral_function() {
89128915
let actual_select_only = verified_only_select(sql);
89138916
let expected = Select {
89148917
select_token: AttachedToken::empty(),
8918+
optimizer_hint: None,
89158919
distinct: None,
89168920
top: None,
89178921
projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())],
@@ -9913,6 +9917,7 @@ fn parse_merge() {
99139917
with: None,
99149918
body: Box::new(SetExpr::Select(Box::new(Select {
99159919
select_token: AttachedToken::empty(),
9920+
optimizer_hint: None,
99169921
distinct: None,
99179922
top: None,
99189923
top_before_distinct: false,
@@ -12316,6 +12321,7 @@ fn parse_unload() {
1231612321
query: Some(Box::new(Query {
1231712322
body: Box::new(SetExpr::Select(Box::new(Select {
1231812323
select_token: AttachedToken::empty(),
12324+
optimizer_hint: None,
1231912325
distinct: None,
1232012326
top: None,
1232112327
top_before_distinct: false,
@@ -12624,6 +12630,7 @@ fn parse_map_access_expr() {
1262412630
fn parse_connect_by() {
1262512631
let expect_query = Select {
1262612632
select_token: AttachedToken::empty(),
12633+
optimizer_hint: None,
1262712634
distinct: None,
1262812635
top: None,
1262912636
top_before_distinct: false,
@@ -12706,6 +12713,7 @@ fn parse_connect_by() {
1270612713
all_dialects_where(|d| d.supports_connect_by()).verified_only_select(connect_by_3),
1270712714
Select {
1270812715
select_token: AttachedToken::empty(),
12716+
optimizer_hint: None,
1270912717
distinct: None,
1271012718
top: None,
1271112719
top_before_distinct: false,
@@ -13639,6 +13647,7 @@ fn test_extract_seconds_ok() {
1363913647
with: None,
1364013648
body: Box::new(SetExpr::Select(Box::new(Select {
1364113649
select_token: AttachedToken::empty(),
13650+
optimizer_hint: None,
1364213651
distinct: None,
1364313652
top: None,
1364413653
top_before_distinct: false,
@@ -15778,6 +15787,7 @@ fn test_select_from_first() {
1577815787
with: None,
1577915788
body: Box::new(SetExpr::Select(Box::new(Select {
1578015789
select_token: AttachedToken::empty(),
15790+
optimizer_hint: None,
1578115791
distinct: None,
1578215792
top: None,
1578315793
projection,

tests/sqlparser_duckdb.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ fn test_select_union_by_name() {
266266
set_quantifier: *expected_quantifier,
267267
left: Box::<SetExpr>::new(SetExpr::Select(Box::new(Select {
268268
select_token: AttachedToken::empty(),
269+
optimizer_hint: None,
269270
distinct: None,
270271
top: None,
271272
projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())],
@@ -297,6 +298,7 @@ fn test_select_union_by_name() {
297298
}))),
298299
right: Box::<SetExpr>::new(SetExpr::Select(Box::new(Select {
299300
select_token: AttachedToken::empty(),
301+
optimizer_hint: None,
300302
distinct: None,
301303
top: None,
302304
projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())],

tests/sqlparser_mssql.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ fn parse_create_procedure() {
141141
pipe_operators: vec![],
142142
body: Box::new(SetExpr::Select(Box::new(Select {
143143
select_token: AttachedToken::empty(),
144+
optimizer_hint: None,
144145
distinct: None,
145146
top: None,
146147
top_before_distinct: false,
@@ -1348,6 +1349,7 @@ fn parse_substring_in_select() {
13481349

13491350
body: Box::new(SetExpr::Select(Box::new(Select {
13501351
select_token: AttachedToken::empty(),
1352+
optimizer_hint: None,
13511353
distinct: Some(Distinct::Distinct),
13521354
top: None,
13531355
top_before_distinct: false,
@@ -1505,6 +1507,7 @@ fn parse_mssql_declare() {
15051507

15061508
body: Box::new(SetExpr::Select(Box::new(Select {
15071509
select_token: AttachedToken::empty(),
1510+
optimizer_hint: None,
15081511
distinct: None,
15091512
top: None,
15101513
top_before_distinct: false,

0 commit comments

Comments
 (0)