oxisql_parse/
parameterize.rs1#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct ParameterizedSql {
16 pub template: String,
18 pub literals: Vec<String>,
20}
21
22pub fn parameterize(sql: &str) -> ParameterizedSql {
45 let bytes = sql.as_bytes();
46 let n = bytes.len();
47 let mut template = String::with_capacity(n);
48 let mut literals: Vec<String> = Vec::new();
49 let mut i = 0usize;
50
51 while i < n {
52 if i + 1 < n && bytes[i] == b'-' && bytes[i + 1] == b'-' {
54 while i < n && bytes[i] != b'\n' {
55 template.push(bytes[i] as char);
56 i += 1;
57 }
58 continue;
59 }
60
61 if i + 1 < n && bytes[i] == b'/' && bytes[i + 1] == b'*' {
63 template.push('/');
64 template.push('*');
65 i += 2;
66 while i + 1 < n && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
67 template.push(bytes[i] as char);
68 i += 1;
69 }
70 if i + 1 < n {
71 template.push('*');
72 template.push('/');
73 i += 2;
74 }
75 continue;
76 }
77
78 if bytes[i] == b'\'' {
80 let start = i;
81 i += 1;
82 while i < n {
83 if bytes[i] == b'\'' {
84 i += 1;
85 if i < n && bytes[i] == b'\'' {
86 i += 1;
88 } else {
89 break; }
91 } else {
92 i += 1;
93 }
94 }
95 literals.push(sql[start..i].to_string());
96 template.push('?');
97 continue;
98 }
99
100 if bytes[i] == b'"' {
102 template.push('"');
103 i += 1;
104 while i < n && bytes[i] != b'"' {
105 template.push(bytes[i] as char);
106 i += 1;
107 }
108 if i < n {
109 template.push('"');
110 i += 1;
111 }
112 continue;
113 }
114
115 if keyword_at(bytes, i, b"NULL") {
117 literals.push("NULL".to_string());
118 template.push('?');
119 i += 4;
120 continue;
121 }
122
123 if keyword_at(bytes, i, b"TRUE") {
125 literals.push("TRUE".to_string());
126 template.push('?');
127 i += 4;
128 continue;
129 }
130
131 if keyword_at(bytes, i, b"FALSE") {
133 literals.push("FALSE".to_string());
134 template.push('?');
135 i += 5;
136 continue;
137 }
138
139 if bytes[i].is_ascii_digit() {
141 let start = i;
142 i = consume_number(bytes, i);
143 literals.push(sql[start..i].to_string());
144 template.push('?');
145 continue;
146 }
147
148 if bytes[i] == b'-'
150 && i + 1 < n
151 && bytes[i + 1].is_ascii_digit()
152 && in_value_context(&template)
153 {
154 let start = i;
155 i += 1; i = consume_number(bytes, i);
157 literals.push(sql[start..i].to_string());
158 template.push('?');
159 continue;
160 }
161
162 template.push(bytes[i] as char);
164 i += 1;
165 }
166
167 ParameterizedSql { template, literals }
168}
169
170fn consume_number(bytes: &[u8], mut i: usize) -> usize {
174 let n = bytes.len();
175 while i < n && bytes[i].is_ascii_digit() {
176 i += 1;
177 }
178 if i < n && bytes[i] == b'.' {
179 i += 1;
180 while i < n && bytes[i].is_ascii_digit() {
181 i += 1;
182 }
183 }
184 if i < n && (bytes[i] == b'e' || bytes[i] == b'E') {
185 i += 1;
186 if i < n && (bytes[i] == b'+' || bytes[i] == b'-') {
187 i += 1;
188 }
189 while i < n && bytes[i].is_ascii_digit() {
190 i += 1;
191 }
192 }
193 i
194}
195
196fn keyword_at(bytes: &[u8], i: usize, keyword: &[u8]) -> bool {
199 let n = keyword.len();
200 if i + n > bytes.len() {
201 return false;
202 }
203 for (j, &k) in keyword.iter().enumerate() {
205 if bytes[i + j].to_ascii_uppercase() != k {
206 return false;
207 }
208 }
209 let after = i + n;
211 if after < bytes.len() && (bytes[after].is_ascii_alphanumeric() || bytes[after] == b'_') {
212 return false;
213 }
214 if i > 0 && (bytes[i - 1].is_ascii_alphanumeric() || bytes[i - 1] == b'_') {
216 return false;
217 }
218 true
219}
220
221fn in_value_context(template: &str) -> bool {
225 let last = template
226 .bytes()
227 .rev()
228 .find(|&b| !matches!(b, b' ' | b'\t' | b'\n' | b'\r'));
229 matches!(
230 last,
231 None | Some(b'=')
232 | Some(b'<')
233 | Some(b'>')
234 | Some(b'(')
235 | Some(b',')
236 | Some(b'[')
237 | Some(b'+')
238 | Some(b'*')
239 | Some(b'/')
240 | Some(b'!')
241 )
242}
243
244#[cfg(test)]
247mod tests {
248 use super::*;
249
250 #[test]
251 fn test_integer_literal() {
252 let p = parameterize("SELECT * FROM t WHERE id = 42");
253 assert_eq!(p.template, "SELECT * FROM t WHERE id = ?");
254 assert_eq!(p.literals, vec!["42"]);
255 }
256
257 #[test]
258 fn test_string_literal() {
259 let p = parameterize("SELECT * FROM t WHERE name = 'Alice'");
260 assert_eq!(p.template, "SELECT * FROM t WHERE name = ?");
261 assert_eq!(p.literals, vec!["'Alice'"]);
262 }
263
264 #[test]
265 fn test_float_literal() {
266 let p = parameterize("WHERE x = 3.14");
267 assert_eq!(p.template, "WHERE x = ?");
268 assert_eq!(p.literals, vec!["3.14"]);
269 }
270
271 #[test]
272 fn test_scientific_notation() {
273 let p = parameterize("WHERE x = 1.5e10");
274 assert_eq!(p.template, "WHERE x = ?");
275 assert_eq!(p.literals, vec!["1.5e10"]);
276 }
277
278 #[test]
279 fn test_null_keyword() {
280 let p = parameterize("WHERE x IS NULL");
281 assert_eq!(p.template, "WHERE x IS ?");
282 assert_eq!(p.literals, vec!["NULL"]);
283 }
284
285 #[test]
286 fn test_true_false_keywords() {
287 let p = parameterize("WHERE active = TRUE AND deleted = FALSE");
288 assert_eq!(p.template, "WHERE active = ? AND deleted = ?");
289 assert_eq!(p.literals, vec!["TRUE", "FALSE"]);
290 }
291
292 #[test]
293 fn test_negative_integer() {
294 let p = parameterize("WHERE x = -5");
295 assert_eq!(p.template, "WHERE x = ?");
296 assert_eq!(p.literals, vec!["-5"]);
297 }
298
299 #[test]
300 fn test_escaped_quote_in_string() {
301 let p = parameterize("WHERE name = 'it''s'");
303 assert_eq!(p.template, "WHERE name = ?");
304 assert_eq!(p.literals, vec!["'it''s'"]);
305 }
306
307 #[test]
308 fn test_double_quoted_identifier_not_parameterized() {
309 let p = parameterize(r#"SELECT "id" FROM t WHERE "id" = 1"#);
310 assert!(
311 p.template.contains(r#""id""#),
312 "double-quoted identifier should be preserved"
313 );
314 assert_eq!(p.literals, vec!["1"]);
315 }
316
317 #[test]
318 fn test_multiple_literals() {
319 let p = parameterize("SELECT * FROM t WHERE x = 1 AND y = 'foo' AND z = NULL");
320 assert_eq!(
321 p.template,
322 "SELECT * FROM t WHERE x = ? AND y = ? AND z = ?"
323 );
324 assert_eq!(p.literals.len(), 3);
325 }
326
327 #[test]
328 fn test_parameterize_is_fixpoint() {
329 let sql = "SELECT id FROM t WHERE x = 42 AND name = 'Bob' AND active = TRUE";
331 let first = parameterize(sql);
332 let second = parameterize(&first.template);
333 assert_eq!(
334 first.template, second.template,
335 "parameterize should be a fixpoint"
336 );
337 }
338
339 #[test]
340 fn test_line_comment_not_parameterized() {
341 let p = parameterize("SELECT 1 -- this is 42\nFROM t");
342 assert_eq!(
344 p.literals.len(),
345 1,
346 "only the numeric literal 1 should be replaced"
347 );
348 }
349
350 #[test]
351 fn test_subtraction_not_negative_number() {
352 let p = parameterize("WHERE x - 5 > 0");
354 assert!(
357 p.template.contains('-'),
358 "subtraction operator should not be consumed as negative literal"
359 );
360 }
361}