From 78baef8491c2e1cbbb3c9d8a81ec2a207df7c399 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 2 Oct 2026 20:04:02 +0200 Subject: [PATCH 1/4] SQLite: Bind <, <=, > and >= tighter than = --- src/dialect/sqlite.rs | 25 ++++++++++++++++ tests/sqlparser_sqlite.rs | 63 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index 3deffafb7d..a1326cabfa 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -25,6 +25,10 @@ use crate::keywords::Keyword; use crate::parser::{Parser, ParserError}; use crate::tokenizer::Token; +/// Precedence of `<`, `<=`, `>` and `>=`, between `=` and the bitwise operators, see +/// +const COMPARISON_PREC: u8 = 21; + /// A [`Dialect`] for [SQLite](https://www.sqlite.org) /// /// This dialect allows columns in a @@ -103,10 +107,31 @@ impl Dialect for SQLiteDialect { fn get_next_precedence(&self, parser: &Parser) -> Option> { match parser.peek_token_ref().token { Token::Ampersand | Token::Pipe => Some(Ok(self.prec_value(Precedence::Ampersand))), + Token::Lt | Token::LtEq | Token::Gt | Token::GtEq => Some(Ok(COMPARISON_PREC)), _ => None, } } + fn prec_value(&self, prec: Precedence) -> u8 { + match prec { + Precedence::Period => 100, + Precedence::DoubleColon => 50, + Precedence::AtTz => 41, + Precedence::MulDivModOp => 40, + Precedence::PlusMinus => 30, + Precedence::Xor => 25, + Precedence::Ampersand => 24, + Precedence::Caret => 23, + Precedence::Pipe | Precedence::Colon | Precedence::PgOther => 22, + Precedence::Between | Precedence::Eq => 20, + Precedence::Like => 19, + Precedence::Is => 17, + Precedence::UnaryNot => 15, + Precedence::And => 10, + Precedence::Or => 5, + } + } + fn supports_in_empty_list(&self) -> bool { true } diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index eaadea263e..ea1e78c651 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -1222,6 +1222,69 @@ fn parse_over_as_column_alias_after_function() { Err(ParserError::ParserError( "Expected: identifier, found: EOF".to_string() )) + )) + ); +} + +#[test] +fn parse_comparison_binds_tighter_than_equality() { + let id = |name: &str| Expr::Identifier(Ident::new(name)); + let binary = |left: Expr, op: BinaryOperator, right: Expr| Expr::BinaryOp { + left: Box::new(left), + op, + right: Box::new(right), + }; + + for (sql, eq_op, cmp_op) in [ + ("a = b < c", BinaryOperator::Eq, BinaryOperator::Lt), + ("a <> b <= c", BinaryOperator::NotEq, BinaryOperator::LtEq), + ("a = b > c", BinaryOperator::Eq, BinaryOperator::Gt), + ("a <> b >= c", BinaryOperator::NotEq, BinaryOperator::GtEq), + ] { + assert_eq!( + sqlite().verified_expr(sql), + binary(id("a"), eq_op, binary(id("b"), cmp_op, id("c"))), + "{sql}" + ); + } + + assert_eq!( + sqlite().verified_expr("a < b = c"), + binary( + binary(id("a"), BinaryOperator::Lt, id("b")), + BinaryOperator::Eq, + id("c") + ), + ); + + // `IN` and `BETWEEN` sit on the level of `=` + assert_eq!( + sqlite().verified_expr("a < b IN (1)"), + Expr::InList { + expr: Box::new(binary(id("a"), BinaryOperator::Lt, id("b"))), + list: vec![Expr::value(number("1"))], + negated: false, + global: false, + }, + ); + assert_eq!( + sqlite().verified_expr("a BETWEEN b AND c < d"), + Expr::Between { + expr: Box::new(id("a")), + negated: false, + low: Box::new(id("b")), + high: Box::new(binary(id("c"), BinaryOperator::Lt, id("d"))), + }, + ); + + // Other dialects keep one level for every comparison + assert_eq!( + all_dialects_except(|d| d.is::()).verified_expr("a = b < c"), + binary( + binary(id("a"), BinaryOperator::Eq, id("b")), + BinaryOperator::Lt, + id("c") + ), ); } From b2376aee75f411c0639e767ce378eb26ebe0bd12 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Tue, 20 Oct 2026 12:51:19 +0200 Subject: [PATCH 2/4] Cover qualified wildcard rejection in SQLite expressions --- tests/sqlparser_sqlite.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index ea1e78c651..cae457690f 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -1288,6 +1288,16 @@ fn parse_comparison_binds_tighter_than_equality() { ); } +#[test] +fn parse_rejects_star_qualified_wildcard_in_boolean_context() { + assert_eq!( + sqlite().parse_sql_statements("SELECT * FROM t WHERE a.*"), + Err(ParserError::ParserError( + "Expected: end of statement, found: .".to_string() + )), + ); +} + fn sqlite() -> TestedDialects { TestedDialects::new(vec![Box::new(SQLiteDialect {})]) } From 6f1328243eef353e47a91af6b669a1fa191de692 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Wed, 21 Oct 2026 09:54:12 +0200 Subject: [PATCH 3/4] Match SQLite comparison assertions to the unified IN target --- tests/sqlparser_sqlite.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index cae457690f..8b1e6db09a 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -1222,7 +1222,6 @@ fn parse_over_as_column_alias_after_function() { Err(ParserError::ParserError( "Expected: identifier, found: EOF".to_string() )) - )) ); } @@ -1260,9 +1259,9 @@ fn parse_comparison_binds_tighter_than_equality() { // `IN` and `BETWEEN` sit on the level of `=` assert_eq!( sqlite().verified_expr("a < b IN (1)"), - Expr::InList { + Expr::In { expr: Box::new(binary(id("a"), BinaryOperator::Lt, id("b"))), - list: vec![Expr::value(number("1"))], + target: InTarget::List(vec![Expr::value(number("1"))]), negated: false, global: false, }, From ea04fc657aafcd9e31a7f03fd260bbf942f75f4c Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 24 Oct 2026 00:32:47 +0200 Subject: [PATCH 4/4] Place regression tests beside related syntax cases --- tests/sqlparser_sqlite.rs | 60 +++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 8b1e6db09a..b5552ef78a 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -1225,6 +1225,36 @@ fn parse_over_as_column_alias_after_function() { ); } +fn sqlite() -> TestedDialects { + TestedDialects::new(vec![Box::new(SQLiteDialect {})]) +} + +fn sqlite_with_options(options: ParserOptions) -> TestedDialects { + TestedDialects::new_with_options(vec![Box::new(SQLiteDialect {})], options) +} + +fn sqlite_and_generic() -> TestedDialects { + TestedDialects::new(vec![ + Box::new(SQLiteDialect {}), + Box::new(GenericDialect {}), + ]) +} + +#[test] +fn test_dollar_sign_in_identifier() { + // SQLite allows $ anywhere after the first character of an identifier + sqlite().verified_stmt("SELECT a$ FROM t"); + sqlite().verified_stmt("SELECT 1 AS a$"); + sqlite().one_statement_parses_to("CREATE TABLE t(a$ INT)", "CREATE TABLE t (a$ INT)"); + sqlite().verified_stmt("INSERT INTO t$ VALUES (1)"); + // AS$ is parsed as the alias name + sqlite().one_statement_parses_to("SELECT a AS$", "SELECT a AS AS$"); + // Dialects without $ as an identifier character still reject these identifiers + TestedDialects::new(vec![Box::new(AnsiDialect {})]) + .parse_sql_statements("SELECT a$ FROM t") + .unwrap_err(); +} + #[test] fn parse_comparison_binds_tighter_than_equality() { let id = |name: &str| Expr::Identifier(Ident::new(name)); @@ -1297,36 +1327,6 @@ fn parse_rejects_star_qualified_wildcard_in_boolean_context() { ); } -fn sqlite() -> TestedDialects { - TestedDialects::new(vec![Box::new(SQLiteDialect {})]) -} - -fn sqlite_with_options(options: ParserOptions) -> TestedDialects { - TestedDialects::new_with_options(vec![Box::new(SQLiteDialect {})], options) -} - -fn sqlite_and_generic() -> TestedDialects { - TestedDialects::new(vec![ - Box::new(SQLiteDialect {}), - Box::new(GenericDialect {}), - ]) -} - -#[test] -fn test_dollar_sign_in_identifier() { - // SQLite allows $ anywhere after the first character of an identifier - sqlite().verified_stmt("SELECT a$ FROM t"); - sqlite().verified_stmt("SELECT 1 AS a$"); - sqlite().one_statement_parses_to("CREATE TABLE t(a$ INT)", "CREATE TABLE t (a$ INT)"); - sqlite().verified_stmt("INSERT INTO t$ VALUES (1)"); - // AS$ is parsed as the alias name - sqlite().one_statement_parses_to("SELECT a AS$", "SELECT a AS AS$"); - // Dialects without $ as an identifier character still reject these identifiers - TestedDialects::new(vec![Box::new(AnsiDialect {})]) - .parse_sql_statements("SELECT a$ FROM t") - .unwrap_err(); -} - #[test] fn parse_bitwise_and_or_share_precedence() { fn num(n: &str) -> Expr {