dm_database_parser_sqllog/
tools.rs

1//! 工具函数模块
2//!
3//! 提供了日志格式验证相关的工具函数,主要用于快速判断行是否为有效的记录起始行。
4//!
5//! # Feature 控制
6//!
7//! 本模块所有内容仅作为库内部工具,普通用户无需直接调用。
8
9// 时间戳格式常量
10const TIMESTAMP_LENGTH: usize = 23;
11const MIN_LINE_LENGTH: usize = 25;
12
13// 预定义的字节常量,避免重复创建
14const SPACE_BYTE: u8 = b' ';
15const OPEN_PAREN_BYTE: u8 = b'(';
16const CLOSE_PAREN_CHAR: char = ')';
17
18/// 判断字节数组是否为有效的时间戳格式
19///
20/// 验证时间戳格式是否为 "YYYY-MM-DD HH:MM:SS.mmm"(恰好 23 字节)。
21///
22/// # 参数
23///
24/// * `bytes` - 要检查的字节数组
25///
26/// # 返回
27///
28/// 如果是有效的时间戳格式返回 `true`,否则返回 `false`
29///
30/// # 示例
31///
32/// ```
33/// use dm_database_parser_sqllog::tools::is_ts_millis_bytes;
34///
35/// let valid = b"2025-08-12 10:57:09.548";
36/// assert!(is_ts_millis_bytes(valid));
37///
38/// let invalid = b"2025-08-12";
39/// assert!(!is_ts_millis_bytes(invalid));
40/// ```
41#[inline(always)]
42pub fn is_ts_millis_bytes(bytes: &[u8]) -> bool {
43    if bytes.len() != TIMESTAMP_LENGTH {
44        return false;
45    }
46
47    // Check separators
48    if bytes[4] != b'-'
49        || bytes[7] != b'-'
50        || bytes[10] != b' '
51        || bytes[13] != b':'
52        || bytes[16] != b':'
53        || bytes[19] != b'.'
54    {
55        return false;
56    }
57
58    // Check digits
59    // 0-3, 5-6, 8-9, 11-12, 14-15, 17-18, 20-22
60    let is_digit = |b: u8| b.is_ascii_digit();
61
62    is_digit(bytes[0])
63        && is_digit(bytes[1])
64        && is_digit(bytes[2])
65        && is_digit(bytes[3])
66        && is_digit(bytes[5])
67        && is_digit(bytes[6])
68        && is_digit(bytes[8])
69        && is_digit(bytes[9])
70        && is_digit(bytes[11])
71        && is_digit(bytes[12])
72        && is_digit(bytes[14])
73        && is_digit(bytes[15])
74        && is_digit(bytes[17])
75        && is_digit(bytes[18])
76        && is_digit(bytes[20])
77        && is_digit(bytes[21])
78        && is_digit(bytes[22])
79}
80
81/// 判断一行日志是否为记录起始行
82///
83/// 这是一个高性能的验证函数,用于快速判断一行文本是否为有效的日志记录起始行。
84///
85/// # 判断标准
86///
87/// 1. 行首 23 字节符合时间戳格式 `YYYY-MM-DD HH:mm:ss.SSS`
88/// 2. 时间戳后紧跟一个空格,然后是 meta 部分
89/// 3. Meta 部分用小括号包含
90/// 4. Meta 部分必须包含所有必需字段(client_ip 可选)
91/// 5. Meta 字段间以一个空格分隔
92/// 6. Meta 字段顺序固定:ep → sess → thrd_id → username → trxid → statement → appname → client_ip(可选)
93///
94/// # 参数
95///
96/// * `line` - 要检查的行
97///
98/// # 返回
99///
100/// 如果是有效的记录起始行返回 `true`,否则返回 `false`
101///
102/// # 示例
103///
104/// ```
105/// use dm_database_parser_sqllog::tools::is_record_start_line;
106///
107/// let valid = "2025-08-12 10:57:09.548 (EP[0] sess:123 thrd:456 user:alice trxid:789 stmt:999 appname:app) SELECT 1";
108/// assert!(is_record_start_line(valid));
109///
110/// let invalid = "This is not a log line";
111/// assert!(!is_record_start_line(invalid));
112/// ```
113/// 7. meta 部分结束后紧跟一个空格,然后是 body 部分。
114pub fn is_record_start_line(line: &str) -> bool {
115    // 早期退出:检查最小长度
116    let bytes = line.as_bytes();
117    if bytes.len() < MIN_LINE_LENGTH {
118        return false;
119    }
120
121    // 早期退出:验证时间戳格式(最快的失败路径)
122    if !is_ts_millis_bytes(&bytes[0..TIMESTAMP_LENGTH]) {
123        return false;
124    }
125
126    // 早期退出:检查时间戳后的分隔符 " ("
127    if bytes[23] != SPACE_BYTE || bytes[24] != OPEN_PAREN_BYTE {
128        return false;
129    }
130
131    // 早期退出:查找 meta 部分的右括号
132    let closing_paren_index = match line.find(CLOSE_PAREN_CHAR) {
133        Some(idx) => idx,
134        None => return false,
135    };
136
137    // 提取 meta 部分并验证字段
138    let meta_part = &line[25..closing_paren_index];
139    validate_meta_fields_fast(meta_part)
140}
141
142/// 一个更轻量的起始行判断,用于 RecordParser 快速判定(减少重复验证)
143///
144/// 该函数仅检查时间戳和括号位置,避免对 meta 字段做完整验证。
145/// 这适用于在解析过程中作为快速筛选器,真正的字段验证仍在解析阶段进行。
146pub fn is_probable_record_start_line(line: &str) -> bool {
147    let bytes = line.as_bytes();
148    if bytes.len() < MIN_LINE_LENGTH {
149        return false;
150    }
151    if !is_ts_millis_bytes(&bytes[0..TIMESTAMP_LENGTH]) {
152        return false;
153    }
154    if bytes[23] != SPACE_BYTE || bytes[24] != OPEN_PAREN_BYTE {
155        return false;
156    }
157    // 只需保证存在闭括号即可
158    line.find(CLOSE_PAREN_CHAR).is_some()
159}
160
161/// 快速验证 meta 字段(只验证 5 个必需字段的顺序和前缀)
162///
163/// 使用字节级操作,比字符串操作快约 2-3 倍
164#[inline]
165fn validate_meta_fields_fast(meta: &str) -> bool {
166    let bytes = meta.as_bytes();
167    let len = bytes.len();
168
169    // 最小长度检查:"EP[0] sess:1 thrd:1 user:a trxid:1" 约 38 字节
170    if len < 38 {
171        return false;
172    }
173
174    // 内联的字节前缀匹配函数
175    #[inline(always)]
176    fn check_prefix(bytes: &[u8], prefix: &[u8]) -> bool {
177        bytes.len() >= prefix.len() && &bytes[..prefix.len()] == prefix
178    }
179
180    // 内联的空格查找函数
181    #[inline(always)]
182    fn find_space(bytes: &[u8]) -> Option<usize> {
183        bytes.iter().position(|&b| b == b' ')
184    }
185
186    let mut pos = 0;
187
188    // 1. 验证 EP[ (必须在开头)
189    if !check_prefix(&bytes[pos..], b"EP[") {
190        return false;
191    }
192    pos = match find_space(&bytes[pos..]) {
193        Some(idx) => pos + idx + 1,
194        None => return false,
195    };
196    if pos >= len {
197        return false;
198    }
199
200    // 2. 验证 sess:
201    if !check_prefix(&bytes[pos..], b"sess:") {
202        return false;
203    }
204    pos = match find_space(&bytes[pos..]) {
205        Some(idx) => pos + idx + 1,
206        None => return false,
207    };
208    if pos >= len {
209        return false;
210    }
211
212    // 3. 验证 thrd:
213    if !check_prefix(&bytes[pos..], b"thrd:") {
214        return false;
215    }
216    pos = match find_space(&bytes[pos..]) {
217        Some(idx) => pos + idx + 1,
218        None => return false,
219    };
220    if pos >= len {
221        return false;
222    }
223
224    // 4. 验证 user:
225    if !check_prefix(&bytes[pos..], b"user:") {
226        return false;
227    }
228    pos = match find_space(&bytes[pos..]) {
229        Some(idx) => pos + idx + 1,
230        None => return false,
231    };
232    if pos >= len {
233        return false;
234    }
235
236    // 5. 验证 trxid:
237    check_prefix(&bytes[pos..], b"trxid:")
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    mod timestamp_tests {
245        use super::*;
246
247        #[test]
248        fn valid_timestamps() {
249            let valid_cases: &[&[u8]] = &[
250                b"2024-06-15 12:34:56.789",
251                b"2000-01-01 00:00:00.000",
252                b"2099-12-31 23:59:59.999",
253                b"2024-02-29 12:34:56.789", // 闰年
254            ];
255            for ts in valid_cases {
256                assert!(is_ts_millis_bytes(ts), "Failed for: {:?}", ts);
257            }
258        }
259
260        #[test]
261        fn wrong_length() {
262            let invalid_cases: &[&[u8]] = &[
263                b"2024-06-15 12:34:56",
264                b"2024-06-15 12:34:56.7",
265                b"2024-06-15 12:34:56.7890",
266                b"",
267                b"2024",
268            ];
269            for ts in invalid_cases {
270                assert!(!is_ts_millis_bytes(ts), "Should fail for: {:?}", ts);
271            }
272        }
273
274        #[test]
275        fn wrong_separator() {
276            let invalid_cases: &[&[u8]] = &[
277                b"2024-06-15 12:34:56,789", // 逗号代替点
278                b"2024/06/15 12:34:56.789", // 斜杠代替短横线
279                b"2024-06-15T12:34:56.789", // T 代替空格
280                b"2024-06-15-12:34:56.789", // 短横线代替空格
281                b"2024-06-15 12-34-56.789", // 短横线代替冒号
282            ];
283            for ts in invalid_cases {
284                assert!(!is_ts_millis_bytes(ts), "Should fail for: {:?}", ts);
285            }
286        }
287
288        #[test]
289        fn non_digits() {
290            let invalid_cases: &[&[u8]] = &[
291                b"202a-06-15 12:34:56.789",
292                b"2024-0b-15 12:34:56.789",
293                b"2024-06-1c 12:34:56.789",
294                b"2024-06-15 1d:34:56.789",
295                b"2024-06-15 12:3e:56.789",
296                b"2024-06-15 12:34:5f.789",
297                b"2024-06-15 12:34:56.78g",
298            ];
299            for ts in invalid_cases {
300                assert!(!is_ts_millis_bytes(ts), "Should fail for: {:?}", ts);
301            }
302        }
303
304        #[test]
305        fn special_chars() {
306            assert!(!is_ts_millis_bytes(b"2024-06-15 12:34:56.\x00\x00\x00"));
307            assert!(!is_ts_millis_bytes(b"\x002024-06-15 12:34:56.789"));
308        }
309    }
310
311    mod record_start_line_tests {
312        use super::*;
313
314        #[test]
315        fn valid_complete_line() {
316            let line = "2025-08-12 10:57:09.548 (EP[0] sess:0x178ebca0 thrd:757455 user:HBTCOMS_V3_PROD trxid:0 stmt:0x285eb060 appname: ip:::ffff:10.3.100.68) [SEL] select 1 from dual EXECTIME: 0(ms) ROWCOUNT: 1(rows) EXEC_ID: 289655178.";
317            assert!(is_record_start_line(line));
318        }
319
320        #[test]
321        fn valid_without_ip() {
322            let line = "2025-08-12 10:57:09.548 (EP[0] sess:0x178ebca0 thrd:757455 user:HBTCOMS_V3_PROD trxid:0 stmt:0x285eb060 appname:) [SEL] select 1 from dual";
323            assert!(is_record_start_line(line));
324        }
325
326        #[test]
327        fn minimal_valid() {
328            let line = "2025-08-12 10:57:09.548 (EP[0] sess:123 thrd:456 user:alice trxid:789 stmt:999 appname:app) body";
329            assert!(is_record_start_line(line));
330        }
331
332        #[test]
333        fn too_short() {
334            let short_lines = [
335                "2025-08-12 10:57:09.548",
336                "2025-08-12 10:57:09.548 (",
337                "",
338                "short",
339            ];
340            for line in &short_lines {
341                assert!(!is_record_start_line(line), "Should fail for: {}", line);
342            }
343        }
344
345        #[test]
346        fn invalid_timestamp() {
347            let line = "2025-08-12 10:57:09,548 (EP[0] sess:123 thrd:456 user:alice trxid:789 stmt:999 appname:app) body";
348            assert!(!is_record_start_line(line));
349        }
350
351        #[test]
352        fn format_errors() {
353            let invalid_lines = [
354                "2025-08-12 10:57:09.548(EP[0] sess:123 thrd:456 user:alice trxid:789 stmt:999 appname:app) body", // 无空格
355                "2025-08-12 10:57:09.548 EP[0] sess:123 thrd:456 user:alice trxid:789 stmt:999 appname:app) body", // 无左括号
356                "2025-08-12 10:57:09.548 (EP[0] sess:123 thrd:456 user:alice trxid:789 stmt:999 appname:app body", // 无右括号
357            ];
358            for line in &invalid_lines {
359                assert!(!is_record_start_line(line), "Should fail for: {}", line);
360            }
361        }
362
363        #[test]
364        fn insufficient_fields() {
365            // 现在支持 5 个字段的格式,测试只有 4 个字段的情况
366            let line = "2025-08-12 10:57:09.548 (EP[0] sess:123 thrd:456 user:alice) body";
367            assert!(!is_record_start_line(line));
368        }
369
370        #[test]
371        fn wrong_field_order() {
372            let line = "2025-08-12 10:57:09.548 (sess:123 EP[0] thrd:456 user:alice trxid:789 stmt:999 appname:app) body";
373            assert!(!is_record_start_line(line));
374        }
375
376        #[test]
377        fn missing_required_fields() {
378            // 只有前 5 个字段是必需的: EP, sess, thrd, user, trxid
379            let test_cases = [
380                (
381                    "2025-08-12 10:57:09.548 (sess:123 thrd:456 user:alice trxid:789 stmt:999 appname:app) body",
382                    "EP",
383                ),
384                (
385                    "2025-08-12 10:57:09.548 (EP[0] thrd:456 user:alice trxid:789 stmt:999 appname:app) body",
386                    "sess",
387                ),
388                (
389                    "2025-08-12 10:57:09.548 (EP[0] sess:123 user:alice trxid:789 stmt:999 appname:app) body",
390                    "thrd",
391                ),
392                (
393                    "2025-08-12 10:57:09.548 (EP[0] sess:123 thrd:456 trxid:789 stmt:999 appname:app) body",
394                    "user",
395                ),
396                (
397                    "2025-08-12 10:57:09.548 (EP[0] sess:123 thrd:456 user:alice stmt:999 appname:app) body",
398                    "trxid",
399                ),
400            ];
401            for (line, field) in &test_cases {
402                assert!(
403                    !is_record_start_line(line),
404                    "Should fail when missing {} field",
405                    field
406                );
407            }
408        }
409
410        #[test]
411        fn with_valid_ip() {
412            let line = "2025-08-12 10:57:09.548 (EP[0] sess:123 thrd:456 user:alice trxid:789 stmt:999 appname:app ip:::ffff:192.168.1.100) body";
413            assert!(is_record_start_line(line));
414        }
415
416        #[test]
417        fn with_invalid_ip_format() {
418            // IP 格式错误(应该是 ip:::ffff: 而不是 ip:)
419            let line = "2025-08-12 10:57:09.548 (EP[0] sess:123 thrd:456 user:alice trxid:789 stmt:999 appname:app ip:192.168.1.100) body";
420            // 这个格式实际上会通过,因为 "ip:192.168.1.100)" 会被当作 appname 值的一部分
421            // 让我们测试一个真正无效的格式
422            assert!(is_record_start_line(line));
423        }
424
425        #[test]
426        fn complex_field_values() {
427            let line = "2025-08-12 10:57:09.548 (EP[123] sess:0xABCD1234 thrd:9999999 user:USER_WITH_UNDERSCORES trxid:12345678 stmt:0xFFFFFFFF appname:app-name-with-dashes ip:::ffff:10.20.30.40) SELECT * FROM table";
428            assert!(is_record_start_line(line));
429        }
430
431        #[test]
432        fn empty_appname() {
433            let line = "2025-08-12 10:57:09.548 (EP[0] sess:123 thrd:456 user:alice trxid:789 stmt:999 appname:) body";
434            assert!(is_record_start_line(line));
435        }
436
437        #[test]
438        fn continuation_line() {
439            let continuation = "    SELECT * FROM users WHERE id = 1";
440            assert!(!is_record_start_line(continuation));
441        }
442
443        #[test]
444        fn double_space_in_meta() {
445            // v0.1.3+: 更严格的验证,要求字段之间只有单个空格
446            // 双空格会导致验证失败
447            let line = "2025-08-12 10:57:09.548 (EP[0]  sess:123 thrd:456 user:alice trxid:789 stmt:999 appname:app) body";
448            // 新版本中这不会通过,因为我们要求严格的单空格分隔
449            assert!(!is_record_start_line(line));
450
451            // 正确的格式应该是单空格
452            let valid_line = "2025-08-12 10:57:09.548 (EP[0] sess:123 thrd:456 user:alice trxid:789 stmt:999 appname:app) body";
453            assert!(is_record_start_line(valid_line));
454        }
455    }
456}