Skip to main content

rust_store_core/dialect/
raw.rs

1//! 原生 SQL 语句编译:命名占位符(`:name`)→ 方言占位符 + 读写推断(纯逻辑,无 IO)。
2//!
3//! 为宿主 `execute_raw` 的「两档」能力提供 core 侧唯一实现:
4//! - **位置档**(params 为数组/null):SQL 原样透传,占位符由调用方手写方言原生风格
5//!   (对标 SQLAlchemy `exec_driver_sql()`);
6//! - **命名档**(params 为对象):`:name` 按出现顺序编译为 [`Backend::placeholder`],
7//!   参数按引用顺序重排、同名复用(对标 SQLAlchemy `text()`)。
8//!
9//! 跳过边界(R4)保证字符串/注释/PG cast 内的冒号不被误编译;读写推断(R7)以
10//! 「默认写」为安全方向——误判为写至多路由主库,误判为读会读到旧数据。
11
12use std::collections::HashSet;
13
14use serde_json::Value;
15
16use super::Backend;
17
18/// 编译产物:最终 SQL + 重排后参数 + 读写标记
19#[derive(Debug)]
20pub struct RawStmt {
21    /// 位置档 = 原文;命名档 = 编译后文本
22    pub sql: String,
23    /// 位置档 = 原数组(顺序保持);命名档 = 按 `:name` 出现顺序重排
24    pub params: Vec<Value>,
25    /// 显式指定优先;否则按首词推断(R7)
26    pub is_write: bool,
27}
28
29/// 原生 SQL 语句编译。
30///
31/// - `params`:`Array` = 位置档(透传);`Object` = 命名档(`:name` 编译);
32///   `Null` = 位置档空参;其余 → Err(R1)。
33/// - `is_write`:`Some(b)` 显式采用;`None` 按首词推断(R7)。
34pub fn compile_raw_stmt(
35    backend: Backend,
36    text: &str,
37    params: Value,
38    is_write: Option<bool>,
39) -> Result<RawStmt, String> {
40    let trimmed = text.trim();
41    if trimmed.is_empty() {
42        return Err("原生 SQL 文本为空".to_string());
43    }
44    let is_write = is_write.unwrap_or_else(|| infer_is_write(trimmed));
45    match params {
46        // R2 位置档:原样透传(Null 视为空参数组)
47        Value::Array(items) => Ok(RawStmt { sql: text.to_string(), params: items, is_write }),
48        Value::Null => Ok(RawStmt { sql: text.to_string(), params: Vec::new(), is_write }),
49        // R3–R6 命名档:`:name` 编译 + 参数重排 + 一致性校验
50        Value::Object(names) => {
51            let (sql, ordered) = compile_named(backend, text, &names)?;
52            Ok(RawStmt { sql, params: ordered, is_write })
53        }
54        other => Err(format!(
55            "原生 SQL params 仅支持数组(位置档)或对象(命名档),收到 {}",
56            json_type_name(&other)
57        )),
58    }
59}
60
61/// R3–R6:命名档扫描编译。
62///
63/// 状态机逐字符扫描:引号字符串 / 行注释 / 块注释 / `::` cast 内部原样复制(R4);
64/// 裸 `?` / `$n` 不在编译范围,保持原样(PG `jsonb ? 'k'` 等操作符合法,误用时由驱动
65/// 报参数数错,不静默)。缺名 / 多余名显式 Err(R6,禁静默)。
66fn compile_named(
67    backend: Backend,
68    text: &str,
69    params: &serde_json::Map<String, Value>,
70) -> Result<(String, Vec<Value>), String> {
71    let chars: Vec<char> = text.chars().collect();
72    let n = chars.len();
73    let mut out = String::with_capacity(text.len() + 8);
74    let mut ordered: Vec<Value> = Vec::new();
75    let mut used: HashSet<String> = HashSet::new();
76    let mut i = 0;
77    while i < n {
78        let c = chars[i];
79        // R4:单引号 / 双引号字符串('' / "" 翻倍转义)
80        if c == '\'' || c == '"' {
81            let quote = c;
82            out.push(c);
83            i += 1;
84            while i < n {
85                if chars[i] == quote {
86                    if i + 1 < n && chars[i + 1] == quote {
87                        // 翻倍转义:两个字面引号,仍在字符串内
88                        out.push(quote);
89                        out.push(quote);
90                        i += 2;
91                        continue;
92                    }
93                    break;
94                }
95                out.push(chars[i]);
96                i += 1;
97            }
98            if i < n {
99                out.push(quote); // 收尾引号(未闭合引号原样保留,交由驱动报错)
100                i += 1;
101            }
102            continue;
103        }
104        // R4:`-- …` 行注释(至行尾)
105        if c == '-' && i + 1 < n && chars[i + 1] == '-' {
106            while i < n && chars[i] != '\n' {
107                out.push(chars[i]);
108                i += 1;
109            }
110            continue;
111        }
112        // R4:`/* … */` 块注释
113        if c == '/' && i + 1 < n && chars[i + 1] == '*' {
114            out.push(c);
115            out.push('*');
116            i += 2;
117            while i < n {
118                if chars[i] == '*' && i + 1 < n && chars[i + 1] == '/' {
119                    out.push('*');
120                    out.push('/');
121                    i += 2;
122                    break;
123                }
124                out.push(chars[i]);
125                i += 1;
126            }
127            continue;
128        }
129        // R4:`::` cast(PG),双冒号一并跳过
130        if c == ':' && i + 1 < n && chars[i + 1] == ':' {
131            out.push(':');
132            out.push(':');
133            i += 2;
134            continue;
135        }
136        // R3:`:name` 命名占位符(首字符字母/下划线,续字母/数字/下划线)
137        if c == ':' && i + 1 < n && (chars[i + 1].is_ascii_alphabetic() || chars[i + 1] == '_') {
138            let start = i + 1;
139            let mut j = start;
140            while j < n && (chars[j].is_ascii_alphanumeric() || chars[j] == '_') {
141                j += 1;
142            }
143            let name: String = chars[start..j].iter().collect();
144            match params.get(&name) {
145                Some(v) => {
146                    out.push_str(&backend.placeholder(ordered.len()));
147                    ordered.push(v.clone());
148                    used.insert(name);
149                }
150                None => return Err(format!("原生 SQL 命名参数 :{} 未在 params 中提供", name)),
151            }
152            i = j;
153            continue;
154        }
155        out.push(c);
156        i += 1;
157    }
158    // R6:params 中 text 未使用的名字 → Err
159    let mut unused: Vec<String> = params
160        .keys()
161        .filter(|k| !used.contains(*k))
162        .map(|k| format!(":{}", k))
163        .collect();
164    if !unused.is_empty() {
165        unused.sort();
166        return Err(format!(
167            "原生 SQL params 中存在未使用的命名参数: {}",
168            unused.join(", ")
169        ));
170    }
171    Ok((out, ordered))
172}
173
174/// R7:读写推断。trim 后跳过一层前导 `(` 与空白,取首词大写化;
175/// ∈ {SELECT, WITH, EXPLAIN, SHOW, PRAGMA, TABLE} → 读,其余 → 写(安全方向)。
176fn infer_is_write(trimmed: &str) -> bool {
177    let mut s = trimmed.trim_start();
178    if let Some(rest) = s.strip_prefix('(') {
179        s = rest.trim_start();
180    }
181    let word: String = s
182        .chars()
183        .take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
184        .collect();
185    !matches!(
186        word.to_ascii_uppercase().as_str(),
187        "SELECT" | "WITH" | "EXPLAIN" | "SHOW" | "PRAGMA" | "TABLE"
188    )
189}
190
191fn json_type_name(v: &Value) -> &'static str {
192    match v {
193        Value::Null => "null",
194        Value::Bool(_) => "bool",
195        Value::Number(_) => "number",
196        Value::String(_) => "string",
197        Value::Array(_) => "array",
198        Value::Object(_) => "object",
199    }
200}