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 row;
21pub mod select;
22pub mod translate;
23pub mod write;
24
25// 保持对外路径稳定:`crate::dialect::*` 直接可用(函数与其同名模块共存)
26pub use introspect::{introspect_to_schema_json, schema_def_from_rows};
27pub use overlay::merge_schema;
28pub use row::restore_rows_json;
29pub use translate::translate;
30
31use crate::schema::Schema;
32
33/// 标量字段 → 列名(本表列);object/array 展平字段跳过;点号路径按整串处理。
34///
35/// **读([`select`])与写([`write`])共用此唯一实现。** 两侧语义若各自漂移会产生
36/// 读写不对称(读得到的列写不进 / 写进去的列读不出),故收口到此,两侧只做薄包装。
37pub(crate) fn scalar_column(schema: &Schema, field: &str) -> Option<String> {
38    if field.contains('.') {
39        let (head, _) = field.split_once('.')?;
40        // 点号字段:若 head 是 object/array(其子字段展平到附属表)则跳过;否则按整串处理
41        let head_type = schema.fields.get(head).map(|f| f.field_type.as_str());
42        if matches!(head_type, Some("object") | Some("array")) {
43            return None;
44        }
45        return Some(field.to_string());
46    }
47    match schema.fields.get(field).map(|f| f.field_type.as_str()) {
48        Some("object") | Some("array") => None,
49        _ => Some(field.to_string()),
50    }
51}
52
53/// 字段 → 列引用(过滤 / 投影 / 回读用;object/array 走 JSON 单列)。
54///
55/// 与 [`scalar_column`] 的差异:`scalar_column` 只认标量列(object/array 返回 `None`,
56/// 供关系键解析、分组键等**必须标量**的场景);本函数在其基础上把 object/array 字段
57/// 展开为 JSON 列引用(见执行文档 §4.5 / 不足清单 #3):
58/// - 标量字段 → [`ColumnRef::Scalar`](调用方按别名限定 + 引号化);
59/// - object/array 整值 → [`ColumnRef::Json`](单列存 JSON 文本,读取时解析);
60/// - object/array 的点号路径(U3 过滤 / U4 排序)→ [`ColumnRef::JsonPath`](提取表达式)。
61///
62/// 未声明字段返回 `None`(调用方据此显式报错,绝不静默)。
63pub(crate) fn field_column_ref(schema: &Schema, field: &str) -> Option<ColumnRef> {
64    if let Some((head, rest)) = field.split_once('.') {
65        let head_type = schema.fields.get(head).map(|f| f.field_type.as_str());
66        match head_type {
67            // U3/U4:object/array 点号路径 → JSON 提取(根列 head + 子路径 rest)
68            Some("object") | Some("array") => {
69                let path: Vec<String> = rest.split('.').map(String::from).collect();
70                Some(ColumnRef::JsonPath(head.to_string(), path))
71            }
72            // 点号字段的 head 非 object/array(如标量同名字段)→ 按整串列名处理(既有语义)
73            _ => Some(ColumnRef::Scalar(field.to_string())),
74        }
75    } else {
76        match schema.fields.get(field).map(|f| f.field_type.as_str()) {
77            Some("object") | Some("array") => Some(ColumnRef::Json(field.to_string())),
78            // 已声明标量 / 未声明字段(与 `scalar_column` 宽松语义一致:按裸列名处理,
79            // 如物理主键 `_id` 不在 schema.fields 但恒为列)
80            _ => Some(ColumnRef::Scalar(field.to_string())),
81        }
82    }
83}
84
85/// 字段 → 列引用(见 [`field_column_ref`]):区分标量列 / JSON 整列 / JSON 点号路径。
86#[derive(Debug, Clone, PartialEq)]
87pub enum ColumnRef {
88    /// 标量列:字段名(调用方按表别名限定 + 引号化)
89    Scalar(String),
90    /// JSON 整列:object/array 字段落单列存 JSON 文本(读取时解析回嵌套对象)
91    Json(String),
92    /// JSON 点号路径(U3 过滤 / U4 排序):根列名 + 子路径(调用方构造提取表达式)
93    JsonPath(String, Vec<String>),
94}
95
96/// §9.7「布尔归一」:schema 字段是否为布尔类型(`boolean` 规范拼写 / `bool` 简写)。
97///
98/// 判定依据是 schema 的**声明类型**(不是驱动元数据,也不是物理列类型)——SQL 侧把布尔
99/// 存成 `TINYINT(1)` / `INTEGER` / `BOOLEAN` 都可能,唯一稳定的依据是 schema。命中即由
100/// [`row::restore_rows`] 在行还原时把 `0/1` 归一为 JSON `bool`。
101///
102/// 两种拼写都接受:core 的类型文档与 introspection 产出 `boolean`,而示例/用户 schema
103/// 常写 `bool`(如场景矩阵的 `paid` / `free`),二者语义相同。
104pub(crate) fn field_is_bool(schema: &Schema, field: &str) -> bool {
105    schema
106        .fields
107        .get(field)
108        .map(|f| {
109            f.field_type.eq_ignore_ascii_case("bool")
110                || f.field_type.eq_ignore_ascii_case("boolean")
111        })
112        .unwrap_or(false)
113}
114
115/// 支持的数据库后端
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum Backend {
118    Mysql,
119    Postgres,
120    Sqlite,
121}
122
123impl Backend {
124    /// 从字符串解析后端(未知 → Err)
125    pub fn parse(s: &str) -> Result<Backend, String> {
126        match s.to_lowercase().as_str() {
127            "mysql" => Ok(Backend::Mysql),
128            "postgres" | "pg" | "postgresql" => Ok(Backend::Postgres),
129            "sqlite" => Ok(Backend::Sqlite),
130            _ => Err(format!("不支持的后端: {}", s)),
131        }
132    }
133
134    /// 双重引号标识符
135    pub fn quote_ident(&self, ident: &str) -> String {
136        match self {
137            Backend::Mysql => format!("`{}`", ident.replace('`', "``")),
138            _ => format!("\"{}\"", ident.replace('"', "\"\"")),
139        }
140    }
141
142    /// 双精度浮点类型名(§9.7「数值归 double」)
143    ///
144    /// `SUM/AVG` 的结果在不同后端原生精度不同:MySQL `AVG(<int 列>)` 只保留 4 位小数
145    /// (`DECIMAL(scale+4)`),PG `AVG(<int 列>)` 返回精确 `numeric`,均与 Mongo `$avg`
146    /// 的 IEEE-754 double 存在差异。故 SQL 侧 `AVG` 前先把入参 `CAST` 到双精度。
147    pub fn double_type(&self) -> &'static str {
148        match self {
149            Backend::Mysql => "DOUBLE",
150            Backend::Postgres => "DOUBLE PRECISION",
151            Backend::Sqlite => "REAL",
152        }
153    }
154
155    /// 表名 → SQL:namespace 可选限定(`Some(ns)` → `"ns"."table"`;`None` → `"table"`)
156    ///
157    /// namespace 与连接的 search_path / 连接库 / ATTACH 库对应(见
158    /// `multi-datasource-routing-plan.md` §二);空串按 `None` 处理。
159    pub fn qualified_table(&self, namespace: Option<&str>, table: &str) -> String {
160        match namespace.filter(|s| !s.is_empty()) {
161            Some(ns) => format!("{}.{}", self.quote_ident(ns), self.quote_ident(table)),
162            None => self.quote_ident(table),
163        }
164    }
165
166    /// 占位符:SQLite/MySQL 用 `?`,PostgreSQL 用 `$n`(调用方保证按顺序传入 index)
167    pub fn placeholder(&self, _index: usize) -> String {
168        match self {
169            Backend::Postgres => format!("${}", _index + 1),
170            _ => "?".to_string(),
171        }
172    }
173
174    /// 后端标识(同 `Debug`,供绑定层序列化)
175    pub fn as_str(&self) -> &'static str {
176        match self {
177            Backend::Mysql => "mysql",
178            Backend::Postgres => "postgres",
179            Backend::Sqlite => "sqlite",
180        }
181    }
182
183    /// MySQL 无 RETURNING —— 需由 Host 编排 UPDATE + find 两段;PG/SQLite 原生支持
184    pub fn supports_returning(&self) -> bool {
185        !matches!(self, Backend::Mysql)
186    }
187
188    /// JSON 列的方言类型名(`object`/`array` 字段落单列存储的契约,见 §4.5 / 不足清单 #3)
189    ///
190    /// ⚠️ 跨方言对齐:`object`/`array` 字段在 SQL 后端**以单列存 JSON 文本** ——
191    /// MySQL `JSON` / PG `jsonb` / SQLite `TEXT`(JSON1 扩展)。core 不写 DDL(铁律 6),
192    /// 此常量供示例 DDL、introspection 与文档约定引用。
193    pub fn json_type_name(&self) -> &'static str {
194        match self {
195            Backend::Mysql => "JSON",
196            Backend::Postgres => "jsonb",
197            Backend::Sqlite => "TEXT",
198        }
199    }
200
201    /// JSON 点号路径 → **标量**提取表达式(U3 对象点号路径过滤 / U4 排序用)
202    ///
203    /// - MySQL:`JSON_UNQUOTE(JSON_EXTRACT(col, '$.a.b'))`
204    /// - PG:`(col #>> '{a,b}')`
205    /// - SQLite:`json_extract(col, '$.a.b')`
206    ///
207    /// 三者统一为「提取后标量(数值/字符串)」,供比较与 `ORDER BY` 使用;
208    /// `col` 须为已按后端引号化的**列标识符**,`path` 为点号各段(不含列名)。
209    pub fn json_extract_scalar(&self, col: &str, path: &[&str]) -> String {
210        match self {
211            Backend::Mysql => format!(
212                "JSON_UNQUOTE(JSON_EXTRACT({}, '{}'))",
213                col,
214                json_path_dollar(path)
215            ),
216            Backend::Postgres => format!("({} #>> '{{{}}}')", col, path.join(",")),
217            Backend::Sqlite => format!("json_extract({}, '{}')", col, json_path_dollar(path)),
218        }
219    }
220}
221
222/// 点号路径 → MySQL/SQLite `JSON_EXTRACT` 的 `$` 路径字面量(`["a","b"]` → `$.a.b`)
223fn json_path_dollar(path: &[&str]) -> String {
224    let mut s = String::from("$");
225    for seg in path {
226        s.push('.');
227        s.push_str(seg);
228    }
229    s
230}