1use std::borrow::Cow;
10
11#[derive(Debug, Clone, Copy, Eq, PartialEq)]
13pub struct MixedParamsError;
14
15enum ParserState {
16 TopLevel,
17 InStringLiteral(u8, u8),
19 MaybeInNamedParam,
20 InNamedParam,
21 InSharpComment,
22 MaybeInDoubleDashComment1,
23 MaybeInDoubleDashComment2,
24 InDoubleDashComment,
25 MaybeInCComment1,
26 MaybeInCComment2,
27 InCComment,
28 MaybeExitCComment,
29 InQuotedIdentifier,
30}
31
32use self::ParserState::*;
33
34#[derive(Debug, Clone, Eq, PartialEq)]
36pub struct ParsedNamedParams<'a> {
37 query: Cow<'a, [u8]>,
38 params: Vec<Cow<'a, [u8]>>,
39}
40
41impl<'a> ParsedNamedParams<'a> {
42 pub fn parse(query: &'a [u8]) -> Result<Self, MixedParamsError> {
49 let mut state = TopLevel;
50 let mut have_positional = false;
51 let mut cur_param = 0;
52 let mut params = Vec::new();
54 for (i, c) in query.iter().enumerate() {
55 let mut rematch = false;
56 match state {
57 TopLevel => match c {
58 b':' => state = MaybeInNamedParam,
59 b'/' => state = MaybeInCComment1,
60 b'-' => state = MaybeInDoubleDashComment1,
61 b'#' => state = InSharpComment,
62 b'\'' => state = InStringLiteral(b'\'', b'\''),
63 b'"' => state = InStringLiteral(b'"', b'"'),
64 b'?' => have_positional = true,
65 b'`' => state = InQuotedIdentifier,
66 _ => (),
67 },
68 InStringLiteral(separator, prev_char) => match c {
69 x if *x == separator && prev_char != b'\\' => state = TopLevel,
70 x => state = InStringLiteral(separator, *x),
71 },
72 MaybeInNamedParam => match c {
73 b'a'..=b'z' | b'_' => {
74 params.push((i - 1, i, 0));
75 state = InNamedParam;
76 }
77 _ => rematch = true,
78 },
79 InNamedParam => {
80 if !matches!(c, b'a'..=b'z' | b'0'..=b'9' | b'_') {
81 params[cur_param].2 = i;
82 cur_param += 1;
83 rematch = true;
84 }
85 }
86 InSharpComment => {
87 if *c == b'\n' {
88 state = TopLevel
89 }
90 }
91 MaybeInDoubleDashComment1 => match c {
92 b'-' => state = MaybeInDoubleDashComment2,
93 _ => state = TopLevel,
94 },
95 MaybeInDoubleDashComment2 => {
96 if c.is_ascii_whitespace() && *c != b'\n' {
97 state = InDoubleDashComment
98 } else {
99 state = TopLevel
100 }
101 }
102 InDoubleDashComment => {
103 if *c == b'\n' {
104 state = TopLevel
105 }
106 }
107 MaybeInCComment1 => match c {
108 b'*' => state = MaybeInCComment2,
109 _ => state = TopLevel,
110 },
111 MaybeInCComment2 => match c {
112 b'!' | b'+' => state = TopLevel, _ => state = InCComment,
114 },
115 InCComment => {
116 if *c == b'*' {
117 state = MaybeExitCComment
118 }
119 }
120 MaybeExitCComment => match c {
121 b'/' => state = TopLevel,
122 _ => state = InCComment,
123 },
124 InQuotedIdentifier => {
125 if *c == b'`' {
126 state = TopLevel
127 }
128 }
129 }
130 if rematch {
131 match c {
132 b':' => state = MaybeInNamedParam,
133 b'\'' => state = InStringLiteral(b'\'', b'\''),
134 b'"' => state = InStringLiteral(b'"', b'"'),
135 _ => state = TopLevel,
136 }
137 }
138 }
139
140 if let InNamedParam = state {
141 params[cur_param].2 = query.len();
142 }
143
144 if !params.is_empty() {
145 if have_positional {
146 return Err(MixedParamsError);
147 }
148 let mut real_query = Vec::with_capacity(query.len());
149 let mut last = 0;
150 let mut out_params = Vec::with_capacity(params.len());
151 for (colon_offset, start, end) in params {
152 real_query.extend(&query[last..colon_offset]);
153 real_query.push(b'?');
154 last = end;
155 out_params.push(Cow::Borrowed(&query[start..end]));
156 }
157 real_query.extend(&query[last..]);
158 Ok(Self {
159 query: Cow::Owned(real_query),
160 params: out_params,
161 })
162 } else {
163 Ok(Self {
164 query: Cow::Borrowed(query),
165 params: vec![],
166 })
167 }
168 }
169
170 pub fn query(&self) -> &[u8] {
172 &self.query
173 }
174
175 pub fn params(&self) -> &[Cow<'a, [u8]>] {
183 &self.params
184 }
185}
186
187#[cfg(test)]
188mod test {
189 use super::*;
190
191 macro_rules! cows {
192 ($($l:expr),+ $(,)?) => { &[$(Cow::Borrowed(&$l[..]),)*] };
193 }
194
195 #[test]
196 fn should_parse_named_params() {
197 let result = ParsedNamedParams::parse(b":a :b").unwrap();
198 assert_eq!(result.query(), b"? ?");
199 assert_eq!(result.params(), cows!(b"a", b"b"));
200
201 let result = ParsedNamedParams::parse(b"SELECT (:a-10)").unwrap();
202 assert_eq!(result.query(), b"SELECT (?-10)");
203 assert_eq!(result.params(), cows!(b"a"));
204
205 let result = ParsedNamedParams::parse(br#"SELECT '"\':a' "'\"':c" :b"#).unwrap();
206 assert_eq!(result.query(), br#"SELECT '"\':a' "'\"':c" ?"#);
207 assert_eq!(result.params(), cows!(b"b"));
208
209 let result = ParsedNamedParams::parse(br":a_Aa:b").unwrap();
210 assert_eq!(result.query(), b"?Aa?");
211 assert_eq!(result.params(), cows!(b"a_", b"b"));
212
213 let result = ParsedNamedParams::parse(br"::b").unwrap();
214 assert_eq!(result.query(), b":?");
215 assert_eq!(result.params(), cows!(b"b"));
216
217 ParsedNamedParams::parse(b":a ?").unwrap_err();
218 }
219
220 #[test]
221 fn should_allow_numbers_in_param_name() {
222 let result = ParsedNamedParams::parse(b":a1 :a2").unwrap();
223 assert_eq!(result.query(), b"? ?");
224 assert_eq!(result.params(), cows!(b"a1", b"a2"));
225
226 let result = ParsedNamedParams::parse(b":1a :2a").unwrap();
227 assert_eq!(result.query(), b":1a :2a");
228 assert!(result.params().is_empty());
229 }
230
231 #[test]
232 fn special_characters_in_query() {
233 let result =
234 ParsedNamedParams::parse("SELECT 1 FROM été WHERE thing = :param;".as_bytes()).unwrap();
235 assert_eq!(
236 result.query(),
237 "SELECT 1 FROM été WHERE thing = ?;".as_bytes()
238 );
239 assert_eq!(result.params(), cows!(b"param"));
240 }
241
242 #[test]
243 fn comments_with_question_marks() {
244 let result = ParsedNamedParams::parse(
245 "SELECT 1 FROM my_table WHERE thing = :param;/* question\n mark '?' in multiline\n\
246 comment? */\n# ??- sharp comment -??\n-- dash-dash?\n/*! extention param :param2 */\n\
247 /*+ optimizer hint :param3 */; select :foo; # another comment?"
248 .as_bytes(),
249 )
250 .unwrap();
251 assert_eq!(
252 result.query(),
253 b"SELECT 1 FROM my_table WHERE thing = ?;/* question\n mark '?' in multiline\n\
254 comment? */\n# ??- sharp comment -??\n-- dash-dash?\n/*! extention param ? */\n\
255 /*+ optimizer hint ? */; select ?; # another comment?"
256 );
257 assert_eq!(
258 result.params(),
259 cows!(b"param", b"param2", b"param3", b"foo"),
260 );
261 }
262
263 #[test]
264 fn quoted_identifier() {
265 let result = ParsedNamedParams::parse(b"INSERT INTO `my:table` VALUES (?)").unwrap();
266 assert_eq!(result.query(), b"INSERT INTO `my:table` VALUES (?)");
267 assert!(result.params().is_empty());
268
269 let result = ParsedNamedParams::parse(b"INSERT INTO `my:table` VALUES (:foo)").unwrap();
270 assert_eq!(result.query(), b"INSERT INTO `my:table` VALUES (?)");
271 assert_eq!(result.params(), cows!(b"foo"));
272 }
273
274 #[cfg(feature = "nightly")]
275 mod bench {
276 use super::*;
277
278 #[bench]
279 fn parse_ten_named_params(bencher: &mut test::Bencher) {
280 bencher.iter(|| {
281 let result = ParsedNamedParams::parse(
282 r#"
283 SELECT :one, :two, :three, :four, :five, :six, :seven, :eight, :nine, :ten
284 "#,
285 )
286 .unwrap();
287 test::black_box(result);
288 });
289 }
290
291 #[bench]
292 fn parse_zero_named_params(bencher: &mut test::Bencher) {
293 bencher.iter(|| {
294 let result = ParsedNamedParams::parse(
295 r"
296 SELECT one, two, three, four, five, six, seven, eight, nine, ten
297 ",
298 )
299 .unwrap();
300 test::black_box(result);
301 });
302 }
303 }
304}