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