dm_database_sqllog2db/stats/
normalize.rs1#[must_use]
18pub fn normalize_sql(sql: &str) -> String {
19 let bytes = sql.as_bytes();
20 let len = bytes.len();
21 let mut output = Vec::with_capacity(len);
22 let mut cursor = 0usize;
23 let mut prev_was_ident_char = false;
24
25 while cursor < len {
26 let byte = bytes[cursor];
27 match byte {
28 b'\'' => {
29 cursor = skip_string_literal(bytes, cursor + 1, len);
30 output.push(b'?');
31 prev_was_ident_char = false;
32 }
33 byte_val if byte_val.is_ascii_digit() && !prev_was_ident_char => {
34 cursor = skip_number_literal(bytes, cursor, len);
35 output.push(b'?');
36 prev_was_ident_char = false;
37 }
38 b'-' | b'+'
39 if !prev_was_ident_char
40 && cursor + 1 < len
41 && bytes[cursor + 1].is_ascii_digit() =>
42 {
43 cursor = skip_number_literal(bytes, cursor + 1, len);
45 output.push(b'?');
46 prev_was_ident_char = false;
47 }
48 _ => {
49 output.push(byte);
50 prev_was_ident_char = byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'$';
51 cursor += 1;
52 }
53 }
54 }
55
56 String::from_utf8(output).expect("normalize_sql produced invalid UTF-8")
57}
58
59fn skip_string_literal(bytes: &[u8], start: usize, len: usize) -> usize {
65 let mut cursor = start;
66 loop {
67 let Some(relative_pos) = memchr::memchr(b'\'', &bytes[cursor..]) else {
68 return len;
70 };
71 cursor += relative_pos + 1;
72 if cursor < len && bytes[cursor] == b'\'' {
73 cursor += 1;
75 } else {
76 return cursor;
78 }
79 }
80}
81
82fn skip_number_literal(bytes: &[u8], start: usize, len: usize) -> usize {
88 let mut cursor = start;
89 while cursor < len && bytes[cursor].is_ascii_digit() {
91 cursor += 1;
92 }
93 if cursor + 1 < len && bytes[cursor] == b'.' && bytes[cursor + 1].is_ascii_digit() {
95 cursor += 1; while cursor < len && bytes[cursor].is_ascii_digit() {
97 cursor += 1;
98 }
99 }
100 cursor
101}
102
103#[cfg(test)]
104#[path = "../../tests/unit/stats/normalize.rs"]
105mod tests;