Skip to content

Commit ecb6ffc

Browse files
authored
Allow alphanumeric characters in SQLLite style parameters. (#32)
1 parent a948990 commit ecb6ffc

File tree

3 files changed

+80
-17
lines changed

3 files changed

+80
-17
lines changed

src/lib.rs

+49-3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ mod tokenizer;
1616
/// Formats whitespace in a SQL string to make it easier to read.
1717
/// Optionally replaces parameter placeholders with `params`.
1818
pub fn format(query: &str, params: &QueryParams, options: FormatOptions) -> String {
19-
let tokens = tokenizer::tokenize(query);
19+
let named_placeholders = matches!(params, QueryParams::Named(_));
20+
21+
let tokens = tokenizer::tokenize(query, named_placeholders);
2022
formatter::format(&tokens, params, options)
2123
}
2224

@@ -1067,21 +1069,40 @@ mod tests {
10671069
assert_eq!(format(input, &QueryParams::None, options), expected);
10681070
}
10691071

1072+
#[test]
1073+
fn it_recognizes_dollar_sign_alphanumeric_placeholders() {
1074+
let input = "SELECT $hash, $foo, $bar;";
1075+
let options = FormatOptions::default();
1076+
let expected = indoc!(
1077+
"
1078+
SELECT
1079+
$hash,
1080+
$foo,
1081+
$bar;"
1082+
);
1083+
1084+
assert_eq!(format(input, &QueryParams::None, options), expected);
1085+
}
1086+
10701087
#[test]
10711088
fn it_recognizes_dollar_sign_numbered_placeholders_with_param_values() {
1072-
let input = "SELECT $2, $3, $1;";
1089+
let input = "SELECT $2, $3, $1, $named, $4, $alias;";
10731090
let params = vec![
10741091
"first".to_string(),
10751092
"second".to_string(),
10761093
"third".to_string(),
1094+
"4th".to_string(),
10771095
];
10781096
let options = FormatOptions::default();
10791097
let expected = indoc!(
10801098
"
10811099
SELECT
10821100
second,
10831101
third,
1084-
first;"
1102+
first,
1103+
$named,
1104+
4th,
1105+
$alias;"
10851106
);
10861107

10871108
assert_eq!(
@@ -1090,6 +1111,31 @@ mod tests {
10901111
);
10911112
}
10921113

1114+
#[test]
1115+
fn it_recognizes_dollar_sign_alphanumeric_placeholders_with_param_values() {
1116+
let input = "SELECT $hash, $salt, $1, $2;";
1117+
let params = vec![
1118+
("hash".to_string(), "hash value".to_string()),
1119+
("salt".to_string(), "salt value".to_string()),
1120+
("1".to_string(), "number 1".to_string()),
1121+
("2".to_string(), "number 2".to_string()),
1122+
];
1123+
let options = FormatOptions::default();
1124+
let expected = indoc!(
1125+
"
1126+
SELECT
1127+
hash value,
1128+
salt value,
1129+
number 1,
1130+
number 2;"
1131+
);
1132+
1133+
assert_eq!(
1134+
format(input, &QueryParams::Named(params), options),
1135+
expected
1136+
);
1137+
}
1138+
10931139
#[test]
10941140
fn it_formats_query_with_go_batch_separator() {
10951141
let input = "SELECT 1 GO SELECT 2";

src/params.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ impl<'a> Params<'a> {
1212
}
1313

1414
pub fn get(&mut self, token: &'a Token<'a>) -> &'a str {
15+
let named_placeholder_token = token.key.as_ref().map_or(false, |key| key.named() != "");
16+
1517
match self.params {
1618
QueryParams::Named(params) => token
1719
.key
@@ -23,7 +25,7 @@ impl<'a> Params<'a> {
2325
.map(|param| param.1.as_str())
2426
})
2527
.unwrap_or(token.value),
26-
QueryParams::Indexed(params) => {
28+
QueryParams::Indexed(params) if !named_placeholder_token => {
2729
if let Some(key) = token.key.as_ref().and_then(|key| key.indexed()) {
2830
params
2931
.get(key)
@@ -38,7 +40,7 @@ impl<'a> Params<'a> {
3840
value
3941
}
4042
}
41-
QueryParams::None => token.value,
43+
_ => token.value,
4244
}
4345
}
4446
}

src/tokenizer.rs

+27-12
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,18 @@ use nom::{AsChar, Err, IResult};
1010
use std::borrow::Cow;
1111
use unicode_categories::UnicodeCategories;
1212

13-
pub(crate) fn tokenize(mut input: &str) -> Vec<Token<'_>> {
13+
pub(crate) fn tokenize(mut input: &str, named_placeholders: bool) -> Vec<Token<'_>> {
1414
let mut tokens: Vec<Token> = Vec::new();
1515

1616
let mut last_reserved_token = None;
1717

1818
// Keep processing the string until it is empty
19-
while let Ok(result) =
20-
get_next_token(input, tokens.last().cloned(), last_reserved_token.clone())
21-
{
19+
while let Ok(result) = get_next_token(
20+
input,
21+
tokens.last().cloned(),
22+
last_reserved_token.clone(),
23+
named_placeholders,
24+
) {
2225
if result.1.kind == TokenKind::Reserved {
2326
last_reserved_token = Some(result.1.clone());
2427
}
@@ -83,15 +86,16 @@ fn get_next_token<'a>(
8386
input: &'a str,
8487
previous_token: Option<Token<'a>>,
8588
last_reserved_token: Option<Token<'a>>,
89+
named_placeholders: bool,
8690
) -> IResult<&'a str, Token<'a>> {
8791
get_whitespace_token(input)
8892
.or_else(|_| get_comment_token(input))
8993
.or_else(|_| get_string_token(input))
9094
.or_else(|_| get_open_paren_token(input))
9195
.or_else(|_| get_close_paren_token(input))
92-
.or_else(|_| get_placeholder_token(input))
9396
.or_else(|_| get_number_token(input))
9497
.or_else(|_| get_reserved_word_token(input, previous_token, last_reserved_token))
98+
.or_else(|_| get_placeholder_token(input, named_placeholders))
9599
.or_else(|_| get_word_token(input))
96100
.or_else(|_| get_operator_token(input))
97101
}
@@ -288,12 +292,23 @@ fn get_close_paren_token(input: &str) -> IResult<&str, Token<'_>> {
288292
})
289293
}
290294

