1212
1313//! SQL Parser
1414
15+ #[cfg(not(feature = "std"))]
16+ use alloc::collections::BTreeSet;
1517#[cfg(not(feature = "std"))]
1618use alloc::{
1719 boxed::Box,
@@ -25,6 +27,8 @@ use core::{
2527 str::FromStr,
2628};
2729use helpers::attached_token::AttachedToken;
30+ #[cfg(feature = "std")]
31+ use std::collections::BTreeSet;
2832
2933use log::debug;
3034
@@ -359,6 +363,14 @@ pub struct Parser<'a> {
359363 options: ParserOptions,
360364 /// Ensures the stack does not overflow by limiting recursion depth.
361365 recursion_counter: RecursionCounter,
366+ /// Token indices where a speculative attempt to parse `NOT` as a unary
367+ /// prefix operator has already failed during this parse. Re-entering
368+ /// `parse_not` at the same position would repeat the same work and fail
369+ /// again, so the second visit short-circuits to identifier interpretation.
370+ /// Without this cache, inputs like `SELECT x-not-b.x-not-b...` (chains of
371+ /// `<ident>-NOT-<ident>.` ending in a parse error) trigger 2^N exploration
372+ /// because each `-NOT-` segment otherwise re-walks the rest of the chain.
373+ failed_unary_not_positions: BTreeSet<usize>,
362374}
363375
364376impl<'a> Parser<'a> {
@@ -385,6 +397,7 @@ impl<'a> Parser<'a> {
385397 dialect,
386398 recursion_counter: RecursionCounter::new(DEFAULT_REMAINING_DEPTH),
387399 options: ParserOptions::new().with_trailing_commas(dialect.supports_trailing_commas()),
400+ failed_unary_not_positions: BTreeSet::new(),
388401 }
389402 }
390403
@@ -446,6 +459,7 @@ impl<'a> Parser<'a> {
446459 pub fn with_tokens_with_locations(mut self, tokens: Vec<TokenWithSpan>) -> Self {
447460 self.tokens = tokens;
448461 self.index = 0;
462+ self.failed_unary_not_positions.clear();
449463 self
450464 }
451465
@@ -1801,6 +1815,17 @@ impl<'a> Parser<'a> {
18011815 // We first try to parse the word and following tokens as a special expression, and if that fails,
18021816 // we rollback and try to parse it as an identifier.
18031817 let w = w.clone();
1818+ // If we already tried to parse `NOT` as a unary prefix operator
1819+ // at this exact position and it failed, skip the speculative
1820+ // path and fall back to identifier interpretation directly. The
1821+ // speculative `parse_not` re-walks the rest of the expression,
1822+ // so repeating it on every visit causes 2^N work on chains
1823+ // like `x-not-b.x-not-b...` that end in a parse error.
1824+ if w.keyword == Keyword::NOT
1825+ && self.failed_unary_not_positions.contains(&self.index)
1826+ {
1827+ return self.parse_expr_prefix_by_unreserved_word(&w, span);
1828+ }
18041829 match self.try_parse(|parser| parser.parse_expr_prefix_by_reserved_word(&w, span)) {
18051830 // This word indicated an expression prefix and parsing was successful
18061831 Ok(Some(expr)) => Ok(expr),
@@ -1815,6 +1840,9 @@ impl<'a> Parser<'a> {
18151840 // we rollback and return the parsing error we got from trying to parse a
18161841 // special expression (to maintain backwards compatibility of parsing errors).
18171842 Err(e) => {
1843+ if w.keyword == Keyword::NOT {
1844+ self.failed_unary_not_positions.insert(self.index);
1845+ }
18181846 if !self.dialect.is_reserved_for_identifier(w.keyword) {
18191847 if let Ok(Some(expr)) = self.maybe_parse(|parser| {
18201848 parser.parse_expr_prefix_by_unreserved_word(&w, span)
0 commit comments