From 5db96541f7f8606628416e93e47b25c6d382960a Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Wed, 23 Sep 2026 12:37:55 +0200 Subject: [PATCH 1/4] SQLite: Parse the `BETWEEN` lower bound up to its `AND` --- src/dialect/mod.rs | 5 +++++ src/dialect/sqlite.rs | 5 +++++ src/parser/mod.rs | 4 +--- tests/sqlparser_sqlite.rs | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index c867b76f61..031271b87d 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1144,6 +1144,11 @@ pub trait Dialect: Debug + Any { 0 } + /// Precedence used to stop parsing the low bound expression of a BETWEEN predicate. + fn between_low_bound_precedence(&self) -> u8 { + self.prec_value(Precedence::Between) + } + /// Returns true if this dialect requires the `TABLE` keyword after `DESCRIBE` /// /// Defaults to false. diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index 44dcc9d267..0ca50050ed 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -244,6 +244,11 @@ impl Dialect for SQLiteDialect { fn supports_bitwise_shift_operators(&self) -> bool { true } + + /// Comparison and postfix operators bind within the BETWEEN low bound in SQLite (). + fn between_low_bound_precedence(&self) -> u8 { + self.prec_value(Precedence::And) + } } /// SQLite permits signed and decimal numeric type modifiers. diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3efb22a349..ed5a5c1822 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4592,9 +4592,7 @@ impl<'a> Parser<'a> { /// Parses `BETWEEN AND `, assuming the `BETWEEN` keyword was already consumed. pub fn parse_between(&mut self, expr: Expr, negated: bool) -> Result { - // Stop parsing subexpressions for and on tokens with - // precedence lower than that of `BETWEEN`, such as `AND`, `IS`, etc. - let low = self.parse_subexpr(self.dialect.prec_value(Precedence::Between))?; + let low = self.parse_subexpr(self.dialect.between_low_bound_precedence())?; self.expect_keyword_is(Keyword::AND)?; let high = self.parse_subexpr(self.dialect.prec_value(Precedence::Between))?; Ok(Expr::Between { diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index ad80e95a93..da2b1e82d6 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -1489,6 +1489,38 @@ fn parse_on_conflict_target_predicate() { } } +fn parse_between_low_bound_precedence() { + match sqlite().verified_expr("a BETWEEN b > 1 AND c") { + Expr::Between { low, .. } => assert!( + matches!( + *low, + Expr::BinaryOp { + op: BinaryOperator::Gt, + .. + } + ), + "{low:?}" + ), + other => panic!("expected BETWEEN, got {other:?}"), + } + sqlite().verified_expr("a BETWEEN b LIKE 'x' AND c"); + sqlite().verified_expr("a BETWEEN b IS NOT NULL AND c"); + sqlite().expr_parses_to("a BETWEEN b NOTNULL AND c", "a BETWEEN b IS NOT NULL AND c"); + sqlite().verified_expr("a BETWEEN b BETWEEN 1 AND 2 AND c"); + + // Operators after the high bound apply to the whole BETWEEN. + sqlite().verified_expr("5 BETWEEN 1 AND NULL IS NULL"); + sqlite().verified_expr("5 BETWEEN 1 AND 10 = 1"); + + assert_eq!( + TestedDialects::new(vec![Box::new(GenericDialect {})]) + .parse_sql_statements("SELECT a BETWEEN b > 1 AND c FROM t"), + Err(ParserError::ParserError( + "Expected: AND, found: >".to_string() + )) + ); +} + fn sqlite() -> TestedDialects { TestedDialects::new(vec![Box::new(SQLiteDialect {})]) } From 17ea1ac6324efada4a50ec1287463cba1fd3da74 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Tue, 20 Oct 2026 12:51:20 +0200 Subject: [PATCH 2/4] Cover malformed parenthesized `BETWEEN` low bounds --- tests/sqlparser_sqlite.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index da2b1e82d6..d5ac49807c 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -1512,6 +1512,14 @@ fn parse_between_low_bound_precedence() { sqlite().verified_expr("5 BETWEEN 1 AND NULL IS NULL"); sqlite().verified_expr("5 BETWEEN 1 AND 10 = 1"); + // The inner AND belongs to the bound expression, so the missing `)` is reported at FROM. + assert_eq!( + sqlite().parse_sql_statements("SELECT a BETWEEN (1 AND 2 FROM t"), + Err(ParserError::ParserError( + "Expected: ), found: FROM".to_string() + )) + ); + assert_eq!( TestedDialects::new(vec![Box::new(GenericDialect {})]) .parse_sql_statements("SELECT a BETWEEN b > 1 AND c FROM t"), From 2379d70d658cb65bb8c2a85ef4584b6fe2edc8b8 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 24 Oct 2026 00:10:58 +0200 Subject: [PATCH 3/4] Place regression tests beside related syntax cases --- tests/sqlparser_sqlite.rs | 82 +++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index d5ac49807c..3c7569c165 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -955,6 +955,47 @@ fn test_drop_trigger() { } } +#[test] +fn parse_between_low_bound_precedence() { + match sqlite().verified_expr("a BETWEEN b > 1 AND c") { + Expr::Between { low, .. } => assert!( + matches!( + *low, + Expr::BinaryOp { + op: BinaryOperator::Gt, + .. + } + ), + "{low:?}" + ), + other => panic!("expected BETWEEN, got {other:?}"), + } + sqlite().verified_expr("a BETWEEN b LIKE 'x' AND c"); + sqlite().verified_expr("a BETWEEN b IS NOT NULL AND c"); + sqlite().expr_parses_to("a BETWEEN b NOTNULL AND c", "a BETWEEN b IS NOT NULL AND c"); + sqlite().verified_expr("a BETWEEN b BETWEEN 1 AND 2 AND c"); + + // Operators after the high bound apply to the whole BETWEEN. + sqlite().verified_expr("5 BETWEEN 1 AND NULL IS NULL"); + sqlite().verified_expr("5 BETWEEN 1 AND 10 = 1"); + + // The inner AND belongs to the bound expression, so the missing `)` is reported at FROM. + assert_eq!( + sqlite().parse_sql_statements("SELECT a BETWEEN (1 AND 2 FROM t"), + Err(ParserError::ParserError( + "Expected: ), found: FROM".to_string() + )) + ); + + assert_eq!( + TestedDialects::new(vec![Box::new(GenericDialect {})]) + .parse_sql_statements("SELECT a BETWEEN b > 1 AND c FROM t"), + Err(ParserError::ParserError( + "Expected: AND, found: >".to_string() + )) + ); +} + #[test] fn parse_pattern_operators_bind_at_like_precedence() { fn where_operator(sql: &str) -> BinaryOperator { @@ -1488,47 +1529,6 @@ fn parse_on_conflict_target_predicate() { other => panic!("unexpected statement: {other:?}"), } } - -fn parse_between_low_bound_precedence() { - match sqlite().verified_expr("a BETWEEN b > 1 AND c") { - Expr::Between { low, .. } => assert!( - matches!( - *low, - Expr::BinaryOp { - op: BinaryOperator::Gt, - .. - } - ), - "{low:?}" - ), - other => panic!("expected BETWEEN, got {other:?}"), - } - sqlite().verified_expr("a BETWEEN b LIKE 'x' AND c"); - sqlite().verified_expr("a BETWEEN b IS NOT NULL AND c"); - sqlite().expr_parses_to("a BETWEEN b NOTNULL AND c", "a BETWEEN b IS NOT NULL AND c"); - sqlite().verified_expr("a BETWEEN b BETWEEN 1 AND 2 AND c"); - - // Operators after the high bound apply to the whole BETWEEN. - sqlite().verified_expr("5 BETWEEN 1 AND NULL IS NULL"); - sqlite().verified_expr("5 BETWEEN 1 AND 10 = 1"); - - // The inner AND belongs to the bound expression, so the missing `)` is reported at FROM. - assert_eq!( - sqlite().parse_sql_statements("SELECT a BETWEEN (1 AND 2 FROM t"), - Err(ParserError::ParserError( - "Expected: ), found: FROM".to_string() - )) - ); - - assert_eq!( - TestedDialects::new(vec![Box::new(GenericDialect {})]) - .parse_sql_statements("SELECT a BETWEEN b > 1 AND c FROM t"), - Err(ParserError::ParserError( - "Expected: AND, found: >".to_string() - )) - ); -} - fn sqlite() -> TestedDialects { TestedDialects::new(vec![Box::new(SQLiteDialect {})]) } From e9e31705d1983c1465794e1645657994a6ecb719 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 24 Oct 2026 13:39:12 +0200 Subject: [PATCH 4/4] Add fuzz seeds for sqlite between operand precedence --- fuzz/fuzz_seeds/sqlite_2664_between_low_bound.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fuzz/fuzz_seeds/sqlite_2664_between_low_bound.sql diff --git a/fuzz/fuzz_seeds/sqlite_2664_between_low_bound.sql b/fuzz/fuzz_seeds/sqlite_2664_between_low_bound.sql new file mode 100644 index 0000000000..297aed4f95 --- /dev/null +++ b/fuzz/fuzz_seeds/sqlite_2664_between_low_bound.sql @@ -0,0 +1,2 @@ +SELECT a BETWEEN b > 1 AND c, a BETWEEN b LIKE 'x' AND c, a BETWEEN b IS NOT NULL AND c, a BETWEEN b NOTNULL AND c, a BETWEEN b BETWEEN 1 AND 2 AND c FROM t; +SELECT 5 BETWEEN 1 AND NULL IS NULL, 5 BETWEEN 1 AND 10 = 1 FROM t