Skip to main content

rust_store_core/dialect/
introspect.rs

1//! introspection 行 JSON → schemaJSON(纯映射,可喂 `Registry::register`)
2//!
3//! 联网查库在 Host 侧做,只把结构化的 `rows` JSON(tables / columns / fks / indexes)传给
4//! 本模块做纯映射。SQLite 用 `sqlite_master` + `PRAGMA`;MySQL/PG 用 `information_schema`,
5//! Host 归一化成下列行形状:
6
7use serde_json::{json, Map, Value};
8
9use super::Backend;
10
11/// 从规范化 introspection 行 JSON 生成 schemaJSON 数组。
12///
13/// 期望输入(Host 归一化后的结构):
14/// ```json
15/// {
16///   "tables": [{ "name": "posts" }],
17///   "columns": [{ "table": "posts", "name": "title", "type": "TEXT",
18///                 "notnull": 0, "pk": 0 }],
19///   "fks":      [{ "table": "order_items", "column": "order_id",
20///                  "refTable": "orders", "refColumn": "_id" }],
21///   "indexes":  [{ "table": "posts", "name": "idx_status", "columns": ["status"], "unique": 0 }]
22/// }
23/// ```
24///
25/// 行形状扩展:`tables[].database`(可选)+ `tables[].schema`(可选,仅 PG)— 当 Host 明确知道
26/// 表所属库/schema(如 SQLite attached db、MySQL 显式 database、PG 显式 schema)时携带。
27/// **本模块不再把落点写进 def**(定义零落点);落点由调用方(宿主)作为
28/// `Location { source, database, schema }` 传给 `Registry::register_batch`。
29///
30/// 输出:schemaJSON 数组(每个 `Schema` 一个 def,可直接传给 `Registry::register`)。
31pub fn schema_def_from_rows(rows: &Value) -> Result<Value, String> {
32    let tables = rows
33        .get("tables")
34        .and_then(|t| t.as_array())
35        .cloned()
36        .unwrap_or_default();
37    let columns = rows
38        .get("columns")
39        .and_then(|c| c.as_array())
40        .cloned()
41        .unwrap_or_default();
42    let fks = rows
43        .get("fks")
44        .and_then(|c| c.as_array())
45        .cloned()
46        .unwrap_or_default();
47    // 索引仅作元数据(铁律 6:绝不写 DDL 回库),此解析结果暂不参与 schema 生成
48    let _indexes = rows
49        .get("indexes")
50        .and_then(|c| c.as_array())
51        .cloned()
52        .unwrap_or_default();
53
54    let mut defs: Vec<Value> = Vec::new();
55    for t in &tables {
56        let name = t
57            .get("name")
58            .and_then(|v| v.as_str())
59            .unwrap_or("")
60            .to_string();
61        if name.is_empty() {
62            continue;
63        }
64        let table_cols: Vec<&Value> = columns
65            .iter()
66            .filter(|c| c.get("table").and_then(|v| v.as_str()) == Some(name.as_str()))
67            .collect();
68        let table_fks: Vec<&Value> = fks
69            .iter()
70            .filter(|f| f.get("table").and_then(|v| v.as_str()) == Some(name.as_str()))
71            .collect();
72
73        let mut fields_map = Map::new();
74        let mut relations_map = Map::new();
75
76        for c in &table_cols {
77            let col = c
78                .get("name")
79                .and_then(|v| v.as_str())
80                .unwrap_or("")
81                .to_string();
82            let db_type = c
83                .get("type")
84                .and_then(|v| v.as_str())
85                .unwrap_or("")
86                .to_uppercase();
87            let required = c.get("notnull").and_then(|v| v.as_i64()).unwrap_or(0) != 0;
88            let is_pk = c.get("pk").and_then(|v| v.as_i64()).unwrap_or(0) != 0;
89            if is_pk {
90                fields_map.insert(
91                    "_id".to_string(),
92                    json!({ "type": "string", "required": true }),
93                );
94                // 主键列可能非 _id 命名:统一映射为 _id 字段。此前此处会补写 "__pk_col": "<原列名>"
95                // 裸字符串键 —— 既被 normalize_fields 误判为「类型简写」产出幻影字段,全库又无任何
96                // 消费方,已删除(评测 m-8-1);如未来需要记录物理主键列,应以对象形态的字段定义承载
97                continue;
98            }
99            fields_map.insert(
100                col.clone(),
101                json!({ "type": db_field_type(&col, &db_type), "required": required }),
102            );
103        }
104
105        // 外键 → 关系;被引用表的主键列名(默认 _id)为 foreignField
106        for f in &table_fks {
107            let col = f
108                .get("column")
109                .and_then(|v| v.as_str())
110                .unwrap_or("")
111                .to_string();
112            let ref_table = f
113                .get("refTable")
114                .and_then(|v| v.as_str())
115                .unwrap_or("")
116                .to_string();
117            let ref_column = f
118                .get("refColumn")
119                .and_then(|v| v.as_str())
120                .unwrap_or("_id")
121                .to_string();
122            if col.is_empty() || ref_table.is_empty() {
123                continue;
124            }
125            // 关系名:被引用表(关系目标)
126            // 本表持有外键 → 对 refTable 是 many(本表多条指向一条);也反推给 refTable 一条
127            relations_map.insert(
128                ref_table.clone(),
129                json!({
130                    "model": ref_table,
131                    "type": "one",
132                    "localField": col,
133                    "foreignField": ref_column,
134                }),
135            );
136            // 反向关系:在 refTable 侧补 a many 到本表 —— 用 `__rev_<refTable>` 占位,
137            // 由第二轮扫描统一归一到 refTable 的 def 里(localField 用引用列,foreignField 用外键列)
138            relations_map.insert(
139                format!("__rev_{}", ref_table),
140                json!({
141                    "model": name,
142                    "type": "many",
143                    "localField": ref_column,
144                    "foreignField": col,
145                }),
146            );
147        }
148
149        let def = json!({
150            "name": name,
151            "collection": name,
152            "idPrefix": "",
153            "timestamps": false,
154            "fields": Value::Object(fields_map),
155            "relations": Value::Object(relations_map),
156        });
157        // 落点(tables[].database / tables[].schema)不再写入 def —— 定义零落点;
158        // 由调用方(宿主)读取行形状后作为 `Location` 传给 `Registry::register_batch`。
159        defs.push(def);
160    }
161
162    // 第二轮:把反向 `__rev_<refTable>` 关系归一到被引用表的 `relations` 里
163    let mut by_name: Map<String, Value> = Map::new();
164    for d in &defs {
165        let n = d
166            .get("name")
167            .and_then(|v| v.as_str())
168            .unwrap_or("")
169            .to_string();
170        by_name.insert(n, d.clone());
171    }
172    // 收集反向关系
173    let mut rev_rels: Vec<(String, String, Value)> = Vec::new(); // (target_table, rel_name, rel_def)
174    for d in &defs {
175        let n = d
176            .get("name")
177            .and_then(|v| v.as_str())
178            .unwrap_or("")
179            .to_string();
180        let rels = d.get("relations").and_then(|r| r.as_object());
181        if let Some(rels) = rels {
182            for (k, v) in rels {
183                if let Some(t) = k.strip_prefix("__rev_").map(String::from) {
184                    rev_rels.push((t, n.clone(), v.clone()));
185                }
186            }
187        }
188    }
189    for (target, rel_name, rel_def) in rev_rels {
190        if let Some(d) = by_name.get_mut(&target) {
191            let rels = d.get_mut("relations").and_then(|r| r.as_object_mut());
192            if let Some(rels) = rels {
193                rels.insert(rel_name, rel_def);
194            }
195        }
196    }
197    // 由合并后的 by_name 输出,去掉 `__rev_` 占位
198    let mut names: Vec<String> = by_name.keys().cloned().collect();
199    names.sort();
200    let out: Vec<Value> = names
201        .into_iter()
202        .map(|n| by_name.get(&n).cloned().unwrap_or(Value::Null))
203        .map(|mut d| {
204            if let Some(rels) = d.get_mut("relations").and_then(|r| r.as_object_mut()) {
205                rels.retain(|k, _| !k.starts_with("__rev_"));
206            }
207            d
208        })
209        .collect();
210    Ok(Value::Array(out))
211}
212
213/// SQL 列类型 → rust-store 字段类型
214fn db_field_type(col: &str, db_type: &str) -> &'static str {
215    // 附属表识别(object/array 展平):`<field>_object` / `<field>_list` 后缀由 Host 命名
216    if col.ends_with("_object") || db_type.contains("JSON") || db_type.contains("OBJECT") {
217        return "object";
218    }
219    if col.ends_with("_list") || db_type.contains("ARRAY") {
220        return "array";
221    }
222    match db_type {
223        t if t.contains("INT")
224            || t.contains("NUMERIC")
225            || t.contains("DECIMAL")
226            || t.contains("REAL")
227            || t.contains("FLOAT")
228            || t.contains("DOUBLE") =>
229        {
230            "number"
231        }
232        t if t.contains("BOOL") => "boolean",
233        t if t.contains("DATE") || t.contains("TIME") || t.contains("TIMESTAMP") => "date",
234        _ => "string",
235    }
236}
237
238/// 方便绑定层:直接返回已注册集合的 schemaJSON(等价 `schema_def_from_rows`,但归一到 model 名)
239pub fn introspect_to_schema_json(rows: &Value, _backend: &Backend) -> Result<Value, String> {
240    schema_def_from_rows(rows)
241}