Skip to content

Commit dd5df3a

Browse files
gamifegamife
authored andcommitted
feat: add arg placeholder (apache#420)
Co-authored-by: gamife <[email protected]>
1 parent d6250ba commit dd5df3a

File tree

4 files changed

+61
-1
lines changed

4 files changed

+61
-1
lines changed

src/ast/value.rs

+3
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ pub enum Value {
6464
},
6565
/// `NULL` value
6666
Null,
67+
/// `?` or `$` Prepared statement arg placeholder
68+
Placeholder(String),
6769
}
6870

6971
impl fmt::Display for Value {
@@ -117,6 +119,7 @@ impl fmt::Display for Value {
117119
Ok(())
118120
}
119121
Value::Null => write!(f, "NULL"),
122+
Value::Placeholder(v) => write!(f, "{}", v),
120123
}
121124
}
122125
}

src/parser.rs

+5
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,10 @@ impl<'a> Parser<'a> {
454454
self.expect_token(&Token::RParen)?;
455455
Ok(expr)
456456
}
457+
Token::Placeholder(_) => {
458+
self.prev_token();
459+
Ok(Expr::Value(self.parse_value()?))
460+
}
457461
unexpected => self.expected("an expression:", unexpected),
458462
}?;
459463

@@ -1991,6 +1995,7 @@ impl<'a> Parser<'a> {
19911995
Token::SingleQuotedString(ref s) => Ok(Value::SingleQuotedString(s.to_string())),
19921996
Token::NationalStringLiteral(ref s) => Ok(Value::NationalStringLiteral(s.to_string())),
19931997
Token::HexStringLiteral(ref s) => Ok(Value::HexStringLiteral(s.to_string())),
1998+
Token::Placeholder(ref s) => Ok(Value::Placeholder(s.to_string())),
19941999
unexpected => self.expected("a value", unexpected),
19952000
}
19962001
}

src/tokenizer.rs

+13
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ pub enum Token {
139139
PGSquareRoot,
140140
/// `||/` , a cube root math operator in PostgreSQL
141141
PGCubeRoot,
142+
/// `?` or `$` , a prepared statement arg placeholder
143+
Placeholder(String),
142144
}
143145

144146
impl fmt::Display for Token {
@@ -194,6 +196,7 @@ impl fmt::Display for Token {
194196
Token::ShiftRight => f.write_str(">>"),
195197
Token::PGSquareRoot => f.write_str("|/"),
196198
Token::PGCubeRoot => f.write_str("||/"),
199+
Token::Placeholder(ref s) => write!(f, "{}", s),
197200
}
198201
}
199202
}
@@ -337,6 +340,7 @@ impl<'a> Tokenizer<'a> {
337340
Token::Word(w) if w.quote_style != None => self.col += w.value.len() as u64 + 2,
338341
Token::Number(s, _) => self.col += s.len() as u64,
339342
Token::SingleQuotedString(s) => self.col += s.len() as u64,
343+
Token::Placeholder(s) => self.col += s.len() as u64,
340344
_ => self.col += 1,
341345
}
342346

@@ -596,6 +600,15 @@ impl<'a> Tokenizer<'a> {
596600
}
597601
'#' => self.consume_and_return(chars, Token::Sharp),
598602
'@' => self.consume_and_return(chars, Token::AtSign),
603+
'?' => self.consume_and_return(chars, Token::Placeholder(String::from("?"))),
604+
'$' => {
605+
chars.next();
606+
let s = peeking_take_while(
607+
chars,
608+
|ch| matches!(ch, '0'..='9' | 'A'..='Z' | 'a'..='z'),
609+
);
610+
Ok(Some(Token::Placeholder(String::from("$") + &s)))
611+
}
599612
other => self.consume_and_return(chars, Token::Char(other)),
600613
},
601614
None => Ok(None),

tests/sqlparser_common.rs

+40-1
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,14 @@
2020
2121
#[macro_use]
2222
mod test_utils;
23+
use sqlparser::test_utils::TestedDialects;
2324
use test_utils::{all_dialects, expr_from_projection, join, number, only, table, table_alias};
2425

2526
use matches::assert_matches;
2627
use sqlparser::ast::*;
27-
use sqlparser::dialect::{GenericDialect, SQLiteDialect};
28+
use sqlparser::dialect::{
29+
AnsiDialect, GenericDialect, MsSqlDialect, PostgreSqlDialect, SQLiteDialect, SnowflakeDialect,
30+
};
2831
use sqlparser::keywords::ALL_KEYWORDS;
2932
use sqlparser::parser::{Parser, ParserError};
3033

@@ -3693,6 +3696,42 @@ fn parse_drop_index() {
36933696
}
36943697
}
36953698

3699+
#[test]
3700+
fn test_placeholder() {
3701+
let sql = "SELECT * FROM student WHERE id = ?";
3702+
let ast = verified_only_select(sql);
3703+
assert_eq!(
3704+
ast.selection,
3705+
Some(Expr::BinaryOp {
3706+
left: Box::new(Expr::Identifier(Ident::new("id"))),
3707+
op: BinaryOperator::Eq,
3708+
right: Box::new(Expr::Value(Value::Placeholder("?".into())))
3709+
})
3710+
);
3711+
3712+
let dialects = TestedDialects {
3713+
dialects: vec![
3714+
Box::new(GenericDialect {}),
3715+
Box::new(PostgreSqlDialect {}),
3716+
Box::new(MsSqlDialect {}),
3717+
Box::new(AnsiDialect {}),
3718+
Box::new(SnowflakeDialect {}),
3719+
// Note: `$` is the starting word for the HiveDialect identifier
3720+
// Box::new(sqlparser::dialect::HiveDialect {}),
3721+
],
3722+
};
3723+
let sql = "SELECT * FROM student WHERE id = $Id1";
3724+
let ast = dialects.verified_only_select(sql);
3725+
assert_eq!(
3726+
ast.selection,
3727+
Some(Expr::BinaryOp {
3728+
left: Box::new(Expr::Identifier(Ident::new("id"))),
3729+
op: BinaryOperator::Eq,
3730+
right: Box::new(Expr::Value(Value::Placeholder("$Id1".into())))
3731+
})
3732+
);
3733+
}
3734+
36963735
#[test]
36973736
fn all_keywords_sorted() {
36983737
// assert!(ALL_KEYWORDS.is_sorted())

0 commit comments

Comments
 (0)