dm_database_parser_sqllog/
tools.rs

1use once_cell::sync::Lazy;
2use super::matcher::Matcher;
3
4// 模式按照要求的顺序列出
5#[allow(dead_code)]
6static DEFAULT_PATTERNS: &[&str] = &[
7    "EP[", "sess:", "thrd:", "user:", "trxid:", "stmt:", "appname:",
8];
9
10// 懒惰初始化一个基于 DEFAULT_PATTERNS 的 Matcher,以便现有测试可以提前预热。
11#[allow(dead_code)]
12static DEFAULT_MATCHER: Lazy<Matcher> = Lazy::new(|| Matcher::from_patterns(DEFAULT_PATTERNS));
13
14#[inline(always)]
15pub fn is_ts_millis(s: &str) -> bool {
16    let bytes = s.as_bytes();
17    if bytes.len() != 23 {
18        return false;
19    }
20    // 固定符号位置
21    if bytes[4] != b'-'
22        || bytes[7] != b'-'
23        || bytes[10] != b' '
24        || bytes[13] != b':'
25        || bytes[16] != b':'
26        || bytes[19] != b'.'
27    {
28        return false;
29    }
30    // 其余位置必须为数字
31    for &i in &[
32        0usize, 1, 2, 3, 5, 6, 8, 9, 11, 12, 14, 15, 17, 18, 20, 21, 22,
33    ] {
34        if !bytes[i].is_ascii_digit() {
35            return false;
36        }
37    }
38    true
39}
40
41/// `is_ts_millis` 的字节切片变体,以避免在扫描大缓冲区时创建临时 `&str` 切片。
42/// 期望输入恰好为 23 字节。
43#[inline(always)]
44pub fn is_ts_millis_bytes(bytes: &[u8]) -> bool {
45    if bytes.len() != 23 {
46        return false;
47    }
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    for &i in &[
59        0usize, 1, 2, 3, 5, 6, 8, 9, 11, 12, 14, 15, 17, 18, 20, 21, 22,
60    ] {
61        if !bytes[i].is_ascii_digit() {
62            return false;
63        }
64    }
65    true
66}
67
68/// 判断一行是否为 sqllog 的“记录起始行”。
69///
70/// 判定规则(严格匹配当前实现):
71/// 1. 要求时间戳严格位于行首(不允许前导空白);
72/// 2. 行首的前 23 个字符必须正好是时间戳,格式为 `YYYY-MM-DD HH:MM:SS.mmm`(由 `is_ts_millis` 验证);
73/// 3. 在时间戳之后必须存在一对圆括号 `(...)`,括号内为元信息(元数据);
74/// 4. 元信息中必须包含以下 7 个关键短语,且它们首次出现的顺序必须严格为:
75///    EP[ -> sess: -> thrd: -> user: -> trxid: -> stmt: -> appname:
76///
77/// 输入/输出:
78/// - 输入:单行文本 `line: &str`(可以包含前导空白);
79/// - 输出:bool,若满足上述所有条件则返回 true,否则返回 false。
80///
81/// 复杂度与性能:
82/// - 使用双数组 Aho-Corasick(daachorse)自动机一次扫描元信息(O(n + total_matches)),比多次子串查找更高效;
83/// - 该函数在最坏情况下仍然是线性相对输入长度的;
84/// - 适合在对大量日志行做快速分组时使用。
85///
86/// 边界情况与注意事项:
87/// - 关键字必须出现在括号内部;若关键字在括号外出现则视为不匹配;
88/// - 关键字匹配是基于文本子串(大小写敏感);如果需要忽略大小写或支持更多变体,应在自动机构建时调整或归一化输入;
89/// - 只检查关键字的首次出现位置,以验证顺序;若关键字重复,只看第一次出现的位置;
90/// - 时间戳严格按字符位置校验,不尝试解析为日期/时间类型以节省分配与解析开销。
91pub fn is_record_start(line: &str) -> bool {
92    // 1) 要求时间戳严格从行首开始(不允许前导空白)
93    //    因为日志格式保证时间戳占据前 23 个字符的位置
94    if line.len() < 23 {
95        return false;
96    }
97
98    // 2) 校验时间戳格式(前 23 字符)
99    if !is_ts_millis(&line[0..23]) {
100        return false;
101    }
102
103    // 3) 在时间戳之后查找第一对圆括号,括号内为元信息
104    // 格式固定:时间戳后紧跟一个空格,然后立刻是 '(',其后为元信息直到匹配的 ')'
105    let rest = &line[23..];
106    // 检查至少有两个字符(空格 + '(')
107    let rest_bytes = rest.as_bytes();
108    if rest_bytes.len() < 2 || rest_bytes[0] != b' ' || rest_bytes[1] != b'(' {
109        return false;
110    }
111    // open 相对于 rest 的索引
112    let open = 1usize; // 紧接在空格之后
113    let close = match rest[open..].find(')') {
114        Some(p) => open + p,
115        // 没有匹配的 ')' 则不是起始行
116        None => return false,
117    };
118    // 元信息字符串(不包含括号)
119    let meta = &rest[open + 1..close];
120
121    // 4) 使用 Matcher 在 meta 中一次扫描所有模式,记录每个模式的首次出现位置
122    //    patterns 的定义顺序就是我们要求的出现顺序(见静态 DEFAULT_PATTERNS 定义)
123    let matcher = &*DEFAULT_MATCHER;
124    let fp = matcher.find_first_positions(meta.as_bytes());
125    let mut first_pos: [Option<usize>; 7] = [None, None, None, None, None, None, None];
126    for (i, p) in fp.into_iter().enumerate().take(first_pos.len()) {
127        first_pos[i] = p;
128    }
129
130    // 要求全部 7 个模式均出现
131    if first_pos.iter().any(|p| p.is_none()) {
132        return false;
133    }
134
135    // 验证首次出现位置严格递增,保证顺序为 EP -> sess -> thrd -> user -> trxid -> stmt -> appname
136    let mut prev: Option<usize> = None;
137    for p in &first_pos {
138        let cur = p.unwrap();
139        if let Some(prev_pos) = prev {
140            // 若当前位置小于等于前一个位置,说明顺序错误或重叠
141            if cur <= prev_pos {
142                return false;
143            }
144        }
145        prev = Some(cur);
146    }
147
148    true
149}
150
151/// 预热内部自动机和相关静态结构,以便第一次计时调用不包含延迟初始化分配。
152#[allow(dead_code)]
153pub fn prewarm() {
154    // 强制初始化静态自动机
155    let _ = &*DEFAULT_MATCHER;
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn test_is_ts_millis() {
164        let valid_ts = "2023-10-05 14:23:45.123";
165        let invalid_ts_1 = "2023/10/05 14:23:45.123"; // 错误的分隔符
166        let invalid_ts_2 = "2023-10-05 14:23:45"; // 缺少毫秒部分
167        let invalid_ts_3 = "2023-10-05T14:23:45.123"; // 日期和时间之间分隔符错误
168        let invalid_ts_4 = "2023-10-05 14:23:4a.123"; // 包含非数字字符
169
170        assert!(is_ts_millis(valid_ts));
171        assert!(!is_ts_millis(invalid_ts_1));
172        assert!(!is_ts_millis(invalid_ts_2));
173        assert!(!is_ts_millis(invalid_ts_3));
174        assert!(!is_ts_millis(invalid_ts_4));
175    }
176
177    #[test]
178    fn test_is_record_start_basic() {
179        let line = "2025-08-12 10:57:09.561 (EP[0] sess:abc thrd:1 user:joe trxid:123 stmt:0x1 appname:my)";
180        assert!(is_record_start(line));
181    }
182
183    #[test]
184    fn test_is_record_start_different_order() {
185        // 相同关键字但顺序错误现在不应被接受
186        let line = "2025-08-12 10:57:09.561 (user:joe appname:my trxid:123 thrd:1 sess:abc stmt:0x1 EP[0])";
187        assert!(!is_record_start(line));
188    }
189
190    #[test]
191    fn test_is_record_start_correct_order_complex() {
192        // 关键字可能穿插出现,但仍需保持所需顺序 EP -> sess -> thrd -> user -> trxid -> stmt -> appname
193        let line = "2025-08-12 10:57:09.561 (EP[0] foobar sess:abc baz thrd:1 qux user:joe trxid:123 stmt:0x1 zz appname:my)";
194        assert!(is_record_start(line));
195    }
196
197    #[test]
198    fn test_is_record_start_leading_whitespace() {
199        // 有前导空格的行现在不被接受(时间戳必须在行首)
200        let line = "   2025-08-12 10:57:09.561 (EP[0] sess:abc thrd:1 user:joe trxid:123 stmt:0x1 appname:my)";
201        assert!(!is_record_start(line));
202    }
203
204    #[test]
205    fn test_is_record_start_missing_keyword() {
206        let line = "2025-08-12 10:57:09.561 (EP[0] sess:abc thrd:1 trxid:123 stmt:0x1 appname:my)"; // 缺少 user
207        assert!(!is_record_start(line));
208    }
209
210    #[test]
211    fn test_is_record_start_keyword_outside_parentheses() {
212        let line =
213            "2025-08-12 10:57:09.561 EP[0] sess:abc thrd:1 user:joe trxid:123 stmt:0x1 appname:my";
214        // 因为我们要求元数据位于括号内,因此应返回 false
215        assert!(!is_record_start(line));
216    }
217
218    #[test]
219    fn test_is_record_start_no_parentheses() {
220        let line = "2025-08-12 10:57:09.561 some random text";
221        assert!(!is_record_start(line));
222    }
223
224    #[test]
225    fn test_is_record_start_invalid_timestamp() {
226        let line =
227            "2025-08-12T10:57:09 (EP[0] sess:abc thrd:1 user:joe trxid:123 stmt:0x1 appname:my)";
228        assert!(!is_record_start(line));
229    }
230}