Skip to main content

rust_store_core/dialect/
mod.rs

1//! Mongo 方言 → 关系型 SQL 翻译层(纯逻辑,无 IO)
2//!
3//! 背景:core 按约定继续产出 **Mongo 命令 JSON**(`find`/`aggregate`/`countDocuments`/…)。
4//! 本模块提供一条**纯函数**翻译通道,把这些命令落成 MySQL / PostgreSQL / SQLite 各后端的
5//! 参数化 SQL 语句([`ir::SqlStmt`]),并负责把驱动返回的**平铺 JOIN 行**还原为嵌套 Mongo
6//! 文档([`row::restore_rows`])。
7//!
8//! 设计边界(对齐「用 MongoDB 方言,其他数据库适配」的要求):
9//! 1. [`translate::translate`] 只做 JSON → SQL 中间表示,完全不触数据库;Host 拿到
10//!    `SqlStmt` 后自行绑定驱动。
11//! 2. 关系模型采用**严格关系范式**:标量字段落在单表,`object`/`array` 展平为附属表,
12//!    关系用 SQL JOIN 解析。
13//! 3. 无法安全翻译的组合输出 `_unsupported` 标志 + warning,**绝不生成错误 SQL**。
14//! 4. 平铺行 → 嵌套文档的还原与 introspection 行 → schemaJSON 的映射都是纯逻辑,四侧对拍可复现。
15
16pub mod filter;
17pub mod introspect;
18pub mod ir;
19pub mod overlay;
20pub mod raw;
21pub mod row;
22pub mod select;
23pub mod translate;
24pub mod write;
25
26// 保持对外路径稳定:`crate::dialect::*` 直接可用(函数与其同名模块共存)
27pub use introspect::{introspect_to_schema_json, schema_def_from_rows};
28pub use overlay::merge_schema;
29pub use raw::{compile_raw_stmt, RawStmt};
30pub use row::restore_rows_json;
31pub use translate::translate;
32
33use crate::schema::Schema;
34
35/// 标量字段 → 列名(本表列);object/array 展平字段跳过;点号路径按整串处理。
36///
37/// **读([`select`])与写([`write`])共用此唯一实现。** 两侧语义若各自漂移会产生
38/// 读写不对称(读得到的列写不进 / 写进去的列读不出),故收口到此,两侧只做薄包装。
39pub(crate) fn scalar_column(schema: &Schema, field: &str) -> Option<String> {
40    if field.contains('.') {
41        let (head, _) = field.split_once('.')?;
42        // 点号字段:若 head 是 object/array(其子字段展平到附属表)则跳过;否则按整串处理
43        let head_type = schema.fields.get(head).map(|f| f.field_type.as_str());
44        if matches!(head_type, Some("object") | Some("array")) {
45            return None;
46        }
47        return Some(field.to_string());
48    }
49    match schema.fields.get(field).map(|f| f.field_type.as_str()) {
50        // vector 按标量列处理:它是单列,只是值以 JSON 文本 / 原生 vector 存放,
51        // 走列引用/条件生成与标量一致(读回解析见 dialect/write/returning_shape)
52        Some("object") | Some("array") => None,
53        _ => Some(field.to_string()),
54    }
55}
56
57/// 字段 → 列引用(过滤 / 投影 / 回读用;object/array 走 JSON 单列)。
58///
59/// 与 [`scalar_column`] 的差异:`scalar_column` 只认标量列(object/array 返回 `None`,
60/// 供关系键解析、分组键等**必须标量**的场景);本函数在其基础上把 object/array 字段
61/// 展开为 JSON 列引用(见执行文档 §4.5 / 不足清单 #3):
62/// - 标量字段 → [`ColumnRef::Scalar`](调用方按别名限定 + 引号化);
63/// - object/array 整值 → [`ColumnRef::Json`](单列存 JSON 文本,读取时解析;`JsonKind` 区分
64///   数组/对象,供 U1/U2 整值条件选择翻译方式);
65/// - object/array 的点号路径(U3 过滤 / U4 排序)→ [`ColumnRef::JsonPath`](提取表达式)。
66///
67/// 未声明字段返回 `None`(调用方据此显式报错,绝不静默)。
68pub(crate) fn field_column_ref(schema: &Schema, field: &str) -> Option<ColumnRef> {
69    if let Some((head, rest)) = field.split_once('.') {
70        let head_type = schema.fields.get(head).map(|f| f.field_type.as_str());
71        match head_type {
72            // U3/U4:object/array 点号路径 → JSON 提取(根列 head + 子路径 rest)
73            Some("object") | Some("array") => {
74                let path: Vec<String> = rest.split('.').map(String::from).collect();
75                Some(ColumnRef::JsonPath(head.to_string(), path))
76            }
77            // 点号字段的 head 非 object/array(如标量同名字段)→ 按整串列名处理(既有语义)
78            _ => Some(ColumnRef::Scalar(field.to_string())),
79        }
80    } else {
81        match schema.fields.get(field).map(|f| f.field_type.as_str()) {
82            Some("object") => Some(ColumnRef::Json(field.to_string(), JsonKind::Object)),
83            Some("array") => Some(ColumnRef::Json(field.to_string(), JsonKind::Array)),
84            // 已声明标量 / 未声明字段(与 `scalar_column` 宽松语义一致:按裸列名处理,
85            // 如物理主键 `_id` 不在 schema.fields 但恒为列)
86            _ => Some(ColumnRef::Scalar(field.to_string())),
87        }
88    }
89}
90
91/// JSON 整列的形态(`JsonKind`):决定 U1/U2 整值条件的翻译方式
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum JsonKind {
94    /// `object` 字段:整值条件按 JSON 结构等值比较(U2)
95    Object,
96    /// `array` 字段:整值条件按「数组包含元素 / 数组整体等值」翻译(U1)
97    Array,
98}
99
100/// 字段 → 列引用(见 [`field_column_ref`]):区分标量列 / JSON 整列 / JSON 点号路径。
101#[derive(Debug, Clone, PartialEq)]
102pub enum ColumnRef {
103    /// 标量列:字段名(调用方按表别名限定 + 引号化)
104    Scalar(String),
105    /// JSON 整列:object/array 字段落单列存 JSON 文本(读取时解析回嵌套对象)
106    Json(String, JsonKind),
107    /// JSON 点号路径(U3 过滤 / U4 排序):根列名 + 子路径(调用方构造提取表达式)
108    JsonPath(String, Vec<String>),
109}
110
111/// JSON 单列字段:object/array/vector 均落单列存 JSON 文本,读回时须解析。
112pub(crate) fn field_is_json(schema: &Schema, field: &str) -> bool {
113    matches!(
114        schema.fields.get(field).map(|f| f.field_type.as_str()),
115        Some("object") | Some("array") | Some("vector")
116    )
117}
118
119/// §9.7「布尔归一」:schema 字段是否为布尔类型(`boolean` 规范拼写 / `bool` 简写)。
120///
121/// 判定依据是 schema 的**声明类型**(不是驱动元数据,也不是物理列类型)——SQL 侧把布尔
122/// 存成 `TINYINT(1)` / `INTEGER` / `BOOLEAN` 都可能,唯一稳定的依据是 schema。命中即由
123/// [`row::restore_rows`] 在行还原时把 `0/1` 归一为 JSON `bool`。
124///
125/// 两种拼写都接受:core 的类型文档与 introspection 产出 `boolean`,而示例/用户 schema
126/// 常写 `bool`(如场景矩阵的 `paid` / `free`),二者语义相同。
127pub(crate) fn field_is_bool(schema: &Schema, field: &str) -> bool {
128    schema
129        .fields
130        .get(field)
131        .map(|f| {
132            f.field_type.eq_ignore_ascii_case("bool")
133                || f.field_type.eq_ignore_ascii_case("boolean")
134        })
135        .unwrap_or(false)
136}
137
138/// 保留物理名(I3):`_id` 恒原样;`__` 前缀内部合成名不翻译。
139fn is_reserved_physical(name: &str) -> bool {
140    name == "_id" || name.starts_with("__")
141}
142
143/// 逻辑名 → 本后端物理标识符(设计 §6,翻译单点在 [`crate::naming`])。
144///
145/// 保留名(I3:`_id` / `__` 前缀)原样返回,其余走 `naming` 归一为 snake_case(SQL 物理风格)。
146/// **只做正向(逻辑 → 物理)**;反向翻译不在本层。
147pub(crate) fn physical_of(name: &str) -> String {
148    if is_reserved_physical(name) {
149        name.to_string()
150    } else {
151        crate::naming::to_snake(&crate::naming::canonical(name))
152    }
153}
154
155/// 支持的数据库后端
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub enum Backend {
158    Mysql,
159    Postgres,
160    Sqlite,
161}
162
163impl Backend {
164    /// 从字符串解析后端(未知 → Err)
165    pub fn parse(s: &str) -> Result<Backend, String> {
166        match s.to_lowercase().as_str() {
167            "mysql" => Ok(Backend::Mysql),
168            "postgres" | "pg" | "postgresql" => Ok(Backend::Postgres),
169            "sqlite" => Ok(Backend::Sqlite),
170            _ => Err(format!("不支持的后端: {}", s)),
171        }
172    }
173
174    /// 双重引号标识符
175    ///
176    /// **仅用于别名 / 已物理名**(`t`、`r0`、`__present`、`__rn`、`fk`、`v`… 等合成名)。
177    /// 逻辑数据标识符(字段 / 关系字段 / 计算列 key / 表名)一律走 [`Backend::pcol`]。
178    pub fn quote_ident(&self, ident: &str) -> String {
179        match self {
180            Backend::Mysql => format!("`{}`", ident.replace('`', "``")),
181            _ => format!("\"{}\"", ident.replace('"', "\"\"")),
182        }
183    }
184
185    /// 列引用统一出口:`quote_ident(physical_of(logical))`(设计 §6 翻译边界「落库」)。
186    ///
187    /// 逻辑数据标识符 → 本后端物理名(snake_case)后再引号化。
188    pub(crate) fn pcol(&self, logical: &str) -> String {
189        self.quote_ident(&physical_of(logical))
190    }
191
192    /// 双精度浮点类型名(§9.7「数值归 double」)
193    ///
194    /// `SUM/AVG` 的结果在不同后端原生精度不同:MySQL `AVG(<int 列>)` 只保留 4 位小数
195    /// (`DECIMAL(scale+4)`),PG `AVG(<int 列>)` 返回精确 `numeric`,均与 Mongo `$avg`
196    /// 的 IEEE-754 double 存在差异。故 SQL 侧 `AVG` 前先把入参 `CAST` 到双精度。
197    pub fn double_type(&self) -> &'static str {
198        match self {
199            Backend::Mysql => "DOUBLE",
200            Backend::Postgres => "DOUBLE PRECISION",
201            Backend::Sqlite => "REAL",
202        }
203    }
204
205    /// 表名 → SQL:`database` / `schema` 可选限定(空串按 `None` 处理)。
206    ///
207    /// **PG 用 `schema` 限定**(`"schema"."table"`;database 由连接承载,**禁**跨库限定);
208    /// **MySQL/SQLite 用 `database` 限定**(`"db"."table"`)。表名走 [`physical_of`](物理名)。
209    /// 见设计 §3.2 / §6.4。
210    pub fn qualified_table(
211        &self,
212        database: Option<&str>,
213        schema: Option<&str>,
214        table: &str,
215    ) -> String {
216        let t = self.quote_ident(&physical_of(table));
217        let qual = match self {
218            Backend::Postgres => schema,
219            _ => database,
220        };
221        match qual.filter(|s| !s.is_empty()) {
222            Some(q) => format!("{}.{}", self.quote_ident(q), t),
223            None => t,
224        }
225    }
226
227    /// 占位符:SQLite/MySQL 用 `?`,PostgreSQL 用 `$n`(调用方保证按顺序传入 index)
228    pub fn placeholder(&self, _index: usize) -> String {
229        match self {
230            Backend::Postgres => format!("${}", _index + 1),
231            _ => "?".to_string(),
232        }
233    }
234
235    /// 后端标识(同 `Debug`,供绑定层序列化)
236    pub fn as_str(&self) -> &'static str {
237        match self {
238            Backend::Mysql => "mysql",
239            Backend::Postgres => "postgres",
240            Backend::Sqlite => "sqlite",
241        }
242    }
243
244    /// MySQL 无 RETURNING —— 需由 Host 编排 UPDATE + find 两段;PG/SQLite 原生支持
245    pub fn supports_returning(&self) -> bool {
246        !matches!(self, Backend::Mysql)
247    }
248
249    /// JSON 列的方言类型名(`object`/`array` 字段落单列存储的契约,见 §4.5 / 不足清单 #3)
250    ///
251    /// ⚠️ 跨方言对齐:`object`/`array` 字段在 SQL 后端**以单列存 JSON 文本** ——
252    /// MySQL `JSON` / PG `jsonb` / SQLite `TEXT`(JSON1 扩展)。core 不写 DDL(铁律 6),
253    /// 此常量供示例 DDL、introspection 与文档约定引用。
254    pub fn json_type_name(&self) -> &'static str {
255        match self {
256            Backend::Mysql => "JSON",
257            Backend::Postgres => "jsonb",
258            Backend::Sqlite => "TEXT",
259        }
260    }
261
262    /// JSON 点号路径 → **标量**提取表达式(U3 对象点号路径过滤 / U4 排序用)
263    ///
264    /// - MySQL:`JSON_UNQUOTE(JSON_EXTRACT(col, '$.a.b'))`
265    /// - PG:`(col #>> '{a,b}')`
266    /// - SQLite:`json_extract(col, '$.a.b')`
267    ///
268    /// 三者统一为「提取后标量(数值/字符串)」,供比较与 `ORDER BY` 使用;
269    /// `col` 须为已按后端引号化的**列标识符**,`path` 为点号各段(不含列名)。
270    ///
271    /// **注入防护(N1)**:每段先过 [`validate_json_path_seg`] 白名单校验,段含
272    /// `'` / `\` / `"` / `,` / 控制字符等 → 显式 `Err`(前缀 `ERR_GQL_PARSE:`),
273    /// 绝不静默丢弃/替换;输出层再按后端字面量口径转义(纵深防御)。
274    pub fn json_extract_scalar(&self, col: &str, path: &[&str]) -> Result<String, String> {
275        for seg in path {
276            validate_json_path_seg(seg)?;
277        }
278        match self {
279            Backend::Mysql => Ok(format!(
280                "JSON_UNQUOTE(JSON_EXTRACT({}, '{}'))",
281                col,
282                json_path_dollar(path)?
283            )),
284            // PG `#>>` 为数组字面量:段逐个按 PG 数组字面量规则转义后 join
285            Backend::Postgres => {
286                let segs: Vec<String> = path.iter().map(|s| pg_array_literal_seg(s)).collect();
287                Ok(format!("({} #>> '{{{}}}')", col, segs.join(",")))
288            }
289            Backend::Sqlite => Ok(format!(
290                "json_extract({}, '{}')",
291                col,
292                json_path_dollar(path)?
293            )),
294        }
295    }
296
297    /// JSON 数组「包含元素」谓词(U1:`{tags: "x"}` ⇒ 数组含 `"x"`)
298    ///
299    /// - MySQL:`JSON_CONTAINS(col, CAST(? AS JSON))`(候选是**值的 JSON 字面量**,如 `"x"` / `1`)
300    /// - PG:`col @> ?::jsonb`(候选是**单元素数组**的 JSON 文本,如 `["x"]`)
301    /// - SQLite:`EXISTS (SELECT 1 FROM json_each(col) WHERE json_each.value = ?)`(候选是标量原值)
302    ///
303    /// `ph` 为已生成的占位符(`?` / `$n`),参数由调用方按上述契约绑定(见 `filter::json_cond`)。
304    pub fn json_array_contains(&self, col: &str, ph: &str) -> String {
305        match self {
306            Backend::Mysql => format!("JSON_CONTAINS({}, CAST({} AS JSON))", col, ph),
307            Backend::Postgres => format!("{} @> {}::jsonb", col, ph),
308            Backend::Sqlite => format!(
309                "EXISTS (SELECT 1 FROM json_each({}) WHERE json_each.value = {})",
310                col, ph
311            ),
312        }
313    }
314
315    /// JSON 整值等值比较(U1 数组整体等值 / U2 对象结构等值)
316    ///
317    /// - MySQL:`col [<>] CAST(? AS JSON)`;PG:`col [<>] ?::jsonb`;SQLite:`json(col) [<>] json(?)`
318    ///
319    /// ⚠️ 跨后端语义差异(**已知不对齐点**,调用方须告警、禁静默,见「自动反馈原则」):
320    /// MySQL `JSON` / PG `jsonb` 的对象比较是**键序无关**的(存储即规范化),
321    /// 而 MongoDB 的对象等值匹配**字段顺序敏感**、SQLite(`json()` 文本 minify)**保留键序**。
322    /// 故 `object` 字段(U2)在 MySQL/PG 上可能比 Mongo/SQLite 命中更多行 ——
323    /// **不建议用于业务查询**,建议改用对象点号路径(U3);仅适合数据迁移 / 功能脚本。
324    pub fn json_value_eq(&self, col: &str, ph: &str, negate: bool) -> String {
325        let op = if negate { "<>" } else { "=" };
326        match self {
327            Backend::Mysql => format!("{} {} CAST({} AS JSON)", col, op, ph),
328            Backend::Postgres => format!("{} {} {}::jsonb", col, op, ph),
329            Backend::Sqlite => format!("json({}) {} json({})", col, op, ph),
330        }
331    }
332}
333
334/// JSON 点号路径段白名单校验(N1 注入防护唯一收口点)。
335///
336/// 路径段语义 = 字段名段,仅允许字母/数字/`_`/`-`/中文(CJK)等字段名常用字符;
337/// 含 `'`、`"`、`\`、`` ` ``、`{`、`}`、`,`、`[`、`]`、`$`、空格、控制字符等 → 显式 `Err`
338/// (前缀 `ERR_GQL_PARSE:`,沿用既有解析类前缀)。**禁静默丢弃/替换**。
339fn validate_json_path_seg(seg: &str) -> Result<(), String> {
340    if seg.is_empty() {
341        return Err("ERR_GQL_PARSE:JSON 点号路径段为空(非法路径段)".to_string());
342    }
343    let ok = seg.chars().all(|c| {
344        c.is_ascii_alphanumeric() || c == '_' || c == '-' || ('\u{4e00}'..='\u{9fff}').contains(&c)
345    });
346    if ok {
347        Ok(())
348    } else {
349        Err(format!(
350            "ERR_GQL_PARSE:JSON 点号路径段 \"{seg}\" 含非法字符(仅允许字母/数字/下划线/连字符/中文)"
351        ))
352    }
353}
354
355/// PG `#>>` 数组字面量元素转义:段内含 `"` / `\` / `,` / `{` / `}` / 空白或为空时,
356/// 须以双引号包裹(PG 数组字面量规则),段内 `"` / `\` 反斜杠转义。
357fn pg_array_literal_seg(seg: &str) -> String {
358    let needs_quote = seg.is_empty()
359        || seg
360            .chars()
361            .any(|c| matches!(c, '"' | '\\' | ',' | '{' | '}' | ' '));
362    if !needs_quote {
363        return seg.to_string();
364    }
365    let mut out = String::from("\"");
366    for ch in seg.chars() {
367        if ch == '"' || ch == '\\' {
368            out.push('\\');
369        }
370        out.push(ch);
371    }
372    out.push('"');
373    out
374}
375
376/// 点号路径 → MySQL/SQLite `JSON_EXTRACT` 的 `$` 路径字面量(`["a","b"]` → `$.a.b`)
377///
378/// 每段先过 [`validate_json_path_seg`](非法即 `Err`);输出层再按 MySQL/SQLite 单引号
379/// 字面量口径转义(`'` → `''`、`\` → `\\`)作纵深防御。
380fn json_path_dollar(path: &[&str]) -> Result<String, String> {
381    let mut s = String::from("$");
382    for seg in path {
383        validate_json_path_seg(seg)?;
384        s.push('.');
385        for ch in seg.chars() {
386            match ch {
387                '\'' => s.push_str("''"),
388                '\\' => s.push_str("\\\\"),
389                _ => s.push(ch),
390            }
391        }
392    }
393    Ok(s)
394}