1use std::collections::HashMap;
2use std::sync::Arc;
3
4pub type ParamBuffer = HashMap<(String, String), Arc<Vec<ParamValue>>>;
13
14#[derive(Debug, Clone)]
16pub enum ParamValue {
17 Quoted(String),
19 Bare(String),
21 Null,
23}
24
25impl ParamValue {
26 fn as_sql(&self) -> &str {
27 match self {
28 Self::Quoted(s) | Self::Bare(s) => s.as_str(),
29 Self::Null => "NULL",
30 }
31 }
32}
33
34#[must_use]
38pub fn parse_params(body: &str) -> Option<Vec<ParamValue>> {
39 let brace = memchr::memmem::find(body.as_bytes(), b"={")?;
41 let inner = body[brace + 2..].strip_suffix('}')?;
42
43 let mut params = Vec::new();
44 let mut rest = inner.trim_start();
46
47 while !rest.is_empty() {
48 let (value, tail) = parse_one_entry(rest)?;
49 params.push(value);
50 rest = tail.trim_start();
51 if let Some(t) = rest.strip_prefix(',') {
52 rest = t.trim_start();
53 }
54 }
55
56 Some(params)
57}
58
59fn parse_one_entry(s: &str) -> Option<(ParamValue, &str)> {
62 let s = s.strip_prefix('(')?;
63
64 let comma1 = memchr::memchr(b',', s.as_bytes())?;
66 let s = s[comma1 + 1..].trim_start();
67
68 let comma2 = memchr::memchr(b',', s.as_bytes())?;
70 let s = s[comma2 + 1..].trim_start();
71
72 if s.starts_with('\'') {
74 let bytes = s.as_bytes();
77 let mut i = 1;
78 loop {
79 let rel = memchr::memchr(b'\'', &bytes[i..])?;
80 i += rel + 1;
81 if i < bytes.len() && bytes[i] == b'\'' {
83 i += 1;
84 } else {
85 break;
86 }
87 }
88 let quoted = &s[..i];
90 let tail = s[i..].trim_start().strip_prefix(')')?;
91 Some((ParamValue::Quoted(String::from(quoted)), tail))
92 } else {
93 let end = memchr::memchr(b')', s.as_bytes())?;
95 let raw = s[..end].trim();
96 let tail = &s[end + 1..];
97 let value = if raw.is_empty() {
98 ParamValue::Null
99 } else {
100 ParamValue::Bare(String::from(raw))
101 };
102 Some((value, tail))
103 }
104}
105
106#[inline]
115#[must_use]
116pub fn count_placeholders(sql: &str) -> (usize, bool) {
117 let bytes = sql.as_bytes();
118 let len = bytes.len();
119 let mut i = 0;
120 let mut question_count = 0usize;
121 let mut max_colon_ordinal = 0usize;
122
123 while i < len {
124 let Some(rel) = memchr::memchr3(b'\'', b'?', b':', &bytes[i..]) else {
126 break; };
128 i += rel;
129
130 match bytes[i] {
131 b'\'' => {
132 i += 1;
134 loop {
135 let Some(r) = memchr::memchr(b'\'', &bytes[i..]) else {
136 i = len;
137 break;
138 };
139 i += r + 1;
140 if i < len && bytes[i] == b'\'' {
141 i += 1; } else {
143 break;
144 }
145 }
146 }
147 b'?' => {
148 question_count += 1;
149 i += 1;
150 }
151 b':' => {
152 let start = i + 1;
154 let mut j = start;
155 while j < bytes.len() && bytes[j].is_ascii_digit() {
156 j += 1;
157 }
158 if j > start {
159 let n: usize = bytes[start..j].iter().fold(0usize, |acc, &b| {
162 acc.saturating_mul(10).saturating_add((b - b'0') as usize)
163 });
164 max_colon_ordinal = max_colon_ordinal.max(n);
165 i = j;
166 } else {
167 i += 1;
168 }
169 }
170 _ => unreachable!(),
171 }
172 }
173
174 if max_colon_ordinal > 0 {
175 (max_colon_ordinal, true)
176 } else {
177 (question_count, false)
178 }
179}
180
181#[inline]
193fn apply_params_into(sql: &str, params: &[ParamValue], colon_style: bool, out: &mut Vec<u8>) {
194 out.clear();
195 if params.is_empty() {
196 out.extend_from_slice(sql.as_bytes());
197 return;
198 }
199
200 let extra: usize = params
201 .iter()
202 .map(|p| p.as_sql().len().saturating_sub(1))
203 .sum();
204 out.reserve(sql.len() + extra);
205 let bytes = sql.as_bytes();
206 let len = bytes.len();
207 let mut i = 0;
208 let mut seq_idx = 0usize; while i < len {
211 let special = if colon_style {
213 memchr::memchr2(b'\'', b':', &bytes[i..])
214 } else {
215 memchr::memchr2(b'\'', b'?', &bytes[i..])
216 };
217 let Some(rel) = special else {
218 out.extend_from_slice(&bytes[i..]);
219 break;
220 };
221 if rel > 0 {
223 out.extend_from_slice(&bytes[i..i + rel]);
224 }
225 i += rel;
226
227 match bytes[i] {
228 b'\'' => {
229 out.push(b'\'');
231 i += 1;
232 loop {
233 let Some(r) = memchr::memchr(b'\'', &bytes[i..]) else {
234 out.extend_from_slice(&bytes[i..]);
235 i = len;
236 break;
237 };
238 out.extend_from_slice(&bytes[i..=(i + r)]); i += r + 1;
240 if i < len && bytes[i] == b'\'' {
241 out.push(b'\''); i += 1;
243 } else {
244 break;
245 }
246 }
247 }
248 b'?' if !colon_style => {
249 if let Some(p) = params.get(seq_idx) {
250 out.extend_from_slice(p.as_sql().as_bytes());
251 } else {
252 out.push(b'?');
253 }
254 seq_idx += 1;
255 i += 1;
256 }
257 b':' if colon_style => {
258 let start = i + 1;
259 let mut j = start;
260 while j < len && bytes[j].is_ascii_digit() {
261 j += 1;
262 }
263 if j > start {
264 let n: usize = bytes[start..j].iter().fold(0usize, |acc, &b| {
267 acc.saturating_mul(10).saturating_add((b - b'0') as usize)
268 });
269 if let Some(p) = n.checked_sub(1).and_then(|idx| params.get(idx)) {
271 out.extend_from_slice(p.as_sql().as_bytes());
272 } else {
273 out.extend_from_slice(&bytes[i..j]);
274 }
275 i = j;
276 } else {
277 out.push(b':');
278 i += 1;
279 }
280 }
281 b => {
282 out.push(b);
283 i += 1;
284 }
285 }
286 }
287}
288
289#[cfg(test)]
307fn apply_params(sql: &str, params: &[ParamValue], colon_style: bool) -> String {
308 let mut buf = Vec::new();
309 apply_params_into(sql, params, colon_style, &mut buf);
310 String::from_utf8(buf).expect("apply_params produced invalid UTF-8")
311}
312
313pub fn compute_normalized<'a>(
356 record: &dm_database_parser_sqllog::Sqllog,
357 pm_sql: &str,
358 buffer: &mut ParamBuffer,
359 placeholder_override: Option<bool>,
360 scratch: &'a mut Vec<u8>,
361) -> Option<&'a str> {
362 if record.tag.is_none() {
363 if pm_sql.starts_with("PARAMS(") {
365 if let Some(params) = parse_params(pm_sql) {
366 buffer.insert(
367 (record.sess_id.clone(), record.statement.clone()),
368 Arc::new(params),
369 );
370 }
371 }
372 return None;
373 }
374
375 let tag = record.tag.as_deref()?;
377 if !matches!(tag, "INS" | "DEL" | "UPD" | "SEL") {
378 return None;
379 }
380
381 let (placeholder_count, detected_colon) = count_placeholders(pm_sql);
382 if placeholder_count == 0 {
383 return None;
384 }
385
386 let key = (record.sess_id.clone(), record.statement.clone());
387
388 let params = buffer.get(&key)?.clone();
389
390 let colon_style = placeholder_override.unwrap_or(detected_colon);
391
392 if params.len() != placeholder_count {
393 log::warn!(
394 "replace_parameters: param count mismatch (params={}, placeholders={}) for sql: {}",
395 params.len(),
396 placeholder_count,
397 pm_sql
398 .char_indices()
399 .nth(80)
400 .map_or(pm_sql, |(i, _)| &pm_sql[..i])
401 );
402 return None;
403 }
404
405 apply_params_into(pm_sql, ¶ms, colon_style, scratch);
406
407 debug_assert!(
415 std::str::from_utf8(scratch).is_ok(),
416 "apply_params_into produced invalid UTF-8 — safety invariant violated"
417 );
418 Some(std::str::from_utf8(scratch).expect("apply_params_into produced invalid UTF-8"))
419}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424
425 fn bare(s: &str) -> ParamValue {
426 ParamValue::Bare(String::from(s))
427 }
428 fn quoted(s: &str) -> ParamValue {
429 ParamValue::Quoted(String::from(s))
430 }
431
432 #[test]
435 fn test_parse_single_varchar() {
436 let params = parse_params("PARAMS(SEQNO, TYPE, DATA)={(0, VARCHAR, 'SM')}").unwrap();
437 assert_eq!(params.len(), 1);
438 assert_eq!(params[0].as_sql(), "'SM'");
439 }
440
441 #[test]
442 fn test_parse_mixed_types() {
443 let params = parse_params(
444 "PARAMS(SEQNO, TYPE, DATA)={(0, DEC, 3), (1, VARCHAR, 'send ok'), (2, DEC, 0), (3, INTEGER, 42)}",
445 )
446 .unwrap();
447 assert_eq!(params.len(), 4);
448 assert_eq!(params[0].as_sql(), "3");
449 assert_eq!(params[1].as_sql(), "'send ok'");
450 assert_eq!(params[2].as_sql(), "0");
451 assert_eq!(params[3].as_sql(), "42");
452 }
453
454 #[test]
455 fn test_parse_blob_empty() {
456 let params = parse_params("PARAMS(SEQNO, TYPE, DATA)={(0, DEC, 1), (1, BLOB, )}").unwrap();
457 assert_eq!(params.len(), 2);
458 assert_eq!(params[0].as_sql(), "1");
459 assert_eq!(params[1].as_sql(), "NULL");
460 }
461
462 #[test]
463 fn test_parse_quoted_with_escaped_quote() {
464 let params = parse_params("PARAMS(SEQNO, TYPE, DATA)={(0, VARCHAR, 'O''Brien')}").unwrap();
465 assert_eq!(params[0].as_sql(), "'O''Brien'");
466 }
467
468 #[test]
469 fn test_parse_invalid_returns_none() {
470 assert!(parse_params("not a params record").is_none());
471 }
472
473 #[test]
476 fn test_apply_single_string_param() {
477 let params = vec![quoted("'3USJ29'")];
478 let result = apply_params("WHERE code = ?", ¶ms, false);
479 assert_eq!(result, "WHERE code = '3USJ29'");
480 }
481
482 #[test]
483 fn test_apply_numeric_param() {
484 let params = vec![bare("42")];
485 let result = apply_params("WHERE id = ?", ¶ms, false);
486 assert_eq!(result, "WHERE id = 42");
487 }
488
489 #[test]
490 fn test_apply_null_param() {
491 let params = vec![ParamValue::Null];
492 let result = apply_params("WHERE tag = ?", ¶ms, false);
493 assert_eq!(result, "WHERE tag = NULL");
494 }
495
496 #[test]
497 fn test_apply_multiple_params() {
498 let params = vec![bare("2370075"), quoted("'SJ-1'"), ParamValue::Null];
499 let result = apply_params("VALUES (?, ?, ?)", ¶ms, false);
500 assert_eq!(result, "VALUES (2370075, 'SJ-1', NULL)");
501 }
502
503 #[test]
504 fn test_apply_no_placeholders() {
505 let params = vec![bare("1")];
506 let result = apply_params("SELECT 1", ¶ms, false);
507 assert_eq!(result, "SELECT 1");
508 }
509
510 #[test]
511 fn test_apply_skip_literal_contents() {
512 let params = vec![quoted("'real'")];
514 let result = apply_params("WHERE a = '?' AND b = ?", ¶ms, false);
515 assert_eq!(result, "WHERE a = '?' AND b = 'real'");
516 }
517
518 #[test]
519 fn test_apply_insert_with_function() {
520 let params = vec![bare("1"), quoted("'hello'"), bare("99")];
522 let result = apply_params(
523 "INSERT INTO t VALUES (?,current_timestamp,?,?)",
524 ¶ms,
525 false,
526 );
527 assert_eq!(
528 result,
529 "INSERT INTO t VALUES (1,current_timestamp,'hello',99)"
530 );
531 }
532
533 #[test]
534 fn test_apply_chinese_in_param() {
535 let params = vec![quoted("'张三'")];
536 let result = apply_params("WHERE name = ?", ¶ms, false);
537 assert_eq!(result, "WHERE name = '张三'");
538 }
539
540 #[test]
543 fn test_apply_colon_style_basic() {
544 let params = vec![bare("10"), quoted("'abc'")];
545 let result = apply_params("WHERE id = :1 AND code = :2", ¶ms, true);
546 assert_eq!(result, "WHERE id = 10 AND code = 'abc'");
547 }
548
549 #[test]
550 fn test_apply_colon_style_out_of_order() {
551 let params = vec![bare("1"), bare("2"), bare("3")];
552 let result = apply_params("SELECT :3, :1, :2", ¶ms, true);
553 assert_eq!(result, "SELECT 3, 1, 2");
554 }
555
556 #[test]
557 fn test_count_placeholders_question() {
558 let (count, colon_style) = count_placeholders("WHERE a = ? AND b = ?");
559 assert_eq!(count, 2);
560 assert!(!colon_style);
561 }
562
563 #[test]
564 fn test_count_placeholders_colon() {
565 let (count, colon_style) = count_placeholders("WHERE a = :1 AND b = :2 AND c = :3");
566 assert_eq!(count, 3);
567 assert!(colon_style);
568 }
569
570 #[test]
571 fn test_count_placeholders_skips_literals() {
572 let (count, colon_style) = count_placeholders("WHERE a = '?' AND b = ?");
573 assert_eq!(count, 1);
574 assert!(!colon_style);
575 }
576
577 #[test]
578 fn test_count_placeholders_none() {
579 let (count, colon_style) = count_placeholders("SELECT 1");
580 assert_eq!(count, 0);
581 assert!(!colon_style);
582 }
583
584 #[test]
585 fn test_count_placeholders_unclosed_string() {
586 let (count, _) = count_placeholders("SELECT 'unclosed");
588 assert_eq!(count, 0);
589 }
590
591 #[test]
592 fn test_count_placeholders_escaped_quote() {
593 let (count, _) = count_placeholders("WHERE name = 'O''Brien' AND id = ?");
595 assert_eq!(count, 1);
596 }
597
598 #[test]
599 fn test_count_placeholders_colon_not_followed_by_digit() {
600 let (count, colon_style) = count_placeholders("SELECT a::text");
602 assert_eq!(count, 0);
603 assert!(!colon_style);
604 }
605
606 #[test]
607 fn test_apply_params_empty_params_returns_sql_unchanged() {
608 let result = apply_params("SELECT * FROM t", &[], false);
610 assert_eq!(result, "SELECT * FROM t");
611 }
612
613 #[test]
614 fn test_apply_params_with_string_literal_verbatim_copy() {
615 let params = vec![bare("42")];
617 let result = apply_params("WHERE code = '?' AND id = ?", ¶ms, false);
618 assert_eq!(result, "WHERE code = '?' AND id = 42");
619 }
620
621 #[test]
622 fn test_apply_params_escaped_quote_in_literal() {
623 let params = vec![bare("1")];
625 let result = apply_params("WHERE name = 'O''Brien' AND id = ?", ¶ms, false);
626 assert_eq!(result, "WHERE name = 'O''Brien' AND id = 1");
627 }
628
629 #[test]
630 fn test_apply_params_unclosed_string_literal() {
631 let params = vec![bare("1")];
633 let result = apply_params("SELECT 'unclosed", ¶ms, false);
634 assert_eq!(result, "SELECT 'unclosed");
636 }
637}