Skip to main content

wp_knowledge/
sql_route.rs

1//! SQL 路由工具:从查询 SQL 中识别命名 provider 前缀并剥离。
2//!
3//! OML 查询如 `select country_name from geo.public.ip_geo_city where ip_num = :ip_num`
4//! 中,首个表引用第一段 `geo` 指向已安装的命名 provider,`geo.` 前缀会被剥离后
5//! 再交给 PostgreSQL(否则 PG 会把三段名当作 `catalog.schema.table` 并报跨库错误)。
6
7use std::collections::HashMap;
8use std::collections::hash_map::DefaultHasher;
9use std::hash::{Hash, Hasher};
10use std::sync::{OnceLock, RwLock};
11
12use crate::runtime::runtime;
13
14/// 路由 memo 容量上限:防 SQL 文本无界增长。
15const ROUTE_MEMO_CAP: usize = 1024;
16
17struct RouteMemo {
18    epoch: u64,
19    map: HashMap<u64, Option<(String, String)>>,
20}
21
22fn route_memo() -> &'static RwLock<RouteMemo> {
23    static MEMO: OnceLock<RwLock<RouteMemo>> = OnceLock::new();
24    MEMO.get_or_init(|| {
25        RwLock::new(RouteMemo {
26            epoch: 0,
27            map: HashMap::new(),
28        })
29    })
30}
31
32fn sql_route_key(sql: &str) -> u64 {
33    let mut hasher = DefaultHasher::new();
34    sql.hash(&mut hasher);
35    hasher.finish()
36}
37
38/// 从 SQL 中提取路由信息:若首个表引用第一段命中已安装的命名 provider,
39/// 返回 `(provider_name, 剥离前缀后的 SQL)`;否则返回 `None`(走默认 provider)。
40///
41/// 结果按 `(sql hash, provider 注册表版本号)` 记忆化,热路径避免重复扫描;
42/// provider 安装/清理时版本号递增使 memo 失效。
43pub fn route_provider_sql(sql: &str) -> Option<(String, String)> {
44    let epoch = runtime().named_provider_epoch();
45    let key = sql_route_key(sql);
46    {
47        let memo = route_memo()
48            .read()
49            .expect("sql route memo read lock poisoned");
50        if memo.epoch == epoch
51            && let Some(routed) = memo.map.get(&key)
52        {
53            return routed.clone();
54        }
55    }
56    let routed = compute_route(sql);
57    let mut memo = route_memo()
58        .write()
59        .expect("sql route memo write lock poisoned");
60    if memo.epoch != epoch {
61        memo.epoch = epoch;
62        memo.map.clear();
63    }
64    if memo.map.len() >= ROUTE_MEMO_CAP {
65        memo.map.clear();
66    }
67    memo.map.insert(key, routed.clone());
68    routed
69}
70
71fn compute_route(sql: &str) -> Option<(String, String)> {
72    let table = first_table_name(sql)?;
73    let name = table.split('.').next()?;
74    if name.is_empty() || !runtime().provider_exists(name) {
75        return None;
76    }
77    Some((name.to_string(), strip_provider_prefix(sql, name)))
78}
79
80fn is_ident_byte(byte: u8) -> bool {
81    byte == b'_' || byte.is_ascii_alphanumeric()
82}
83
84/// 查找关键词,跳过字符串字面量、反引号、方括号内的内容。
85fn find_keyword(sql: &str, keyword: &[u8]) -> Option<usize> {
86    let bytes = sql.as_bytes();
87    let mut idx = 0usize;
88
89    while idx < bytes.len() {
90        match bytes[idx] {
91            b'\'' | b'"' => {
92                let quote = bytes[idx];
93                idx += 1;
94                while idx < bytes.len() {
95                    if bytes[idx] == quote {
96                        idx += 1;
97                        if idx < bytes.len() && bytes[idx] == quote {
98                            idx += 1;
99                            continue;
100                        }
101                        break;
102                    }
103                    idx += 1;
104                }
105            }
106            b'`' => {
107                idx += 1;
108                while idx < bytes.len() && bytes[idx] != b'`' {
109                    idx += 1;
110                }
111                idx += usize::from(idx < bytes.len());
112            }
113            b'[' => {
114                idx += 1;
115                while idx < bytes.len() && bytes[idx] != b']' {
116                    idx += 1;
117                }
118                idx += usize::from(idx < bytes.len());
119            }
120            b'-' if bytes.get(idx + 1) == Some(&b'-') => {
121                // 行注释:跳过到行尾
122                idx += 2;
123                while idx < bytes.len() && bytes[idx] != b'\n' {
124                    idx += 1;
125                }
126            }
127            b'/' if bytes.get(idx + 1) == Some(&b'*') => {
128                // 块注释:跳过到 `*/`
129                idx += 2;
130                while idx + 1 < bytes.len() && !(bytes[idx] == b'*' && bytes[idx + 1] == b'/') {
131                    idx += 1;
132                }
133                if idx + 1 < bytes.len() {
134                    idx += 2;
135                } else {
136                    idx = bytes.len();
137                }
138            }
139            _ => {
140                let end = idx + keyword.len();
141                if end <= bytes.len()
142                    && bytes[idx..end].eq_ignore_ascii_case(keyword)
143                    && idx
144                        .checked_sub(1)
145                        .is_none_or(|prev| !is_ident_byte(bytes[prev]))
146                    && bytes.get(end).is_none_or(|next| !is_ident_byte(*next))
147                {
148                    return Some(idx);
149                }
150                idx += 1;
151            }
152        }
153    }
154
155    None
156}
157
158/// 找到与 `open_pos` 处左括号匹配的右括号位置。
159fn matching_paren(sql: &str, open_pos: usize) -> Option<usize> {
160    let bytes = sql.as_bytes();
161    let mut depth = 0usize;
162    let mut idx = open_pos;
163
164    while idx < bytes.len() {
165        match bytes[idx] {
166            b'\'' | b'"' => {
167                let quote = bytes[idx];
168                idx += 1;
169                while idx < bytes.len() {
170                    if bytes[idx] == quote {
171                        idx += 1;
172                        if idx < bytes.len() && bytes[idx] == quote {
173                            idx += 1;
174                            continue;
175                        }
176                        break;
177                    }
178                    idx += 1;
179                }
180            }
181            b'`' => {
182                idx += 1;
183                while idx < bytes.len() && bytes[idx] != b'`' {
184                    idx += 1;
185                }
186                idx += usize::from(idx < bytes.len());
187            }
188            b'[' => {
189                idx += 1;
190                while idx < bytes.len() && bytes[idx] != b']' {
191                    idx += 1;
192                }
193                idx += usize::from(idx < bytes.len());
194            }
195            b'-' if bytes.get(idx + 1) == Some(&b'-') => {
196                idx += 2;
197                while idx < bytes.len() && bytes[idx] != b'\n' {
198                    idx += 1;
199                }
200            }
201            b'/' if bytes.get(idx + 1) == Some(&b'*') => {
202                idx += 2;
203                while idx + 1 < bytes.len() && !(bytes[idx] == b'*' && bytes[idx + 1] == b'/') {
204                    idx += 1;
205                }
206                if idx + 1 < bytes.len() {
207                    idx += 2;
208                } else {
209                    idx = bytes.len();
210                }
211            }
212            b'(' => {
213                depth += 1;
214                idx += 1;
215            }
216            b')' => {
217                depth = depth.checked_sub(1)?;
218                if depth == 0 {
219                    return Some(idx);
220                }
221                idx += 1;
222            }
223            _ => idx += 1,
224        }
225    }
226
227    None
228}
229
230/// 返回 SQL 中首个 FROM 表引用(跳过字符串与注释,递归处理子查询)。
231///
232/// 供路由与表判定复用(wp-oml 的 `resolve_sql_route` 也使用本实现)。
233pub fn first_table_name(sql: &str) -> Option<&str> {
234    let from_pos = find_keyword(sql, b"from")?;
235    let mut table_start = from_pos + b"from".len();
236    let bytes = sql.as_bytes();
237    while table_start < bytes.len() && bytes[table_start].is_ascii_whitespace() {
238        table_start += 1;
239    }
240
241    if bytes.get(table_start) == Some(&b'(') {
242        let close_pos = matching_paren(sql, table_start)?;
243        return first_table_name(&sql[table_start + 1..close_pos]);
244    }
245
246    let table_end = sql[table_start..]
247        .find(|c: char| c.is_ascii_whitespace() || matches!(c, ';' | ',' | ')'))
248        .map_or(sql.len(), |end| table_start + end);
249    let table = sql[table_start..table_end].trim();
250    if table.is_empty() { None } else { Some(table) }
251}
252
253/// 引号感知地剥离 SQL 中所有 `<name>.` 前缀(仅当处于标识符起始位置,
254/// 即前一个字符不是标识符或 `.`,且 `<name>.` 后紧跟标识符)。
255///
256/// 示例:`select geo.country_name from geo.public.t` → `select country_name from public.t`;
257/// 字符串字面量 `'geo.x'` 与 `geography.x` 不受影响。
258pub fn strip_provider_prefix(sql: &str, name: &str) -> String {
259    if name.is_empty() {
260        return sql.to_string();
261    }
262    let bytes = sql.as_bytes();
263    let name_bytes = name.as_bytes();
264    let mut out = String::with_capacity(sql.len());
265    let mut i = 0usize;
266    while i < bytes.len() {
267        let b = bytes[i];
268        // 引号/注释块:原样拷贝,跳过内部匹配
269        match b {
270            b'\'' | b'"' | b'`' => {
271                let quote = b;
272                let start = i;
273                i += 1;
274                while i < bytes.len() {
275                    if bytes[i] == quote {
276                        i += 1;
277                        if i < bytes.len() && bytes[i] == quote {
278                            i += 1;
279                            continue;
280                        }
281                        break;
282                    }
283                    i += 1;
284                }
285                out.push_str(&sql[start..i]);
286                continue;
287            }
288            b'[' => {
289                let start = i;
290                i += 1;
291                while i < bytes.len() && bytes[i] != b']' {
292                    i += 1;
293                }
294                i += usize::from(i < bytes.len());
295                out.push_str(&sql[start..i]);
296                continue;
297            }
298            b'-' if bytes.get(i + 1) == Some(&b'-') => {
299                let start = i;
300                while i < bytes.len() && bytes[i] != b'\n' {
301                    i += 1;
302                }
303                out.push_str(&sql[start..i]);
304                continue;
305            }
306            b'/' if bytes.get(i + 1) == Some(&b'*') => {
307                let start = i;
308                i += 2;
309                while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
310                    i += 1;
311                }
312                if i + 1 < bytes.len() {
313                    i += 2;
314                } else {
315                    i = bytes.len();
316                }
317                out.push_str(&sql[start..i]);
318                continue;
319            }
320            _ => {}
321        }
322        // 标识符起始边界:`name` 后紧跟 `.`,前一个字符不是标识符或 `.`。
323        let prev = i.checked_sub(1).map(|p| bytes[p]);
324        let is_boundary = prev.is_none_or(|p| !is_ident_byte(p) && p != b'.');
325        if is_boundary
326            && i + name_bytes.len() < bytes.len()
327            && &bytes[i..i + name_bytes.len()] == name_bytes
328            && bytes[i + name_bytes.len()] == b'.'
329            && bytes
330                .get(i + name_bytes.len() + 1)
331                .is_some_and(|next| is_ident_byte(*next) || matches!(*next, b'"' | b'`' | b'['))
332        {
333            i += name_bytes.len() + 1;
334            continue;
335        }
336        out.push(b as char);
337        i += 1;
338    }
339    out
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345    use crate::loader::ProviderKind;
346    use crate::runtime::{DatasourceId, runtime, runtime_test_guard};
347
348    fn install_test_provider(name: &str) {
349        use crate::error::KnowledgeResult;
350        use crate::mem::RowData;
351        use crate::runtime::ProviderExecutor;
352        use async_trait::async_trait;
353        use std::sync::Arc;
354        use wp_model_core::model::{DataField, DataType, Value};
355
356        struct TestProvider;
357
358        #[async_trait]
359        impl ProviderExecutor for TestProvider {
360            fn query(&self, _sql: &str) -> KnowledgeResult<Vec<RowData>> {
361                Ok(vec![vec![DataField::new(
362                    DataType::default(),
363                    "v",
364                    Value::Null,
365                )]])
366            }
367            fn query_fields(
368                &self,
369                _sql: &str,
370                _params: &[DataField],
371            ) -> KnowledgeResult<Vec<RowData>> {
372                self.query("")
373            }
374            fn query_row(&self, _sql: &str) -> KnowledgeResult<RowData> {
375                Ok(vec![DataField::new(DataType::default(), "v", Value::Null)])
376            }
377            fn query_named_fields(
378                &self,
379                _sql: &str,
380                _params: &[DataField],
381            ) -> KnowledgeResult<RowData> {
382                self.query_row("")
383            }
384        }
385
386        runtime()
387            .install_provider_named(
388                name,
389                ProviderKind::Postgres,
390                DatasourceId::from_seed(ProviderKind::Postgres, name),
391                |_generation| Ok(Arc::new(TestProvider)),
392                false,
393            )
394            .expect("install named provider");
395    }
396
397    #[test]
398    fn route_provider_sql_matches_installed_provider() {
399        let _guard = runtime_test_guard().lock().expect("guard");
400        install_test_provider("geo");
401
402        let (name, stripped) = route_provider_sql(
403            "select country_name from geo.public.ip_geo_city where ip_num = :ip",
404        )
405        .expect("route to geo");
406        assert_eq!(name, "geo");
407        assert_eq!(
408            stripped,
409            "select country_name from public.ip_geo_city where ip_num = :ip"
410        );
411    }
412
413    #[test]
414    fn route_provider_sql_falls_back_for_unknown_or_unqualified() {
415        let _guard = runtime_test_guard().lock().expect("guard");
416        install_test_provider("geo");
417
418        // 未安装的 provider 名 → 不路由
419        assert!(route_provider_sql("select a from nope.public.t").is_none());
420        // 无前缀 → 不路由
421        assert!(route_provider_sql("select a from ip_geo_city").is_none());
422    }
423
424    #[test]
425    fn strip_keeps_string_literals_and_mid_dotted_idents() {
426        let sql = "select geo.country_name from geo.public.t where name = 'geo.x'";
427        let out = strip_provider_prefix(sql, "geo");
428        assert_eq!(
429            out,
430            "select country_name from public.t where name = 'geo.x'"
431        );
432
433        // 非起始位置的 `geo.` 不剥离
434        let sql2 = "select a.geo.country from geo.public.t";
435        let out2 = strip_provider_prefix(sql2, "geo");
436        assert_eq!(out2, "select a.geo.country from public.t");
437
438        // 不是 `geo.` 前缀的标识符不受影响
439        let sql3 = "select geography.x from geo.public.t";
440        let out3 = strip_provider_prefix(sql3, "geo");
441        assert_eq!(out3, "select geography.x from public.t");
442    }
443
444    #[test]
445    fn strip_handles_distinct_qualifiers() {
446        let sql = "select group_concat(distinct geo.asset_type) from geo.asset_enrichment";
447        let out = strip_provider_prefix(sql, "geo");
448        assert_eq!(
449            out,
450            "select group_concat(distinct asset_type) from asset_enrichment"
451        );
452    }
453
454    #[test]
455    fn route_provider_sql_handles_subquery() {
456        let _guard = runtime_test_guard().lock().expect("guard");
457        install_test_provider("geo");
458
459        let sql = "select a from (select a from geo.public.t where x = 1) sub";
460        let (name, stripped) = route_provider_sql(sql).expect("route subquery to geo");
461        assert_eq!(name, "geo");
462        assert_eq!(
463            stripped,
464            "select a from (select a from public.t where x = 1) sub"
465        );
466    }
467
468    #[test]
469    fn route_provider_sql_prefix_requires_installed_provider() {
470        let _guard = runtime_test_guard().lock().expect("guard");
471        // 未安装的 provider 名(无任何测试安装),即使带前缀也不路由(SQL 原样落回默认 provider)
472        assert!(route_provider_sql("select a from ghost.public.t").is_none());
473    }
474
475    #[test]
476    fn route_provider_sql_ignores_from_inside_comment() {
477        let _guard = runtime_test_guard().lock().expect("guard");
478        install_test_provider("geo");
479
480        // 块注释内的 `from ghost.x` 不应干扰表引用解析
481        let sql = "select 1 /* from ghost.x */ from geo.public.t";
482        let (name, stripped) = route_provider_sql(sql).expect("route to geo");
483        assert_eq!(name, "geo");
484        assert_eq!(stripped, "select 1 /* from ghost.x */ from public.t");
485
486        // 行注释同理
487        let sql2 = "select 1 -- from ghost.x\nfrom geo.public.t";
488        let (name, stripped2) = route_provider_sql(sql2).expect("route to geo");
489        assert_eq!(name, "geo");
490        assert_eq!(stripped2, "select 1 -- from ghost.x\nfrom public.t");
491    }
492
493    #[test]
494    fn strip_skips_comments() {
495        let sql = "select geo.a -- geo.b\nfrom geo.public.t /* geo.c */";
496        let out = strip_provider_prefix(sql, "geo");
497        assert_eq!(out, "select a -- geo.b\nfrom public.t /* geo.c */");
498    }
499
500    #[test]
501    fn strip_keeps_backtick_quoted_identifiers() {
502        let sql = "select `geo.col` from geo.public.t";
503        let out = strip_provider_prefix(sql, "geo");
504        assert_eq!(out, "select `geo.col` from public.t");
505    }
506
507    #[test]
508    fn strip_handles_provider_name_with_underscore_and_digits() {
509        let sql = "select x from geo_db_v1.public.t where geo_db_v1.k = 1";
510        let out = strip_provider_prefix(sql, "geo_db_v1");
511        assert_eq!(out, "select x from public.t where k = 1");
512    }
513
514    #[test]
515    fn strip_does_not_touch_name_inside_longer_identifier() {
516        // `geox.`(name 后不是点)与 `xgeo.`(name 不在标识符起始)都不剥离
517        let sql = "select geox.a, xgeo.b from geo.public.t";
518        let out = strip_provider_prefix(sql, "geo");
519        assert_eq!(out, "select geox.a, xgeo.b from public.t");
520    }
521
522    #[test]
523    fn strip_supports_quoted_table_names() {
524        // 引号表名:`geo."t"` / `geo.\`t\`` / `geo.[t]` 也剥离前缀
525        let out = strip_provider_prefix("select x from geo.\"public\".\"t\"", "geo");
526        assert_eq!(out, "select x from \"public\".\"t\"");
527
528        let out = strip_provider_prefix("select x from geo.`public`.`t`", "geo");
529        assert_eq!(out, "select x from `public`.`t`");
530
531        let out = strip_provider_prefix("select x from geo.[public].[t]", "geo");
532        assert_eq!(out, "select x from [public].[t]");
533    }
534}