Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions fuzz/fuzz_seeds/sqlite_2469_hexadecimal_pragma.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PRAGMA user_version = 0x10
7 changes: 7 additions & 0 deletions fuzz/fuzz_seeds/sqlite_2469_pragma_values.sql
Original file line number Diff line number Diff line change
@@ -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);
4 changes: 2 additions & 2 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4836,8 +4836,8 @@ pub enum Statement {
Pragma {
/// Pragma name (possibly qualified).
name: ObjectName,
/// Optional pragma value.
value: Option<ValueWithSpan>,
/// Optional pragma value (`signed-number`, `name`, or `signed-literal`).
value: Option<Expr>,
/// Whether the pragma used `=`.
is_eq: bool,
},
Expand Down
3 changes: 3 additions & 0 deletions src/ast/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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))
Expand Down
73 changes: 44 additions & 29 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1718,6 +1718,7 @@ impl<'a> Parser<'a> {
Token::SingleQuotedString(_)
| Token::DoubleQuotedString(_)
| Token::HexStringLiteral(_)
| Token::HexNumber(_)
if w.value.starts_with('_') =>
{
Ok(Expr::Prefixed {
Expand All @@ -1729,6 +1730,7 @@ impl<'a> Parser<'a> {
Token::SingleQuotedString(_)
| Token::DoubleQuotedString(_)
| Token::HexStringLiteral(_)
| Token::HexNumber(_)
if w.value.starts_with('_') =>
{
Ok(Expr::Prefixed {
Expand Down Expand Up @@ -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()?))
}
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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)?
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -20833,44 +20839,53 @@ impl<'a> Parser<'a> {
})
}

fn parse_pragma_value(&mut self) -> Result<ValueWithSpan, ParserError> {
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<Expr, ParserError> {
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()?)),
}
Comment on lines +20844 to 20873

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm can this be replaced by a call to parse_expr()?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In terms of accepting things, yes it would work, but parse_expr() would over-extend the grammar accepting arbitrary expressions like PRAGMA p = 1 + 2, foo(bar), a AND b or (SELECT 1).

SQLite defines a pragma value as signed-number | name | signed-literal, so parse_pragma_value mirrors that production exactly while still leaving the per-pragma value validation to execution.

I understand I tend to lean much more strongly on the strict parser approach, but here I really believe allowing any expr would be excessively loose.

}

/// PRAGMA [schema-name '.'] pragma-name [('=' pragma-value) | '(' pragma-value ')']
pub fn parse_pragma(&mut self) -> Result<Statement, ParserError> {
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]`
Expand Down
12 changes: 6 additions & 6 deletions src/tokenizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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}\""),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 [
Expand Down
2 changes: 1 addition & 1 deletion tests/sqlparser_mssql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
127 changes: 125 additions & 2 deletions tests/sqlparser_sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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();
}
}
Loading