diff --git a/fuzz/fuzz_seeds/sqlite_2469_hexadecimal_pragma.sql b/fuzz/fuzz_seeds/sqlite_2469_hexadecimal_pragma.sql new file mode 100644 index 0000000000..6fbc5842a6 --- /dev/null +++ b/fuzz/fuzz_seeds/sqlite_2469_hexadecimal_pragma.sql @@ -0,0 +1 @@ +PRAGMA user_version = 0x10 diff --git a/fuzz/fuzz_seeds/sqlite_2469_pragma_values.sql b/fuzz/fuzz_seeds/sqlite_2469_pragma_values.sql new file mode 100644 index 0000000000..3661e6c294 --- /dev/null +++ b/fuzz/fuzz_seeds/sqlite_2469_pragma_values.sql @@ -0,0 +1,7 @@ +PRAGMA case_sensitive_like = true; +PRAGMA case_sensitive_like = oN; +PRAGMA journal_mode = WAL; +PRAGMA cache_size = -2000; +PRAGMA cache_size = +2000; +PRAGMA optimize = 0x10002; +PRAGMA wal_checkpoint(TRUNCATE); diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8e798524c9..7085a368e8 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4836,8 +4836,8 @@ pub enum Statement { Pragma { /// Pragma name (possibly qualified). name: ObjectName, - /// Optional pragma value. - value: Option, + /// Optional pragma value (`signed-number`, `name`, or `signed-literal`). + value: Option, /// Whether the pragma used `=`. is_eq: bool, }, diff --git a/src/ast/value.rs b/src/ast/value.rs index 5b05af694c..31c37b6438 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -196,6 +196,8 @@ pub enum Value { NationalQuoteDelimitedStringLiteral(QuoteDelimitedString), /// X'hex value' HexStringLiteral(String), + /// Hexadecimal numeric literal written as `0x...`. + HexNumber(String), /// Double quoted string literal, e.g. `"abc"`. DoubleQuotedString(String), @@ -277,6 +279,7 @@ impl fmt::Display for Value { Value::QuoteDelimitedStringLiteral(v) => v.fmt(f), Value::NationalQuoteDelimitedStringLiteral(v) => write!(f, "N{v}"), Value::HexStringLiteral(v) => write!(f, "X'{}'", escape_single_quote_string(v)), + Value::HexNumber(v) => write!(f, "0x{v}"), Value::Boolean(v) => write!(f, "{v}"), Value::SingleQuotedByteStringLiteral(v) => { write!(f, "B'{}'", escape_single_quote_string(v)) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3efb22a349..3ca93b7a12 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1718,6 +1718,7 @@ impl<'a> Parser<'a> { Token::SingleQuotedString(_) | Token::DoubleQuotedString(_) | Token::HexStringLiteral(_) + | Token::HexNumber(_) if w.value.starts_with('_') => { Ok(Expr::Prefixed { @@ -1729,6 +1730,7 @@ impl<'a> Parser<'a> { Token::SingleQuotedString(_) | Token::DoubleQuotedString(_) | Token::HexStringLiteral(_) + | Token::HexNumber(_) if w.value.starts_with('_') => { Ok(Expr::Prefixed { @@ -2052,7 +2054,8 @@ impl<'a> Parser<'a> { | Token::NationalStringLiteral(_) | Token::QuoteDelimitedStringLiteral(_) | Token::NationalQuoteDelimitedStringLiteral(_) - | Token::HexStringLiteral(_) => { + | Token::HexStringLiteral(_) + | Token::HexNumber(_) => { self.prev_token(); Ok(Expr::Value(self.parse_value()?)) } @@ -3273,7 +3276,8 @@ impl<'a> Parser<'a> { | Token::NationalStringLiteral(_) | Token::QuoteDelimitedStringLiteral(_) | Token::NationalQuoteDelimitedStringLiteral(_) - | Token::HexStringLiteral(_) => Some(Box::new(self.parse_expr()?)), + | Token::HexStringLiteral(_) + | Token::HexNumber(_) => Some(Box::new(self.parse_expr()?)), _ => self.expected_ref( "either filler, WITH, or WITHOUT in LISTAGG", self.peek_token_ref(), @@ -12939,6 +12943,7 @@ impl<'a> Parser<'a> { ok_value(Value::UnicodeStringLiteral(s.to_string())) } Token::HexStringLiteral(ref s) => ok_value(Value::HexStringLiteral(s.to_string())), + Token::HexNumber(s) => ok_value(Value::HexNumber(s)), Token::Placeholder(ref s) => ok_value(Value::Placeholder(s.to_string())), tok @ Token::Colon | tok @ Token::AtSign => { // 1. Not calling self.parse_identifier(false)? @@ -13052,6 +13057,7 @@ impl<'a> Parser<'a> { Token::HexStringLiteral(ref s) => Ok(Expr::Value( Value::HexStringLiteral(s.to_string()).with_span(span), )), + Token::HexNumber(s) => Ok(Expr::Value(Value::HexNumber(s).with_span(span))), unexpected => self.expected( "a string value", TokenWithSpan { @@ -20833,44 +20839,53 @@ impl<'a> Parser<'a> { }) } - fn parse_pragma_value(&mut self) -> Result { - let v = self.parse_value()?; - match &v.value { - Value::SingleQuotedString(_) => Ok(v), - Value::DoubleQuotedString(_) => Ok(v), - Value::Number(_, _) => Ok(v), - Value::Placeholder(_) => Ok(v), - _ => { - self.prev_token(); - self.expected_ref("number or string or ? placeholder", self.peek_token_ref()) + /// Parse a SQLite `pragma-value`: `signed-number | name | signed-literal`. + fn parse_pragma_value(&mut self) -> Result { + if matches!(self.peek_token_ref().token, Token::Plus | Token::Minus) { + let op = match self.next_token().token { + Token::Plus => UnaryOperator::Plus, + _ => UnaryOperator::Minus, + }; + // SQLite only permits a sign before a number + if !matches!( + self.peek_token_ref().token, + Token::Number(..) | Token::HexNumber(_) + ) { + return self.expected_ref("number", self.peek_token_ref()); } + return Ok(Expr::UnaryOp { + op, + expr: Box::new(Expr::Value(self.parse_value()?)), + }); + } + if matches!( + self.peek_token_ref().token, + Token::Word(Word { + keyword: Keyword::NULL, + .. + }) | Token::HexStringLiteral(_) + ) { + return self.expected_ref("number, name or string", self.peek_token_ref()); + } + match self.try_parse(|parser| parser.parse_value()) { + Ok(value) => Ok(Expr::Value(value)), + Err(_) => Ok(Expr::Identifier(self.parse_identifier()?)), } } /// PRAGMA [schema-name '.'] pragma-name [('=' pragma-value) | '(' pragma-value ')'] pub fn parse_pragma(&mut self) -> Result { let name = self.parse_object_name(false)?; - if self.consume_token(&Token::LParen) { + let (value, is_eq) = if self.consume_token(&Token::LParen) { let value = self.parse_pragma_value()?; self.expect_token(&Token::RParen)?; - Ok(Statement::Pragma { - name, - value: Some(value), - is_eq: false, - }) + (Some(value), false) } else if self.consume_token(&Token::Eq) { - Ok(Statement::Pragma { - name, - value: Some(self.parse_pragma_value()?), - is_eq: true, - }) + (Some(self.parse_pragma_value()?), true) } else { - Ok(Statement::Pragma { - name, - value: None, - is_eq: false, - }) - } + (None, false) + }; + Ok(Statement::Pragma { name, value, is_eq }) } /// `INSTALL [extension_name]` diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 9276f91ded..dc77f49d4c 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -113,6 +113,8 @@ pub enum Token { UnicodeStringLiteral(String), /// Hexadecimal string literal: i.e.: X'deadbeef' HexStringLiteral(String), + /// Hexadecimal numeric literal written as `0x...`. + HexNumber(String), /// Comma Comma, /// Whitespace (space, tab, etc) @@ -315,6 +317,7 @@ impl fmt::Display for Token { Token::EscapedStringLiteral(ref s) => write!(f, "E'{s}'"), Token::UnicodeStringLiteral(ref s) => write!(f, "U&'{s}'"), Token::HexStringLiteral(ref s) => write!(f, "X'{s}'"), + Token::HexNumber(s) => write!(f, "0x{s}"), Token::SingleQuotedByteStringLiteral(ref s) => write!(f, "B'{s}'"), Token::TripleSingleQuotedByteStringLiteral(ref s) => write!(f, "B'''{s}'''"), Token::DoubleQuotedByteStringLiteral(ref s) => write!(f, "B\"{s}\""), @@ -1606,11 +1609,10 @@ impl<'a> Tokenizer<'a> { return Ok(Some(Token::Number(s, false))); } - // match binary literal that starts with 0x if s == "0" && chars.peek() == Some(&'x') { chars.next(); let s2 = self.tokenize_number_part(chars, |ch| ch.is_ascii_hexdigit())?; - return Ok(Some(Token::HexStringLiteral(s2))); + return Ok(Some(Token::HexNumber(s2))); } // match one period @@ -3035,10 +3037,8 @@ mod tests { ], ); - numeric_underscore_dialects.tokenizes_to( - "0xFF_FF", - vec![Token::HexStringLiteral("FF_FF".to_string())], - ); + numeric_underscore_dialects + .tokenizes_to("0xFF_FF", vec![Token::HexNumber("FF_FF".to_string())]); for dialect in &numeric_underscore_dialects.dialects { for sql in [ diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 62f3a53efd..e5d3d00687 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -774,7 +774,7 @@ fn parse_mssql_top() { #[test] fn parse_mssql_bin_literal() { - let _ = ms_and_generic().one_statement_parses_to("SELECT 0xdeadBEEF", "SELECT X'deadBEEF'"); + ms_and_generic().verified_stmt("SELECT 0xdeadBEEF"); } #[test] diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index ad80e95a93..1a9c437114 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -30,8 +30,8 @@ use sqlparser::ast::SelectItem::UnnamedExpr; use sqlparser::ast::Value::Placeholder; use sqlparser::ast::*; use sqlparser::dialect::{AnsiDialect, GenericDialect, SQLiteDialect}; -use sqlparser::parser::{ParserError, ParserOptions}; -use sqlparser::tokenizer::Token; +use sqlparser::parser::{Parser, ParserError, ParserOptions}; +use sqlparser::tokenizer::{Span, Token}; #[test] fn pragma_no_value() { @@ -126,6 +126,106 @@ fn pragma_eq_placeholder_style() { } } +#[test] +fn pragma_values() { + // signed-literal + let statement = sqlite_and_generic().verified_stmt("PRAGMA case_sensitive_like = true"); + assert!(matches!( + statement, + Statement::Pragma { + value: Some(Expr::Value(ValueWithSpan { + value: Value::Boolean(true), + .. + })), + is_eq: true, + .. + } + )); + + // name: kept verbatim as an identifier + for spelling in [ + "ON", "OFF", "YES", "NO", "WAL", "DELETE", "NORMAL", "FULL", "MEMORY", + ] { + let sql = format!("PRAGMA case_sensitive_like = {spelling}"); + let Statement::Pragma { + value: Some(Expr::Identifier(ident)), + is_eq: true, + .. + } = sqlite_and_generic().verified_stmt(&sql) + else { + panic!("expected identifier pragma value for {spelling}"); + }; + assert_eq!(spelling, ident.value); + assert_eq!(None, ident.quote_style); + } + + // identifier value keeps its span + let statements = + Parser::parse_sql(&SQLiteDialect {}, "PRAGMA case_sensitive_like = oN").unwrap(); + let [Statement::Pragma { + value: Some(Expr::Identifier(ident)), + is_eq: true, + .. + }] = statements.as_slice() + else { + panic!("Expected equality-form PRAGMA") + }; + assert_eq!("oN", ident.value); + assert_eq!(Span::new((1, 30).into(), (1, 32).into()), ident.span); + + // signed-number + let statement = sqlite_and_generic().verified_stmt("PRAGMA cache_size = -2000"); + assert!(matches!( + statement, + Statement::Pragma { + value: Some(Expr::UnaryOp { + op: UnaryOperator::Minus, + .. + }), + is_eq: true, + .. + } + )); + sqlite_and_generic().verified_stmt("PRAGMA cache_size = +2000"); + + sqlite_and_generic().verified_stmt("PRAGMA optimize = 0x10002"); + + // function-call form with a name argument + let statement = sqlite_and_generic().verified_stmt("PRAGMA wal_checkpoint(TRUNCATE)"); + assert!(matches!( + statement, + Statement::Pragma { + value: Some(Expr::Identifier(_)), + is_eq: false, + .. + } + )); + // values SQLite rejects + for (sql, error) in [ + ("PRAGMA x = -'str'", "Expected: number, found: 'str'"), + ("PRAGMA x = +'str'", "Expected: number, found: 'str'"), + ( + "PRAGMA x = NULL", + "Expected: number, name or string, found: NULL", + ), + ("PRAGMA x()", "Expected: identifier, found: )"), + ("PRAGMA x = )", "Expected: identifier, found: )"), + ("PRAGMA x(=)", "Expected: identifier, found: ="), + ] { + let actual = sqlite_and_generic().parse_sql_statements(sql).unwrap_err(); + assert!(actual.to_string().contains(error), "{sql}: {actual}"); + } +} + +#[cfg(feature = "bigdecimal")] +#[test] +fn pragma_values_reject_unrepresentable_signed_numbers() { + let error = sqlite_and_generic() + .parse_sql_statements("PRAGMA x = -1e999999999999999999999999") + .unwrap_err(); + assert!(matches!(error, ParserError::ParserError(_))); +} + #[test] fn parse_create_table_without_rowid() { let sql = "CREATE TABLE t (a INT) WITHOUT ROWID"; @@ -1786,3 +1886,26 @@ fn parse_bitwise_shift_precedence_boundaries() { assert_eq!(sqlite().verified_expr(sql), expected, "{sql}"); } } + +#[test] +fn pragma_hexadecimal_numbers_round_trip() { + for sql in [ + "PRAGMA user_version = 0x10", + "PRAGMA user_version = +0x10", + "PRAGMA user_version = -0x10", + "PRAGMA user_version(0xffffffffffffffff)", + ] { + sqlite_and_generic().verified_stmt(sql); + } +} + +#[test] +fn pragma_rejects_blob_literals() { + for sql in [ + "PRAGMA user_version = X'10'", + "PRAGMA user_version = +X'10'", + "PRAGMA user_version = -X'10'", + ] { + sqlite_and_generic().parse_sql_statements(sql).unwrap_err(); + } +}