From b908ae8b398cb76d066f2cc725e922663e2f7b21 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Mon, 5 Oct 2026 16:35:24 +0200 Subject: [PATCH 1/4] SQLite: Reject colon-based semi-structured data traversal --- src/dialect/sqlite.rs | 2 ++ tests/sqlparser_sqlite.rs | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index 44dcc9d267..af6423c3e9 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -117,6 +117,8 @@ impl Dialect for SQLiteDialect { Some(Ok(self.prec_value(Precedence::Ampersand))) } Token::Lt | Token::LtEq | Token::Gt | Token::GtEq => Some(Ok(COMPARISON_PREC)), + // lowest prec to prevent it from turning into a binary op + Token::Colon => Some(Ok(self.prec_unknown())), _ => None, } } diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index ad80e95a93..da6a7ce6e1 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -1489,6 +1489,24 @@ fn parse_on_conflict_target_predicate() { } } +fn parse_colon_does_not_start_json_access() { + // SQLite rejects `SELECT c:se` (`near ":se": syntax error`); no `:` traversal. + assert!(sqlite().parse_sql_statements("SELECT c:se").is_err()); + assert!(sqlite().parse_sql_statements("SELECT a:b FROM t").is_err()); + + let ast = sqlite().verified_only_select("SELECT :xxx"); + assert_eq!( + ast.projection[0], + UnnamedExpr(Expr::Value( + (Value::Placeholder(":xxx".into())).with_empty_span() + )), + ); + + assert!(TestedDialects::new(vec![Box::new(GenericDialect {})]) + .parse_sql_statements("SELECT a:b FROM t") + .is_ok()); +} + fn sqlite() -> TestedDialects { TestedDialects::new(vec![Box::new(SQLiteDialect {})]) } From 70f66cd94c8b80c50640241a670da946b002f2e7 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 16 Oct 2026 23:29:28 +0200 Subject: [PATCH 2/4] Assert exact errors for the new deny lints --- tests/sqlparser_sqlite.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index da6a7ce6e1..36cdb9bd3e 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -1491,8 +1491,14 @@ fn parse_on_conflict_target_predicate() { fn parse_colon_does_not_start_json_access() { // SQLite rejects `SELECT c:se` (`near ":se": syntax error`); no `:` traversal. - assert!(sqlite().parse_sql_statements("SELECT c:se").is_err()); - assert!(sqlite().parse_sql_statements("SELECT a:b FROM t").is_err()); + for sql in ["SELECT c:se", "SELECT a:b FROM t"] { + assert_eq!( + sqlite().parse_sql_statements(sql), + Err(ParserError::ParserError( + "Expected: end of statement, found: :".to_string() + )) + ); + } let ast = sqlite().verified_only_select("SELECT :xxx"); assert_eq!( @@ -1502,9 +1508,9 @@ fn parse_colon_does_not_start_json_access() { )), ); - assert!(TestedDialects::new(vec![Box::new(GenericDialect {})]) + TestedDialects::new(vec![Box::new(GenericDialect {})]) .parse_sql_statements("SELECT a:b FROM t") - .is_ok()); + .unwrap(); } fn sqlite() -> TestedDialects { From 35ec8f595e1bfc1b7ecf079984c8f271ffaafb23 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 24 Oct 2026 00:26:26 +0200 Subject: [PATCH 3/4] Place regression tests beside related syntax cases --- tests/sqlparser_sqlite.rs | 50 +++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 36cdb9bd3e..f1298a3641 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -390,6 +390,31 @@ fn test_placeholder() { ); } +#[test] +fn parse_colon_does_not_start_json_access() { + // SQLite rejects `SELECT c:se` (`near ":se": syntax error`); no `:` traversal. + for sql in ["SELECT c:se", "SELECT a:b FROM t"] { + assert_eq!( + sqlite().parse_sql_statements(sql), + Err(ParserError::ParserError( + "Expected: end of statement, found: :".to_string() + )) + ); + } + + let ast = sqlite().verified_only_select("SELECT :xxx"); + assert_eq!( + ast.projection[0], + UnnamedExpr(Expr::Value( + (Value::Placeholder(":xxx".into())).with_empty_span() + )), + ); + + TestedDialects::new(vec![Box::new(GenericDialect {})]) + .parse_sql_statements("SELECT a:b FROM t") + .unwrap(); +} + #[test] fn parse_create_table_with_strict() { let sql = "CREATE TABLE Fruits (id TEXT NOT NULL PRIMARY KEY) STRICT"; @@ -1488,31 +1513,6 @@ fn parse_on_conflict_target_predicate() { other => panic!("unexpected statement: {other:?}"), } } - -fn parse_colon_does_not_start_json_access() { - // SQLite rejects `SELECT c:se` (`near ":se": syntax error`); no `:` traversal. - for sql in ["SELECT c:se", "SELECT a:b FROM t"] { - assert_eq!( - sqlite().parse_sql_statements(sql), - Err(ParserError::ParserError( - "Expected: end of statement, found: :".to_string() - )) - ); - } - - let ast = sqlite().verified_only_select("SELECT :xxx"); - assert_eq!( - ast.projection[0], - UnnamedExpr(Expr::Value( - (Value::Placeholder(":xxx".into())).with_empty_span() - )), - ); - - TestedDialects::new(vec![Box::new(GenericDialect {})]) - .parse_sql_statements("SELECT a:b FROM t") - .unwrap(); -} - fn sqlite() -> TestedDialects { TestedDialects::new(vec![Box::new(SQLiteDialect {})]) } From 306917eba1c3f7d9b8648d75e563d4b11bcec938 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 24 Oct 2026 13:41:30 +0200 Subject: [PATCH 4/4] Add fuzz seeds for upstream/sqlite postfix colon --- fuzz/fuzz_seeds/sqlite_2613_colon_traversal.sql | 1 + 1 file changed, 1 insertion(+) create mode 100644 fuzz/fuzz_seeds/sqlite_2613_colon_traversal.sql diff --git a/fuzz/fuzz_seeds/sqlite_2613_colon_traversal.sql b/fuzz/fuzz_seeds/sqlite_2613_colon_traversal.sql new file mode 100644 index 0000000000..346ddc89e1 --- /dev/null +++ b/fuzz/fuzz_seeds/sqlite_2613_colon_traversal.sql @@ -0,0 +1 @@ +SELECT a:b, :xxx FROM t;