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;
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` 的 `sources` + `defs`)
29#[derive(Debug, Clone, Default)]
30pub struct LoadConfig {
31    /// 保持声明顺序(`serde_json::Map` 为 BTreeMap ⇒ 字典序;确定性输出)
32    pub sources: Vec<SourceDecl>,
33    pub defs: Vec<String>,
34}
35
36impl LoadConfig {
37    /// 解析 `{ "sources": { <name>: { kind, databases:[...] } }, "defs": [<root>...] }`。
38    ///
39    /// 非法即 `Err`(禁静默兜底;含 kind 白名单、databases 非空、defs 非空)。
40    pub fn from_json(v: &Value) -> Result<Self, String> {
41        let obj = v
42            .as_object()
43            .ok_or_else(|| "ERR:LOAD store.config.json 必须是对象".to_string())?;
44
45        let sources_obj = obj
46            .get("sources")
47            .and_then(|s| s.as_object())
48            .ok_or_else(|| {
49                "ERR:LOAD 缺 sources(须为 { name: { kind, databases } } 对象)".to_string()
50            })?;
51        if sources_obj.is_empty() {
52            return Err("ERR:LOAD sources 不得为空".to_string());
53        }
54
55        let mut sources = Vec::new();
56        for (name, decl) in sources_obj {
57            let d = decl
58                .as_object()
59                .ok_or_else(|| format!("ERR:LOAD 连接 {name} 声明必须是对象"))?;
60            let raw_kind = d
61                .get("kind")
62                .and_then(|k| k.as_str())
63                .filter(|s| !s.is_empty())
64                .ok_or_else(|| format!("ERR:LOAD 连接 {name} 缺 kind"))?;
65            // kind 白名单:复用 DataSource::from_kind(mongo/mongodb/mysql/pg/postgres/postgresql/sqlite/local)
66            DataSource::from_kind(raw_kind).map_err(|_| {
67                format!(
68                    "ERR:LOAD 连接 {name} 的 kind 非法: {raw_kind}(仅 mongodb/mysql/pg/sqlite/local)"
69                )
70            })?;
71            let databases: Vec<String> = d
72                .get("databases")
73                .and_then(|ds| ds.as_array())
74                .map(|arr| {
75                    arr.iter()
76                        .filter_map(|x| x.as_str())
77                        .filter(|s| !s.is_empty())
78                        .map(String::from)
79                        .collect()
80                })
81                .unwrap_or_default();
82            if databases.is_empty() {
83                return Err(format!(
84                    "ERR:LOAD 连接 {name} 的 databases 须为非空字符串数组"
85                ));
86            }
87            sources.push(SourceDecl {
88                name: name.clone(),
89                kind: raw_kind.to_lowercase(),
90                databases,
91            });
92        }
93
94        let defs: Vec<String> = obj
95            .get("defs")
96            .and_then(|ds| ds.as_array())
97            .map(|arr| {
98                arr.iter()
99                    .filter_map(|x| x.as_str())
100                    .filter(|s| !s.is_empty())
101                    .map(String::from)
102                    .collect()
103            })
104            .unwrap_or_default();
105        if defs.is_empty() {
106            return Err("ERR:LOAD defs 须为非空字符串数组(定义根)".to_string());
107        }
108
109        Ok(LoadConfig { sources, defs })
110    }
111
112    /// db 名 → (连接声明, 语义深度):同一 db 名挂多个连接 ⇒ `Err`(设计 §5.2)。
113    ///
114    /// 深度由 [`depth_layered`] 按 kind 决定(PG=2,其余=1)。
115    pub fn source_of_db(&self, db: &str) -> Result<(&SourceDecl, u8), String> {
116        let mut hit: Option<&SourceDecl> = None;
117        for s in &self.sources {
118            if s.databases.iter().any(|d| d == db) {
119                if let Some(prev) = hit {
120                    return Err(format!(
121                        "ERR:LOAD 库归属冲突: {db} 同时声明在连接 {} 与 {}(同一 db 名只能挂一个连接)",
122                        prev.name, s.name
123                    ));
124                }
125                hit = Some(s);
126            }
127        }
128        let decl = hit.ok_or_else(|| {
129            format!(
130                "ERR:LOAD 库目录未声明: {db}(须在 store.config.json 的某连接 databases 中列出)"
131            )
132        })?;
133        let depth = depth_layered(&decl.kind)?;
134        Ok((decl, depth))
135    }
136}
137
138/// 语义层级深度:PG=2(L2=schema),Mongo/MySQL/SQLite=1(L2+ 打平)。
139/// kind 非法 ⇒ `Err`(禁回落默认)。
140pub fn depth_layered(kind: &str) -> Result<u8, String> {
141    let ds = DataSource::from_kind(kind)
142        .map_err(|_| format!("ERR:LOAD 连接 kind 非法: {kind}(仅 mongodb/mysql/pg/sqlite/local)"))?;
143    Ok(match ds.backend() {
144        Some(Backend::Postgres) => 2,
145        _ => 1,
146    })
147}
148
149/// 由「db 目录以下的相对路径」算落点(设计 §5.2 固定语义,D3):
150/// - `rel_under_db` 至少 1 段(文件名);除末段(文件名)外最多 `depth-1` 段参与语义,其余**打平**;
151/// - `depth == 2`(PG):有子目录 ⇒ 子目录首段 = `schema`;无子目录 ⇒ `schema = None`
152///   (回落连接默认 search_path,见 `definition.rs` Location 语义);
153/// - `depth == 1`:`schema = None`;L2+ 全部打平(不影响 `database` 归属);
154/// - 输出 `Location { source, database: Some(db), schema }`。
155pub fn locate(rel_under_db: &str, source: &str, db: &str, depth: u8) -> Result<Location, String> {
156    let segs: Vec<&str> = rel_under_db
157        .split(['/', '\\'])
158        .filter(|s| !s.is_empty())
159        .collect();
160    if segs.is_empty() {
161        return Err(format!("ERR:LOAD 定义路径非法(缺文件名): {rel_under_db}"));
162    }
163    // 末段 = 文件名;其余 = 目录段
164    let dirs = &segs[..segs.len() - 1];
165    let schema = if depth >= 2 && !dirs.is_empty() {
166        Some(dirs[0].to_string())
167    } else {
168        None
169    };
170    Ok(Location {
171        source: source.to_string(),
172        database: Some(db.to_string()),
173        schema,
174    })
175}
176
177/// 纯批次规划(设计 §5.2 伪码 + §5.3 主从):
178/// `files: [(rel_from_defs_root, defn)]`(`rel` 首段 = db 目录名)。
179///
180/// 步骤:① 按 `rel` 首段定位 db → `source_of_db` 反查(未声明 / 重挂 ⇒ `Err`);
181///      ② `depth_layered(kind)` → `locate`;
182///      ③ 判据单点 `classify_batch`(分组 / 主唯一 / 批内四元组冲突);
183///      ④ 每组主**恰好一份**,0 份 ⇒ `Err`(A1/A3);
184///      ⑤ 输出有序装载项:**主在前、其后从**(组间按 name 字典序)。
185pub fn plan_load(
186    cfg: &LoadConfig,
187    files: &[(String, Value)],
188) -> Result<Vec<(Value, Location)>, String> {
189    // ①/② 归属 + locate
190    let mut items: Vec<(Value, Location)> = Vec::with_capacity(files.len());
191    for (rel, defn) in files {
192        let segs: Vec<&str> = rel.split(['/', '\\']).filter(|s| !s.is_empty()).collect();
193        if segs.is_empty() {
194            return Err(format!("ERR:LOAD 定义路径非法: {rel}"));
195        }
196        let db = segs[0];
197        let (decl, depth) = cfg.source_of_db(db)?;
198        let rel_under_db = segs[1..].join("/");
199        let loc = locate(&rel_under_db, &decl.name, db, depth)?;
200        items.push((defn.clone(), loc));
201    }
202
203    // ③ 判据单点(分组 / 主唯一 / 批内四元组冲突)
204    let class = classify_batch(&items).map_err(|e| batch_err_to_load(e, files))?;
205
206    // ④/⑤ 组间按 name 字典序;主在前、其后从
207    let mut names: Vec<&String> = class.order.iter().collect();
208    names.sort();
209
210    // 批内四元组冲突(与 register_batch 同源的判据函数)
211    let mut quads: Vec<(String, String, Location)> = Vec::new();
212    for name in &names {
213        if let Some((_, loc, coll)) = class.primaries.get(*name) {
214            quads.push(((*name).clone(), coll.clone(), loc.clone()));
215        }
216        if let Some(rs) = class.replicas.get(*name) {
217            for (defn, loc) in rs {
218                let coll = defn
219                    .get("name")
220                    .and_then(|v| v.as_str())
221                    .unwrap_or(name)
222                    .to_string();
223                quads.push(((*name).clone(), coll, loc.clone()));
224            }
225        }
226    }
227    if let Some(c) = detect_location_conflict(&quads) {
228        return Err(format!(
229            "ERR:LOAD 落点冲突: {} 同时映射 ({}, {:?}, {:?})(同一落点只能有一个 collection;冲突名 `{}` 与 `{}`)",
230            c.collection, c.source, c.database, c.schema, c.prev, c.name
231        ));
232    }
233
234    let mut out: Vec<(Value, Location)> = Vec::with_capacity(items.len());
235    for name in names {
236        let Some((defn, loc, _coll)) = class.primaries.get(name) else {
237            return Err(format!(
238                "ERR:LOAD 主定义缺失: {name}(同名项均为 replica:true,须恰好一份主)"
239            ));
240        };
241        out.push((defn.clone(), loc.clone()));
242        if let Some(rs) = class.replicas.get(name) {
243            for (rdefn, rloc) in rs {
244                out.push((rdefn.clone(), rloc.clone()));
245            }
246        }
247    }
248    Ok(out)
249}
250
251/// `BatchError` → `ERR:LOAD` 文案(`indices` → `files[i].rel` 供定位重复主定义)。
252fn batch_err_to_load(e: BatchError, files: &[(String, Value)]) -> String {
253    match e {
254        BatchError::Parse(m) => format!("ERR:LOAD {m}"),
255        BatchError::PrimaryDuplicate { name, indices } => {
256            let rels: Vec<String> = indices
257                .iter()
258                .map(|i| {
259                    files
260                        .get(*i)
261                        .map(|(r, _)| r.clone())
262                        .unwrap_or_else(|| format!("#{i}"))
263                })
264                .collect();
265            format!(
266                "ERR:LOAD 主定义重复: {name}({})——同名只允许一份主,其余须 replica:true",
267                rels.join(" 与 ")
268            )
269        }
270        BatchError::LocationConflict(c) => format!(
271            "ERR:LOAD 落点冲突: {} 同时映射 ({}, {:?}, {:?})(同一落点只能有一个 collection;冲突名 `{}` 与 `{}`)",
272            c.collection, c.source, c.database, c.schema, c.prev, c.name
273        ),
274    }
275}