Skip to main content

rust_store_core/schema/
load.rs

1//! 目录语义装载(设计 §5.2/§5.3):纯逻辑、零 IO。
2//!
3//! 输入 = 「store.config.json 的 JSON + 目录扫描结果(相对 defs-root 的路径 + 已读入的 defn)」;
4//! 输出 = 定位后的装载项 / 显式错误。IO(读文件 / walk)在宿主与脚手架。
5//!
6//! 批次判据(分组 / 主唯一 / 四元组冲突)**与 [`super::registry::register_batch`] 同源**:
7//! 共用 [`super::registry::classify_batch`] / [`super::registry::detect_location_conflict`],
8//! 禁各写一份(判据单点,防 A1/A3 漂移)。
9
10use serde_json::Value;
11
12use crate::datasource::DataSource;
13use crate::dialect::Backend;
14
15use super::definition::{Location, Role};
16use super::registry::{classify_batch, detect_location_conflict, BatchError};
17
18/// 一个连接声明(`store.config.json` 的 `sources` 一项)
19#[derive(Debug, Clone)]
20pub struct SourceDecl {
21    pub name: String,
22    /// 小写归一后:mongodb/mysql/pg/sqlite(kind 白名单见执行文档 §4.2)
23    pub kind: String,
24    /// 该连接维护的库
25    pub databases: Vec<String>,
26}
27
28/// 一条链路声明(`store.config.json` 的 `links[<schema>]` 一项)。
29///
30/// 外置配置,**禁写入 schema 定义文件**(设计 §4.1);`source`/`database`/`schema`
31/// 三元组标识一个既有或待补建的落点,`role`/`priority` 为读解析排序属性。
32#[derive(Debug, Clone)]
33pub struct LinkDecl {
34    /// 连接名(必须已在 `sources` 声明)
35    pub source: String,
36    /// 链路角色;缺省 `None` = 未声明(排序时视作最低层,不参与 primary 唯一性校验)
37    pub role: Option<Role>,
38    /// 同 role 内升序排序;缺省 `None` = 视作最大;允许负数
39    pub priority: Option<i64>,
40    /// 库:缺省 = `sources[source].databases` 的唯一库(多库须显式给)
41    pub database: Option<String>,
42    /// 连接内 schema(仅 PG 有意义)
43    pub schema: Option<String>,
44}
45
46/// 装载配置(`store.config.json` 的 `sources` + `defs` + `links`)
47#[derive(Debug, Clone, Default)]
48pub struct LoadConfig {
49    /// 保持声明顺序(`serde_json::Map` 为 BTreeMap ⇒ 字典序;确定性输出)
50    pub sources: Vec<SourceDecl>,
51    pub defs: Vec<String>,
52    /// schema 名 → 有序链路声明(保声明序;缺省为空 ⇒ 零回归)
53    pub links: Vec<(String, Vec<LinkDecl>)>,
54}
55
56impl LoadConfig {
57    /// 解析 `{ "sources": { <name>: { kind, databases:[...] } }, "defs": [<root>...], "links": {...} }`。
58    ///
59    /// 非法即 `Err`(禁静默兜底;含 kind 白名单、databases 非空、defs 非空、links 六条校验)。
60    /// `links` 缺省 ⇒ 空(零回归);非空时每项须 `source` 已声明、`role` 合法、
61    /// `priority` 为整数、恰一条 `primary`、`database` 可判(多库须显式给)。
62    pub fn from_json(v: &Value) -> Result<Self, String> {
63        let obj = v
64            .as_object()
65            .ok_or_else(|| "ERR:LOAD store.config.json 必须是对象".to_string())?;
66
67        let sources_obj = obj
68            .get("sources")
69            .and_then(|s| s.as_object())
70            .ok_or_else(|| {
71                "ERR:LOAD 缺 sources(须为 { name: { kind, databases } } 对象)".to_string()
72            })?;
73        if sources_obj.is_empty() {
74            return Err("ERR:LOAD sources 不得为空".to_string());
75        }
76
77        let mut sources = Vec::new();
78        for (name, decl) in sources_obj {
79            let d = decl
80                .as_object()
81                .ok_or_else(|| format!("ERR:LOAD 连接 {name} 声明必须是对象"))?;
82            let raw_kind = d
83                .get("kind")
84                .and_then(|k| k.as_str())
85                .filter(|s| !s.is_empty())
86                .ok_or_else(|| format!("ERR:LOAD 连接 {name} 缺 kind"))?;
87            // kind 白名单:复用 DataSource::from_kind(mongo/mongodb/mysql/pg/postgres/postgresql/sqlite/local/memory)
88            DataSource::from_kind(raw_kind).map_err(|_| {
89                format!(
90                    "ERR:LOAD 连接 {name} 的 kind 非法: {raw_kind}(仅 mongodb/mysql/pg/sqlite/local/memory)"
91                )
92            })?;
93            let databases: Vec<String> = d
94                .get("databases")
95                .and_then(|ds| ds.as_array())
96                .map(|arr| {
97                    arr.iter()
98                        .filter_map(|x| x.as_str())
99                        .filter(|s| !s.is_empty())
100                        .map(String::from)
101                        .collect()
102                })
103                .unwrap_or_default();
104            if databases.is_empty() {
105                return Err(format!(
106                    "ERR:LOAD 连接 {name} 的 databases 须为非空字符串数组"
107                ));
108            }
109            sources.push(SourceDecl {
110                name: name.clone(),
111                kind: raw_kind.to_lowercase(),
112                databases,
113            });
114        }
115
116        let defs: Vec<String> = obj
117            .get("defs")
118            .and_then(|ds| ds.as_array())
119            .map(|arr| {
120                arr.iter()
121                    .filter_map(|x| x.as_str())
122                    .filter(|s| !s.is_empty())
123                    .map(String::from)
124                    .collect()
125            })
126            .unwrap_or_default();
127        if defs.is_empty() {
128            return Err("ERR:LOAD defs 须为非空字符串数组(定义根)".to_string());
129        }
130
131        // links:schema 名 → 有序链路声明;缺省 ⇒ 空(零回归,全走既有目录语义)
132        let links = match obj.get("links") {
133            None | Some(Value::Null) => Vec::new(),
134            Some(v) => {
135                let links_obj = v.as_object().ok_or_else(|| {
136                    "ERR:LOAD links 必须是对象(schema 名 → 链路声明数组)".to_string()
137                })?;
138                let mut out: Vec<(String, Vec<LinkDecl>)> = Vec::with_capacity(links_obj.len());
139                for (name, decls_v) in links_obj {
140                    let arr = decls_v.as_array().ok_or_else(|| {
141                        format!("ERR:LOAD links[{name}] 必须是数组(链路声明列表)")
142                    })?;
143                    let mut decls: Vec<LinkDecl> = Vec::with_capacity(arr.len());
144                    let mut primary_count = 0usize;
145                    for (i, item) in arr.iter().enumerate() {
146                        let d = item
147                            .as_object()
148                            .ok_or_else(|| format!("ERR:LOAD links[{name}][{i}] 必须是对象"))?;
149                        // source 必填,且必须已在 sources 声明(未声明即 Err,禁猜)
150                        let source = d
151                            .get("source")
152                            .and_then(|s| s.as_str())
153                            .filter(|s| !s.is_empty())
154                            .ok_or_else(|| format!("ERR:LOAD links[{name}][{i}] 缺 source"))?;
155                        let src_decl = sources.iter().find(|s| s.name == source).ok_or_else(|| {
156                            format!("ERR:LOAD links[{name}] 的 source 未在 sources 中声明: {source}")
157                        })?;
158                        // role 缺省 ⇒ None;给值须经 Role::parse(非法 ⇒ Err)
159                        let role = match d.get("role") {
160                            None | Some(Value::Null) => None,
161                            Some(rv) => {
162                                let rs = rv.as_str().ok_or_else(|| {
163                                    format!("ERR:LOAD links[{name}][{i}] 的 role 须为字符串")
164                                })?;
165                                Some(Role::parse(rs)?)
166                            }
167                        };
168                        if role == Some(Role::Primary) {
169                            primary_count += 1;
170                        }
171                        // priority 必须为整数
172                        let priority = match d.get("priority") {
173                            None | Some(Value::Null) => None,
174                            Some(pv) => Some(pv.as_i64().ok_or_else(|| {
175                                format!("ERR:LOAD links[{name}][{i}] 的 priority 须为整数")
176                            })?),
177                        };
178                        // database 缺省:source 恰 1 库 ⇒ 取其值;多库 ⇒ Err(禁猜)
179                        let database = match d.get("database") {
180                            None | Some(Value::Null) => {
181                                if src_decl.databases.len() == 1 {
182                                    Some(src_decl.databases[0].clone())
183                                } else {
184                                    return Err(format!(
185                                        "ERR:LOAD links[{name}] 的 source {source} 维护多库,须显式给 database"
186                                    ));
187                                }
188                            }
189                            Some(dv) => Some(
190                                dv.as_str()
191                                    .filter(|s| !s.is_empty())
192                                    .ok_or_else(|| {
193                                        format!(
194                                            "ERR:LOAD links[{name}][{i}] 的 database 须为非空字符串"
195                                        )
196                                    })?
197                                    .to_string(),
198                            ),
199                        };
200                        let schema = match d.get("schema") {
201                            None | Some(Value::Null) => None,
202                            Some(sv) => Some(
203                                sv.as_str()
204                                    .filter(|s| !s.is_empty())
205                                    .ok_or_else(|| {
206                                        format!(
207                                            "ERR:LOAD links[{name}][{i}] 的 schema 须为非空字符串"
208                                        )
209                                    })?
210                                    .to_string(),
211                            ),
212                        };
213                        decls.push(LinkDecl {
214                            source: source.to_string(),
215                            role,
216                            priority,
217                            database,
218                            schema,
219                        });
220                    }
221                    // 同一 links[<name>] 内 role=primary 恰好一条
222                    if primary_count != 1 {
223                        return Err(format!(
224                            "ERR:LOAD links[{name}] 必须恰好一条 role=primary(实际 {primary_count} 条)"
225                        ));
226                    }
227                    out.push((name.clone(), decls));
228                }
229                out
230            }
231        };
232
233        Ok(LoadConfig {
234            sources,
235            defs,
236            links,
237        })
238    }
239
240    /// db 名 → (连接声明, 语义深度):同一 db 名挂多个连接 ⇒ `Err`(设计 §5.2)。
241    ///
242    /// 深度由 [`depth_layered`] 按 kind 决定(PG=2,其余=1)。
243    pub fn source_of_db(&self, db: &str) -> Result<(&SourceDecl, u8), String> {
244        let mut hit: Option<&SourceDecl> = None;
245        for s in &self.sources {
246            if s.databases.iter().any(|d| d == db) {
247                if let Some(prev) = hit {
248                    return Err(format!(
249                        "ERR:LOAD 库归属冲突: {db} 同时声明在连接 {} 与 {}(同一 db 名只能挂一个连接)",
250                        prev.name, s.name
251                    ));
252                }
253                hit = Some(s);
254            }
255        }
256        let decl = hit.ok_or_else(|| {
257            format!(
258                "ERR:LOAD 库目录未声明: {db}(须在 store.config.json 的某连接 databases 中列出)"
259            )
260        })?;
261        let depth = depth_layered(&decl.kind)?;
262        Ok((decl, depth))
263    }
264}
265
266/// 语义层级深度:PG=2(L2=schema),Mongo/MySQL/SQLite=1(L2+ 打平)。
267/// kind 非法 ⇒ `Err`(禁回落默认)。
268pub fn depth_layered(kind: &str) -> Result<u8, String> {
269    let ds = DataSource::from_kind(kind).map_err(|_| {
270        format!("ERR:LOAD 连接 kind 非法: {kind}(仅 mongodb/mysql/pg/sqlite/local/memory)")
271    })?;
272    Ok(match ds.backend() {
273        Some(Backend::Postgres) => 2,
274        _ => 1,
275    })
276}
277
278/// 由「db 目录以下的相对路径」算落点(设计 §5.2 固定语义,D3):
279/// - `rel_under_db` 至少 1 段(文件名);除末段(文件名)外最多 `depth-1` 段参与语义,其余**打平**;
280/// - `depth == 2`(PG):有子目录 ⇒ 子目录首段 = `schema`;无子目录 ⇒ `schema = None`
281///   (回落连接默认 search_path,见 `definition.rs` Location 语义);
282/// - `depth == 1`:`schema = None`;L2+ 全部打平(不影响 `database` 归属);
283/// - 输出 `Location { source, database: Some(db), schema }`。
284pub fn locate(rel_under_db: &str, source: &str, db: &str, depth: u8) -> Result<Location, String> {
285    let segs: Vec<&str> = rel_under_db
286        .split(['/', '\\'])
287        .filter(|s| !s.is_empty())
288        .collect();
289    if segs.is_empty() {
290        return Err(format!("ERR:LOAD 定义路径非法(缺文件名): {rel_under_db}"));
291    }
292    // 末段 = 文件名;其余 = 目录段
293    let dirs = &segs[..segs.len() - 1];
294    let schema = if depth >= 2 && !dirs.is_empty() {
295        Some(dirs[0].to_string())
296    } else {
297        None
298    };
299    Ok(Location {
300        source: source.to_string(),
301        database: Some(db.to_string()),
302        schema,
303        role: None,
304        priority: None,
305    })
306}
307
308/// 纯批次规划(设计 §5.2 伪码 + §5.3 主从):
309/// `files: [(rel_from_defs_root, defn)]`(`rel` 首段 = db 目录名)。
310///
311/// 步骤:① 按 `rel` 首段定位 db → `source_of_db` 反查(未声明 / 重挂 ⇒ `Err`);
312///      ② `depth_layered(kind)` → `locate`;
313///      ③ 判据单点 `classify_batch`(分组 / 主唯一 / 批内四元组冲突);
314///      ④ 每组主**恰好一份**,0 份 ⇒ `Err`(A1/A3);
315///      ⑤ 输出有序装载项:**主在前、其后从**(组间按 name 字典序)。
316pub fn plan_load(
317    cfg: &LoadConfig,
318    files: &[(String, Value)],
319) -> Result<Vec<(Value, Location)>, String> {
320    // ①/② 归属 + locate
321    let mut items: Vec<(Value, Location)> = Vec::with_capacity(files.len());
322    for (rel, defn) in files {
323        let segs: Vec<&str> = rel.split(['/', '\\']).filter(|s| !s.is_empty()).collect();
324        if segs.is_empty() {
325            return Err(format!("ERR:LOAD 定义路径非法: {rel}"));
326        }
327        let db = segs[0];
328        let (decl, depth) = cfg.source_of_db(db)?;
329        let rel_under_db = segs[1..].join("/");
330        let loc = locate(&rel_under_db, &decl.name, db, depth)?;
331        items.push((defn.clone(), loc));
332    }
333
334    // ③ 判据单点(分组 / 主唯一 / 批内四元组冲突)
335    let class = classify_batch(&items).map_err(|e| batch_err_to_load(e, files))?;
336
337    // ④/⑤ 组间按 name 字典序;主在前、其后从
338    let mut names: Vec<&String> = class.order.iter().collect();
339    names.sort();
340
341    // 批内四元组冲突(与 register_batch 同源的判据函数)
342    let mut quads: Vec<(String, String, Location)> = Vec::new();
343    for name in &names {
344        if let Some((_, loc, coll)) = class.primaries.get(*name) {
345            quads.push(((*name).clone(), coll.clone(), loc.clone()));
346        }
347        if let Some(rs) = class.replicas.get(*name) {
348            for (defn, loc) in rs {
349                let coll = defn
350                    .get("name")
351                    .and_then(|v| v.as_str())
352                    .unwrap_or(name)
353                    .to_string();
354                quads.push(((*name).clone(), coll, loc.clone()));
355            }
356        }
357    }
358    if let Some(c) = detect_location_conflict(&quads) {
359        return Err(format!(
360            "ERR:LOAD 落点冲突: {} 同时映射 ({}, {:?}, {:?})(同一落点只能有一个 collection;冲突名 `{}` 与 `{}`)",
361            c.collection, c.source, c.database, c.schema, c.prev, c.name
362        ));
363    }
364
365    let mut out: Vec<(Value, Location)> = Vec::with_capacity(items.len());
366    for name in names {
367        let Some((defn, loc, _coll)) = class.primaries.get(name) else {
368            return Err(format!(
369                "ERR:LOAD 主定义缺失: {name}(同名项均为 replica:true,须恰好一份主)"
370            ));
371        };
372        out.push((defn.clone(), loc.clone()));
373        if let Some(rs) = class.replicas.get(name) {
374            for (rdefn, rloc) in rs {
375                out.push((rdefn.clone(), rloc.clone()));
376            }
377        }
378    }
379    Ok(out)
380}
381
382/// `BatchError` → `ERR:LOAD` 文案(`indices` → `files[i].rel` 供定位重复主定义)。
383fn batch_err_to_load(e: BatchError, files: &[(String, Value)]) -> String {
384    match e {
385        BatchError::Parse(m) => format!("ERR:LOAD {m}"),
386        BatchError::PrimaryDuplicate { name, indices } => {
387            let rels: Vec<String> = indices
388                .iter()
389                .map(|i| {
390                    files
391                        .get(*i)
392                        .map(|(r, _)| r.clone())
393                        .unwrap_or_else(|| format!("#{i}"))
394                })
395                .collect();
396            format!(
397                "ERR:LOAD 主定义重复: {name}({})——同名只允许一份主,其余须 replica:true",
398                rels.join(" 与 ")
399            )
400        }
401        BatchError::LocationConflict(c) => format!(
402            "ERR:LOAD 落点冲突: {} 同时映射 ({}, {:?}, {:?})(同一落点只能有一个 collection;冲突名 `{}` 与 `{}`)",
403            c.collection, c.source, c.database, c.schema, c.prev, c.name
404        ),
405    }
406}