Skip to content

Commit a8332a7

Browse files
committed
[MySQL, Oracle] Parse optimizer hints for SELECTs
1 parent 3880a93 commit a8332a7

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
@@ -11666,6 +11666,57 @@ pub struct ResetStatement {
1166611666
pub reset: Reset,
1166711667
}
1166811668

11669+
/// Query optimizer hints are optionally supported comments after the
11670+
/// `SELECT`, `INSERT`, `UPDATE`, `REPLACE`, `MERGE`, and `DELETE` keywords in
11671+
/// the corresponding statements.
11672+
///
11673+
/// See [Select::optimizer_hint]
11674+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
11675+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
11676+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
11677+
pub struct OptimizerHint {
11678+
/// the raw test of the optimizer hint without its markers
11679+
pub text: String,
11680+
/// the style of the comment which `text` was extracted from,
11681+
/// e.g. `/*+...*/` or `--+...`
11682+
///
11683+
/// Not all dialects support all styles, though.
11684+
pub style: OptimizerHintStyle,
11685+
}
11686+
11687+
/// The commentary style of an [optimizer hint](OptimizerHint)
11688+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
11689+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
11690+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
11691+
pub enum OptimizerHintStyle {
11692+
/// A hint corresponding to a single line comment,
11693+
/// e.g. `--+ LEADING(v.e v.d t)`
11694+
SingleLine {
11695+
/// the comment prefix, e.g. `--`
11696+
prefix: String,
11697+
},
11698+
/// A hint corresponding to a multi line comment,
11699+
/// e.g. `/*+ LEADING(v.e v.d t) */`
11700+
MultiLine,
11701+
}
11702+
11703+
impl fmt::Display for OptimizerHint {
11704+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
11705+
match &self.style {
11706+
OptimizerHintStyle::SingleLine { prefix } => {
11707+
f.write_str(prefix)?;
11708+
f.write_str("+")?;
11709+
f.write_str(&self.text)
11710+
}
11711+
OptimizerHintStyle::MultiLine => {
11712+
f.write_str("/*+")?;
11713+
f.write_str(&self.text)?;
11714+
f.write_str("*/")
11715+
}
11716+
}
11717+
}
11718+
}
11719+
1166911720
impl fmt::Display for ResetStatement {
1167011721
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1167111722
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
@@ -2229,6 +2229,7 @@ impl Spanned for Select {
22292229
fn span(&self) -> Span {
22302230
let Select {
22312231
select_token,
2232+
optimizer_hint: _,
22322233
distinct: _, // todo
22332234
top: _, // todo, mysql specific
22342235
projection,

src/parser/mod.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4283,6 +4283,11 @@ impl<'a> Parser<'a> {
42834283
})
42844284
}
42854285

4286+
/// Return nth token, possibly whitespace, that has not yet been processed.
4287+
fn peek_nth_token_no_skip_ref(&self, n: usize) -> &TokenWithSpan {
4288+
self.tokens.get(self.index + n).unwrap_or(&EOF_TOKEN)
4289+
}
4290+
42864291
/// Return true if the next tokens exactly `expected`
42874292
///
42884293
/// Does not advance the current token.
@@ -13784,6 +13789,7 @@ impl<'a> Parser<'a> {
1378413789
if !self.peek_keyword(Keyword::SELECT) {
1378513790
return Ok(Select {
1378613791
select_token: AttachedToken(from_token),
13792+
optimizer_hint: None,
1378713793
distinct: None,
1378813794
top: None,
1378913795
top_before_distinct: false,
@@ -13811,6 +13817,7 @@ impl<'a> Parser<'a> {
1381113817
}
1381213818

1381313819
let select_token = self.expect_keyword(Keyword::SELECT)?;
13820+
let optimizer_hint = self.parse_optional_optimizer_hint()?;
1381413821
let value_table_mode = self.parse_value_table_mode()?;
1381513822

1381613823
let mut top_before_distinct = false;
@@ -13966,6 +13973,7 @@ impl<'a> Parser<'a> {
1396613973

1396713974
Ok(Select {
1396813975
select_token: AttachedToken(select_token),
13976+
optimizer_hint,
1396913977
distinct,
1397013978
top,
1397113979
top_before_distinct,
@@ -13994,6 +14002,59 @@ impl<'a> Parser<'a> {
1399414002
})
1399514003
}
1399614004

14005+
/// Parses an optional optimizer hint at the current token position
14006+
///
14007+
/// [MySQL](https://dev.mysql.com/doc/refman/8.4/en/optimizer-hints.html#optimizer-hints-overview)
14008+
/// [Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Comments.html#GUID-D316D545-89E2-4D54-977F-FC97815CD62E)
14009+
fn parse_optional_optimizer_hint(&mut self) -> Result<Option<OptimizerHint>, ParserError> {
14010+
let supports_multiline = dialect_of!(self is MySqlDialect | OracleDialect | GenericDialect);
14011+
let supports_singleline = dialect_of!(self is OracleDialect | GenericDialect);
14012+
if !supports_multiline && !supports_singleline {
14013+
return Ok(None);
14014+
}
14015+
loop {
14016+
let t = self.peek_nth_token_no_skip_ref(0);
14017+
match &t.token {
14018+
// ~ only the very first comment
14019+
Token::Whitespace(ws) => {
14020+
match ws {
14021+
Whitespace::SingleLineComment { comment, prefix } => {
14022+
return Ok(if supports_singleline && comment.starts_with("+") {
14023+
let text = comment.split_at(1).1.into();
14024+
let prefix = prefix.clone();
14025+
self.next_token_no_skip(); // ~ consume the token
14026+
Some(OptimizerHint {
14027+
text,
14028+
style: OptimizerHintStyle::SingleLine { prefix },
14029+
})
14030+
} else {
14031+
None
14032+
});
14033+
}
14034+
Whitespace::MultiLineComment(comment) => {
14035+
return Ok(if supports_multiline && comment.starts_with("+") {
14036+
let text = comment.split_at(1).1.into();
14037+
self.next_token_no_skip(); // ~ consume the token
14038+
Some(OptimizerHint {
14039+
text,
14040+
style: OptimizerHintStyle::MultiLine,
14041+
})
14042+
} else {
14043+
None
14044+
});
14045+
}
14046+
// ~ but skip (pure) whitespace
14047+
Whitespace::Space | Whitespace::Tab | Whitespace::Newline => {
14048+
// ~ consume the token and try with the next whitespace (if any)
14049+
self.next_token_no_skip();
14050+
}
14051+
}
14052+
}
14053+
_ => return Ok(None),
14054+
}
14055+
}
14056+
}
14057+
1399714058
fn parse_value_table_mode(&mut self) -> Result<Option<ValueTableMode>, ParserError> {
1399814059
if !dialect_of!(self is BigQueryDialect) {
1399914060
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,
@@ -5794,6 +5795,7 @@ fn test_parse_named_window() {
57945795
let actual_select_only = dialects.verified_only_select(sql);
57955796
let expected = Select {
57965797
select_token: AttachedToken::empty(),
5798+
optimizer_hint: None,
57975799
distinct: None,
57985800
top: None,
57995801
top_before_distinct: false,
@@ -6523,6 +6525,7 @@ fn parse_interval_and_or_xor() {
65236525
with: None,
65246526
body: Box::new(SetExpr::Select(Box::new(Select {
65256527
select_token: AttachedToken::empty(),
6528+
optimizer_hint: None,
65266529
distinct: None,
65276530
top: None,
65286531
top_before_distinct: false,
@@ -8897,6 +8900,7 @@ fn lateral_function() {
88978900
let actual_select_only = verified_only_select(sql);
88988901
let expected = Select {
88998902
select_token: AttachedToken::empty(),
8903+
optimizer_hint: None,
89008904
distinct: None,
89018905
top: None,
89028906
projection: vec![SelectItem::Wildcard(WildcardAdditionalOptions::default())],
@@ -9897,6 +9901,7 @@ fn parse_merge() {
98979901
with: None,
98989902
body: Box::new(SetExpr::Select(Box::new(Select {
98999903
select_token: AttachedToken::empty(),
9904+
optimizer_hint: None,
99009905
distinct: None,
99019906
top: None,
99029907
top_before_distinct: false,
@@ -12299,6 +12304,7 @@ fn parse_unload() {
1229912304
query: Some(Box::new(Query {
1230012305
body: Box::new(SetExpr::Select(Box::new(Select {
1230112306
select_token: AttachedToken::empty(),
12307+
optimizer_hint: None,
1230212308
distinct: None,
1230312309
top: None,
1230412310
top_before_distinct: false,
@@ -12607,6 +12613,7 @@ fn parse_map_access_expr() {
1260712613
fn parse_connect_by() {
1260812614
let expect_query = Select {
1260912615
select_token: AttachedToken::empty(),
12616+
optimizer_hint: None,
1261012617
distinct: None,
1261112618
top: None,
1261212619
top_before_distinct: false,
@@ -12689,6 +12696,7 @@ fn parse_connect_by() {
1268912696
all_dialects_where(|d| d.supports_connect_by()).verified_only_select(connect_by_3),
1269012697
Select {
1269112698
select_token: AttachedToken::empty(),
12699+
optimizer_hint: None,
1269212700
distinct: None,
1269312701
top: None,
1269412702
top_before_distinct: false,
@@ -13619,6 +13627,7 @@ fn test_extract_seconds_ok() {
1361913627
with: None,
1362013628
body: Box::new(SetExpr::Select(Box::new(Select {
1362113629
select_token: AttachedToken::empty(),
13630+
optimizer_hint: None,
1362213631
distinct: None,
1362313632
top: None,
1362413633
top_before_distinct: false,
@@ -15711,6 +15720,7 @@ fn test_select_from_first() {
1571115720
with: None,
1571215721
body: Box::new(SetExpr::Select(Box::new(Select {
1571315722
select_token: AttachedToken::empty(),
15723+
optimizer_hint: None,
1571415724
distinct: None,
1571515725
top: None,
1571615726
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)