291-
fn get_placeholder_token(input: &str) -> IResult<&str, Token<'_>> {
292-
alt((
293-
get_ident_named_placeholder_token,
294-
get_string_named_placeholder_token,
295-
get_indexed_placeholder_token,
296-
))(input)
295+
fn get_placeholder_token(input: &str, named_placeholders: bool) -> IResult<&str, Token<'_>> {
296+
// The precedence changes based on 'named_placeholders' but not the exhaustiveness.
297+
// This is to ensure the formatting is the same even if parameters aren't used.
298+
299+
if named_placeholders {
300+
alt((
301+
get_ident_named_placeholder_token,
302+
get_string_named_placeholder_token,
303+
get_indexed_placeholder_token,
304+
))(input)
305+
} else {
306+
alt((
307+
get_indexed_placeholder_token,
308+
get_ident_named_placeholder_token,
309+
get_string_named_placeholder_token,
310+
))(input)
311+
}
297312
}
298313

299314
fn get_indexed_placeholder_token(input: &str) -> IResult<&str, Token<'_>> {
@@ -327,7 +342,7 @@ fn get_indexed_placeholder_token(input: &str) -> IResult<&str, Token<'_>> {
327342

328343
fn get_ident_named_placeholder_token(input: &str) -> IResult<&str, Token<'_>> {
329344
recognize(tuple((
330-
alt((char('@'), char(':'))),
345+
alt((char('@'), char(':'), char('$'))),
331346
take_while1(|item: char| {
332347
item.is_alphanumeric() || item == '.' || item == '_' || item == '$'
333348
}),

0 commit comments

Comments
 (0)