mysql_common/
named_params.rs

1// Copyright (c) 2017 Anatoly Ikorsky
2//
3// Licensed under the Apache License, Version 2.0
4// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT
5// license <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
6// option. All files in the project carrying such notice may not be copied,
7// modified, or distributed except according to those terms.
8
9use std::borrow::Cow;
10
11/// Appears if a statement have both named and positional parameters.
12#[derive(Debug, Clone, Copy, Eq, PartialEq)]
13pub struct MixedParamsError;
14
15enum ParserState {
16    TopLevel,
17    // (string_delimiter, last_char)
18    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/// Parsed named params (see [`ParsedNamedParams::parse`]).
35#[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    /// Parse named params in the given query.
43    ///
44    /// Parameters must be named according to the following convention:
45    ///
46    /// * parameter name must start with either `_` or `a..z`
47    /// * parameter name may continue with `_`, `a..z` and `0..9`
48    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        // Vec<(colon_offset, start_offset, end_offset)>
53        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, // extensions and optimizer hints
113                    _ => 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    /// Returns a query string to pass to MySql (named parameters have been replaced with `?`).
171    pub fn query(&self) -> &[u8] {
172        &self.query
173    }
174
175    /// Names of named parameters in order of appearance.
176    ///
177    /// # Note
178    ///
179    /// * the returned slice might be empty if original query contained
180    ///   no named parameters.
181    /// * same name may appear multiple times.
182    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}