Skip to main content

rust_store_core/schema/
registry.rs

1//! Schema 注册表:注册/定位/配置(数据结构与规范化见 [`super::definition`])。
2
3use std::collections::HashMap;
4
5use serde_json::{json, Map, Value};
6
7use crate::types::{is_truthy, str_list, AGG_OPS};
8
9use super::definition::{normalize_fields, ComputeDef, FieldDef, Location, RelationDef, Schema};
10
11use crate::command::WriteLinkPolicy;
12
13/// 查询档位:判决唯一在 core(照 [`Registry::require_context`] 既有范式)。
14///
15/// - [`Profile::Standard`](默认):标准调用 —— 跨方言对齐的公共能力集;
16///   DB 独有能力可用但须代码注释标注「不建议业务查询」(见执行文档 §4.5)。
17/// - [`Profile::Text2Query`]:AI 问数 —— 功能收缩 + 硬性限制(行数/深度/准入全收紧)。
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum Profile {
20    /// 标准调用:跨方言对齐的公共能力集;DB 独有能力可用但须注释标注(不建议业务查询)
21    #[default]
22    Standard,
23    /// AI 问数:功能收缩 + 硬性限制
24    Text2Query,
25}
26
27impl Profile {
28    /// 档位字符串(Host / 绑定层单点取用)
29    pub fn as_str(&self) -> &'static str {
30        match self {
31            Profile::Standard => "standard",
32            Profile::Text2Query => "text2query",
33        }
34    }
35
36    /// 字符串 → 档位;未知值 **Err**(禁静默回落 `Standard`,见执行文档 §7)
37    pub fn from_str_or_err(s: &str) -> Result<Profile, String> {
38        match s {
39            "standard" => Ok(Profile::Standard),
40            "text2query" => Ok(Profile::Text2Query),
41            other => Err(format!("未知 profile: {other}(仅 standard / text2query)")),
42        }
43    }
44}
45
46/// 一条注册项:一份主结构 + 主/从链路 + 版本(D4/D5/D13)。
47#[derive(Debug, Clone)]
48struct Entry {
49    schema: Schema,
50    /// 主 + 从链路;`links[0]` 恒为主落点(= `schema` 的定位投影)
51    links: Vec<Location>,
52    /// 版本:首次注册 = 1;跨批次 reload 同名 ⇒ +1
53    version: u64,
54}
55
56/// 批次分组结果(判据单点输出;`register_batch` 与 `schema::load::plan_load` 共用)。
57pub(super) struct BatchClass {
58    /// 组名(按首次出现顺序)
59    pub order: Vec<String>,
60    /// name → (主定义, 落点, collection);每组至多一份主
61    pub primaries: HashMap<String, (Value, Location, String)>,
62    /// name → 从项(原序)
63    pub replicas: HashMap<String, Vec<(Value, Location)>>,
64}
65
66/// 批次判据错误(调用方各自格式化文案:注册期与装载期文案口径不同)。
67pub(super) enum BatchError {
68    /// 解析错误(非对象 / 缺 name)
69    Parse(String),
70    /// 同名主 ≥2(A1):组名 + 主项在输入中的下标
71    PrimaryDuplicate { name: String, indices: Vec<usize> },
72    /// 批内四元组冲突
73    LocationConflict {
74        collection: String,
75        prev: String,
76        name: String,
77        source: String,
78        database: Option<String>,
79        schema: Option<String>,
80    },
81}
82
83/// 批次判据单点:**分组 + 主唯一(≥2 ⇒ `Err`)+ 批内四元组冲突(⇒ `Err`)**。
84///
85/// **0 主不在此报错**(交由调用方决定:`register_batch` 走既有 entry 追加从链路,
86/// `plan_load` 直接拒——「主定义缺失」)。`register_batch` 与 `plan_load` 共用本函数,
87/// 禁各写一份(判据单点,防 A1/A3 漂移)。
88pub(super) fn classify_batch(items: &[(Value, Location)]) -> Result<BatchClass, BatchError> {
89    let mut order: Vec<String> = Vec::new();
90    let mut primaries: HashMap<String, (Value, Location, String)> = HashMap::new();
91    let mut primary_idx: HashMap<String, usize> = HashMap::new();
92    let mut replicas: HashMap<String, Vec<(Value, Location)>> = HashMap::new();
93
94    for (idx, (defn, loc)) in items.iter().enumerate() {
95        let obj = defn
96            .as_object()
97            .ok_or_else(|| BatchError::Parse("schema 定义必须是对象".to_string()))?;
98        let name = obj
99            .get("name")
100            .and_then(|v| v.as_str())
101            .ok_or_else(|| BatchError::Parse("schema 缺少 name".to_string()))?
102            .to_string();
103        if !order.iter().any(|n| n == &name) {
104            order.push(name.clone());
105        }
106
107        if obj.get("replica").map(is_truthy).unwrap_or(false) {
108            replicas
109                .entry(name)
110                .or_default()
111                .push((defn.clone(), loc.clone()));
112            continue;
113        }
114
115        let collection = obj
116            .get("collection")
117            .and_then(|v| v.as_str())
118            .filter(|s| !s.is_empty())
119            .map(String::from)
120            .unwrap_or_else(|| name.clone());
121        if let Some(first) = primary_idx.get(&name) {
122            // 同名主 ≥2 ⇒ Err(A1);携带下标供调用方定位(rel 路径 / 提示)
123            return Err(BatchError::PrimaryDuplicate {
124                name,
125                indices: vec![*first, idx],
126            });
127        }
128        primary_idx.insert(name.clone(), idx);
129        primaries.insert(name, (defn.clone(), loc.clone(), collection));
130    }
131
132    // 批内四元组冲突:主 + 从全部链路(判据函数 `detect_location_conflict`)
133    let mut quads: Vec<(String, String, Location)> = Vec::new();
134    for name in &order {
135        if let Some((_, loc, coll)) = primaries.get(name) {
136            quads.push((name.clone(), coll.clone(), loc.clone()));
137        }
138        if let Some(rs) = replicas.get(name) {
139            for (defn, loc) in rs {
140                let coll = defn
141                    .get("name")
142                    .and_then(|v| v.as_str())
143                    .unwrap_or(name)
144                    .to_string();
145                quads.push((name.clone(), coll, loc.clone()));
146            }
147        }
148    }
149    if let Some(c) = detect_location_conflict(&quads) {
150        return Err(BatchError::LocationConflict {
151            collection: c.collection,
152            prev: c.prev,
153            name: c.name,
154            source: c.source,
155            database: c.database,
156            schema: c.schema,
157        });
158    }
159
160    Ok(BatchClass {
161        order,
162        primaries,
163        replicas,
164    })
165}
166
167/// 四元组冲突信息(供调用方格式化文案)
168pub(super) struct LocationConflict {
169    pub collection: String,
170    pub prev: String,
171    pub name: String,
172    pub source: String,
173    pub database: Option<String>,
174    pub schema: Option<String>,
175}
176
177/// 四元组 `(source, database, schema, collection)` 冲突检测(**判据单点**):
178/// 同一四元组被两个不同 schema 名占用 ⇒ `Some(冲突)`。`register_batch`(含既有 entry)
179/// 与 `plan_load`(批内)共用。
180pub(super) fn detect_location_conflict(
181    entries: &[(String, String, Location)],
182) -> Option<LocationConflict> {
183    let mut seen: HashMap<(String, Option<String>, Option<String>, String), String> = HashMap::new();
184    for (name, collection, loc) in entries {
185        let key = (
186            loc.source.clone(),
187            loc.database.clone(),
188            loc.schema.clone(),
189            collection.clone(),
190        );
191        if let Some(prev) = seen.get(&key) {
192            if prev != name {
193                return Some(LocationConflict {
194                    collection: collection.clone(),
195                    prev: prev.clone(),
196                    name: name.clone(),
197                    source: loc.source.clone(),
198                    database: loc.database.clone(),
199                    schema: loc.schema.clone(),
200                });
201            }
202        } else {
203            seen.insert(key, name.clone());
204        }
205    }
206    None
207}
208
209/// `BatchError` → 注册期文案(保留既有子串:`同名主定义重复` / `定位冲突`)。
210fn batch_err_to_msg(e: BatchError) -> String {
211    match e {
212        BatchError::Parse(m) => m,
213        BatchError::PrimaryDuplicate { name, .. } => format!(
214            "同名主定义重复: {}(同一装载批次内 name 必须唯一,从定义请用 replica: true)",
215            name
216        ),
217        BatchError::LocationConflict {
218            collection,
219            prev,
220            source,
221            database,
222            schema,
223            ..
224        } => format!(
225            "定位冲突: ({}, {:?}, {:?}, {}) 已被 schema `{}` 占用",
226            source, database, schema, collection, prev
227        ),
228    }
229}
230
231#[derive(Debug, Clone, Default)]
232pub struct Registry {
233    entries: HashMap<String, Entry>,
234    order: Vec<String>,
235    /// 上下文强制开关(默认关闭 = fail-open,与 JS 原版 parity);
236    /// 开启后所有 plan 入口对 `ctx: None` 显式报错(fail-secure,见 `permission` 模块文档)。
237    /// 内部调用请传显式系统上下文(JSON `{"internal": true}` / `Context::system()`)。
238    require_context: bool,
239    /// 查询档位(默认 [`Profile::Standard`];text2query 由 AI 问数链路显式进入)。
240    profile: Profile,
241    /// RBAC 动态策略(`None` = 未启用,判决原语直通、行为与现状一致)。见 [`crate::rbac`]
242    rbac: Option<crate::rbac::RbacPolicy>,
243    /// 角色清单与未配置姿态(豁免 / 拒写 / Open|Closed,默认 []/[]/Open)。见 [`crate::permission::RoleRules`]
244    role_rules: crate::permission::RoleRules,
245    /// 定义层门禁策略(默认 Open —— 全放行,保持既有 parity)。见 [`crate::permission::MetaPolicy`]
246    meta_policy: crate::permission::MetaPolicy,
247    /// 跨连接写策略(默认 [`WriteLinkPolicy::Reject`])。见 [`crate::command::write_links`]
248    write_link_policy: WriteLinkPolicy,
249}
250
251impl Registry {
252    pub fn new() -> Self {
253        Self::default()
254    }
255
256    /// 注册一个 schema(含自动注册 `<Name>Deleted` 归档表),对应 JS `register`。
257    ///
258    /// 兼容入口:单条 + 缺省落点(`source="default"`、`database/schema=None`)。
259    /// 定义层门禁按当前 [`MetaPolicy`](crate::permission::MetaPolicy) 判决
260    /// (默认 Open → 全放行)。需带 ctx 显式过门禁请用 [`Self::register_with_ctx`]。
261    pub fn register(&mut self, defn: &Value) -> Result<(), String> {
262        self.register_with_ctx(defn, None)
263    }
264
265    /// 带 ctx 的注册(单条 + 缺省落点):内部转 [`Self::register_batch`]。
266    pub fn register_with_ctx(
267        &mut self,
268        defn: &Value,
269        ctx: Option<&crate::permission::Context>,
270    ) -> Result<(), String> {
271        self.register_batch(&[(defn.clone(), Location::default())], ctx)
272    }
273
274    /// 带定位的批量注册(D13:唯一性校验落「装载批次集合」)。
275    ///
276    /// 算法(**先构造后落库,任一步 Err ⇒ 零副作用**):
277    /// 1. 判据单点 [`classify_batch`](分组 / 主唯一 ≥2 ⇒ `Err` / 批内四元组冲突),与
278    ///    `schema::load::plan_load` 同源。
279    /// 2. `can_register` 门禁(判决先于 `build_schema`)。
280    /// 3. 逐组:有主 ⇒ `build_schema` 为 **主**;0 主 ⇒ 追加既有同名 entry 的从链路,无既有 ⇒ `Err`。
281    /// 4. 全批 + 既有 `(source, database, schema, collection)` 跨名冲突 ⇒ `Err`
282    ///    (判据函数 [`detect_location_conflict`],替代旧 `check_location_unique`)。
283    /// 5. 提交:`links = [primary.loc] ++ replicas`;命中既有 entry ⇒ `version + 1`
284    ///    (`order` 位置不变)、替换结构与链路;新名 ⇒ `version = 1`、`order.push`。
285    ///    `Schema.source/database/schema` 由 `primary.loc` 注入(`source == "default"` ⇒ `None`)。
286    /// 6. 归档派生:主 `name` 不以 `"Deleted"` 结尾且非 `_isArchive` ⇒ 派生 `<Name>Deleted`
287    ///    (`links` 继承主链路,定位同主)。
288    pub fn register_batch(
289        &mut self,
290        items: &[(Value, Location)],
291        ctx: Option<&crate::permission::Context>,
292    ) -> Result<(), String> {
293        // ── 1:判据单点(分组 / 主唯一 / 批内四元组冲突),与 plan_load 同源 ──
294        let class = classify_batch(items).map_err(batch_err_to_msg)?;
295
296        // 定义层门禁:判决先于 build_schema —— 拒绝即返回,绝不部分写入
297        for (defn, _loc) in items {
298            let name = defn.get("name").and_then(|v| v.as_str()).unwrap_or("");
299            if !crate::permission::can_register(&self.meta_policy, ctx) {
300                return Err(format!("ERR_PERMISSION: 无权注册或覆盖定义 {name}"));
301            }
302        }
303
304        // ── 2:逐组拟定 entry 序列(主 + 归档附表,保序) ──
305        struct Planned {
306            name: String,
307            schema: Schema,
308            links: Vec<Location>,
309        }
310        let mut planned: Vec<Planned> = Vec::new();
311        for name in &class.order {
312            let rs: Vec<Location> = class
313                .replicas
314                .get(name)
315                .map(|v| v.iter().map(|(_, l)| l.clone()).collect())
316                .unwrap_or_default();
317            if let Some((raw, loc, collection)) = class.primaries.get(name) {
318                let obj = raw.as_object().unwrap();
319                let schema = build_schema(obj, name.clone(), collection.clone(), loc)?;
320                let is_archive = obj.get("_isArchive").map(is_truthy).unwrap_or(false);
321                let mut links = vec![loc.clone()];
322                links.extend(rs);
323                planned.push(Planned {
324                    name: name.clone(),
325                    schema: schema.clone(),
326                    links: links.clone(),
327                });
328                // 归档附表紧随主定义派生(`links` 继承主链路,定位同主)
329                if !is_archive && !name.ends_with("Deleted") {
330                    let aobj = archive_defn(obj, name, &schema.collection)
331                        .as_object()
332                        .cloned()
333                        .unwrap();
334                    let aname = format!("{}Deleted", name);
335                    let acoll = format!("{}_deleted", schema.collection);
336                    let aschema = build_schema(&aobj, aname.clone(), acoll, &links[0])?;
337                    planned.push(Planned {
338                        name: aname,
339                        schema: aschema,
340                        links,
341                    });
342                }
343            } else {
344                // 0 主:只能是对既有 entry 追加从链路(跨批演进)
345                let Some(existing) = self.entries.get(name) else {
346                    return Err(format!(
347                        "未找到主 schema: {}(从定义 `replica` 必须伴随同批主定义,或该 name 已注册)",
348                        name
349                    ));
350                };
351                let mut links = existing.links.clone();
352                links.extend(rs);
353                planned.push(Planned {
354                    name: name.clone(),
355                    schema: existing.schema.clone(),
356                    links,
357                });
358            }
359        }
360
361        // ── 3:四元组唯一性(本批 + 既有,跨名冲突即 Err;判据函数单点) ──
362        let batch_names: std::collections::HashSet<&str> =
363            planned.iter().map(|p| p.name.as_str()).collect();
364        let mut quads: Vec<(String, String, Location)> = Vec::new();
365        for (n, e) in &self.entries {
366            if batch_names.contains(n.as_str()) {
367                continue; // 本批将整体替换该名,旧链路不参与冲突判定
368            }
369            for l in &e.links {
370                quads.push((n.clone(), e.schema.collection.clone(), l.clone()));
371            }
372        }
373        for p in &planned {
374            for l in &p.links {
375                quads.push((p.name.clone(), p.schema.collection.clone(), l.clone()));
376            }
377        }
378        if let Some(c) = detect_location_conflict(&quads) {
379            return Err(format!(
380                "定位冲突: ({}, {:?}, {:?}, {}) 已被 schema `{}` 占用",
381                c.source, c.database, c.schema, c.collection, c.prev
382            ));
383        }
384
385        // ── 4:提交(命中既有 ⇒ version + 1 且 order 位置不变;新名 ⇒ version = 1 + push) ──
386        for p in planned {
387            if let Some(e) = self.entries.get_mut(&p.name) {
388                e.schema = p.schema;
389                e.links = p.links;
390                e.version += 1;
391            } else {
392                self.entries.insert(
393                    p.name.clone(),
394                    Entry {
395                        schema: p.schema,
396                        links: p.links,
397                        version: 1,
398                    },
399                );
400                self.order.push(p.name);
401            }
402        }
403
404        Ok(())
405    }
406
407    /// 主链路 + 从链路(只读)
408    pub fn links_of(&self, name: &str) -> Result<&[Location], String> {
409        Ok(&self.get_entry(name)?.links)
410    }
411
412    /// 主落点(= `links[0]`)
413    pub fn primary_location(&self, name: &str) -> Result<&Location, String> {
414        self.get_entry(name)?
415            .links
416            .first()
417            .ok_or_else(|| format!("Schema 无落点链路: {}", name))
418    }
419
420    /// 版本(首次 = 1)
421    pub fn version_of(&self, name: &str) -> Result<u64, String> {
422        Ok(self.get_entry(name)?.version)
423    }
424
425    fn get_entry(&self, name: &str) -> Result<&Entry, String> {
426        self.entries
427            .get(name)
428            .ok_or_else(|| format!("Schema 未注册: {}", name))
429    }
430
431    /// 按名称获取 schema
432    pub fn get(&self, name: &str) -> Result<&Schema, String> {
433        self.get_entry(name).map(|e| &e.schema)
434    }
435
436    pub fn has(&self, name: &str) -> bool {
437        self.entries.contains_key(name)
438    }
439
440    /// 清空 schema 注册表(`schemas` + `order`),对应绑定层的测试隔离 / 动态重建场景。
441    ///
442    /// 只清 schema,**不动**配置开关(`require_context` / `profile` / `rbac` /
443    /// `role_rules`)与回调表(`clear_fns` 对称:各清各的)——开关生命周期属
444    /// Registry 配置面,不随 schema 集合重建而丢。
445    pub fn clear(&mut self) {
446        self.entries.clear();
447        self.order.clear();
448    }
449
450    /// 开关「上下文强制」(默认关闭 = fail-open,保持 JS parity)。
451    /// 开启后:plan 入口遇 `ctx: None` 报 `ERR_NO_CONTEXT`(fail-secure)。
452    /// 内部调用须显式传系统上下文(`{"internal": true}`)。
453    pub fn set_require_context(&mut self, require: bool) {
454        self.require_context = require;
455    }
456
457    /// 「上下文强制」开关当前值
458    pub fn require_context(&self) -> bool {
459        self.require_context
460    }
461
462    /// 设置查询档位(`standard` / `text2query`)。
463    ///
464    /// 档位为**单值状态**(非栈):由 Host 的 `text2query()` 上下文管理器负责
465    /// 进入时设档、退出时恢复(见 `py-store` / `nodejs-store` 门面)。判决一律在 core。
466    pub fn set_profile(&mut self, profile: Profile) {
467        self.profile = profile;
468    }
469
470    /// 当前查询档位
471    pub fn profile(&self) -> Profile {
472        self.profile
473    }
474
475    /// 豁免角色清单:命中者在一切判决环节(静态 + RBAC)直接放行。默认空——无豁免。
476    pub fn set_exempt_roles(&mut self, roles: Vec<String>) {
477        self.role_rules.exempt_roles = roles;
478    }
479
480    /// 拒写角色清单:命中者一切写路径拒绝(读不受影响)。默认空——无拒写。
481    pub fn set_deny_write_roles(&mut self, roles: Vec<String>) {
482        self.role_rules.deny_write_roles = roles;
483    }
484
485    /// schema 白名单缺失/为空时的默认姿态。默认 Open(保持现状语义)。
486    pub fn set_unconfigured_policy(&mut self, policy: crate::permission::UnconfiguredPolicy) {
487        self.role_rules.unconfigured = policy;
488    }
489
490    /// 当前角色规则(静态判决函数与 RBAC decide 的共用取参入口)
491    pub fn role_rules(&self) -> &crate::permission::RoleRules {
492        &self.role_rules
493    }
494
495    /// 定义层门禁策略(`closed=true` 时仅 internal 或 `roles` 白名单可注册/覆盖)。
496    /// 默认 Open —— 全放行(保持既有 parity;须在首次业务注册前调用方生效于该次注册)。
497    pub fn set_meta_policy(&mut self, closed: bool, roles: Vec<String>) {
498        self.meta_policy = crate::permission::MetaPolicy { closed, roles };
499    }
500
501    /// 当前定义层门禁策略
502    pub fn meta_policy(&self) -> &crate::permission::MetaPolicy {
503        &self.meta_policy
504    }
505
506    /// 设置跨连接写策略(默认 [`WriteLinkPolicy::Reject`])。
507    ///
508    /// 判决唯一在 core([`crate::command::write_links::resolve_write_links`]);
509    /// 宿主仅需在初始化期把它透传到 registry。
510    pub fn set_write_link_policy(&mut self, policy: WriteLinkPolicy) {
511        self.write_link_policy = policy;
512    }
513
514    /// 当前跨连接写策略
515    pub fn write_link_policy(&self) -> WriteLinkPolicy {
516        self.write_link_policy
517    }
518
519    /// 只读定义层判决(`workflow` 等宿主侧定义面复用同一门禁;判决唯一在 core)。
520    ///
521    /// 不改变任何注册状态:Open 全放行;Closed 仅 `internal` 或 `roles` 白名单角色。
522    /// 语义与 [`Self::register_with_ctx`] 的门禁判据**完全一致**(同一
523    /// [`can_register`](crate::permission::can_register)),仅作只读暴露。
524    pub fn can_register(&self, ctx: Option<&crate::permission::Context>) -> bool {
525        crate::permission::can_register(&self.meta_policy, ctx)
526    }
527
528    /// 注入/清除 RBAC 动态策略;`None` = 关闭(判决原语直通)。
529    /// 解析失败 Err(fail-fast,禁静默吞配置错误),成功后判决链路即刻生效。
530    pub fn set_rbac(&mut self, policy: Option<&Value>) -> Result<(), String> {
531        match policy {
532            None => {
533                self.rbac = None;
534                Ok(())
535            }
536            Some(v) => {
537                let p = crate::rbac::RbacPolicy::from_json(v)?;
538                self.rbac = Some(p);
539                Ok(())
540            }
541        }
542    }
543
544    /// 当前 RBAC 策略(`None` = 未启用)
545    pub fn rbac(&self) -> Option<&crate::rbac::RbacPolicy> {
546        self.rbac.as_ref()
547    }
548
549    /// 按定位四元组精确获取 schema(命令路由的唯一定位入口)
550    ///
551    /// 命中判据:`collection` 相等 **且** 四元组命中该 entry 的**任一链路**。
552    /// **不做任何猜测回落**。
553    pub fn get_by_location(
554        &self,
555        source: &str,
556        database: Option<&str>,
557        schema: Option<&str>,
558        collection: &str,
559    ) -> Result<&Schema, String> {
560        let db = database.filter(|s| !s.is_empty());
561        let sc = schema.filter(|s| !s.is_empty());
562        self.entries
563            .values()
564            .find(|e| {
565                e.schema.collection == collection
566                    && e.links.iter().any(|l| {
567                        l.source == source
568                            && l.database.as_deref().filter(|s| !s.is_empty()) == db
569                            && l.schema.as_deref().filter(|s| !s.is_empty()) == sc
570                    })
571            })
572            .map(|e| &e.schema)
573            .ok_or_else(|| {
574                format!(
575                    "Schema 未定位(source = {}, database = {:?}, schema = {:?}, collection = {})",
576                    source, db, sc, collection
577                )
578            })
579    }
580
581    /// 命令结构定位(方言翻译用):优先四元组精确命中;未命中时(`route_override`
582    /// 改写落点的多租户场景)回落 `(source, collection)` 定位 —— 结构 schema
583    /// 与定位落点正交(见 multi-datasource-routing-plan.md §6:权限/字段校验仍按结构 schema)。
584    ///
585    /// `(source, collection)` 恰一候选 → 命中;多候选时取 `database`/`schema` 均 `null` 的
586    /// 结构声明,无该声明 → 报错(拒绝猜测,铁律 3)。
587    pub fn get_for_command(
588        &self,
589        source: &str,
590        database: Option<&str>,
591        schema: Option<&str>,
592        collection: &str,
593    ) -> Result<&Schema, String> {
594        if let Ok(s) = self.get_by_location(source, database, schema, collection) {
595            return Ok(s);
596        }
597        let candidates: Vec<&Entry> = self
598            .entries
599            .values()
600            .filter(|e| {
601                e.schema.collection == collection && e.links.iter().any(|l| l.source == source)
602            })
603            .collect();
604        match candidates.len() {
605            0 => self.get_by_location(source, database, schema, collection),
606            1 => Ok(&candidates[0].schema),
607            _ => candidates
608                .iter()
609                .find(|e| e.schema.database().is_none() && e.schema.schema().is_none())
610                .map(|e| &e.schema)
611                .ok_or_else(|| {
612                    format!(
613                        "命令定位 ({}, {:?}, {:?}, {}) 存在多个结构 schema(多租户 override 需唯一的 (source, collection) 结构声明)",
614                        source, database, schema, collection
615                    )
616                }),
617        }
618    }
619
620    pub fn list(&self) -> Vec<String> {
621        self.order.clone()
622    }
623
624    /// schema 绑定的数据源名(= 主链路 source;`default` → `None`)
625    pub fn schema_datasource(&self, name: &str) -> Result<Option<String>, String> {
626        Ok(self.get(name)?.source.clone())
627    }
628
629    /// 解析 schema 绑定的数据源(`config` 为 `{ "sources": { name: kind } }`)
630    ///
631    /// 纯逻辑:不持有连接(连接句柄留在 Host)。`null` / 空配置下等价单源 Mongo,
632    /// 保证既有调用方与 parity fixture 零变更。见 [`crate::datasource`]。
633    pub fn resolve_datasource(
634        &self,
635        schema_name: &str,
636        config: &Value,
637    ) -> Result<crate::datasource::DataSource, String> {
638        let declared = self.get(schema_name)?.source.as_deref();
639        let cfg = crate::datasource::DataSourceConfig::from_json(config)?;
640        cfg.resolve(declared)
641    }
642}
643
644/// 由原始定义构建 [`Schema`](字段/关系/计算列/元数据分块解析,保持单一职责)。
645/// 落点(`source`/`database`/`schema`)由 `loc` 注入(定义文件零落点)。
646fn build_schema(
647    obj: &Map<String, Value>,
648    name: String,
649    collection: String,
650    loc: &Location,
651) -> Result<Schema, String> {
652    let timestamps = parse_timestamps(obj)?;
653    let mut fields = normalize_fields(obj.get("fields"))?;
654    if timestamps {
655        add_timestamp_fields(&mut fields);
656    }
657    let relations = parse_relations(obj);
658    // 计算列的 agg 需要校验「引用的关系存在」→ 必须晚于 relations 解析
659    let computes = parse_computes(obj, &relations)?;
660    // 命名冲突检测(A7):fields ∪ relations ∪ computes 归一后撞名 / 撞保留键 ⇒ 报错
661    // (三者同属结果文档命名空间;`_id` 已在 detect_conflicts 内豁免)
662    let mut ids: Vec<&str> = Vec::new();
663    ids.extend(fields.keys().map(String::as_str));
664    ids.extend(relations.keys().map(String::as_str));
665    ids.extend(computes.iter().map(|(k, _)| k.as_str()));
666    crate::naming::detect_conflicts(ids)?;
667    Ok(Schema {
668        name,
669        collection,
670        id_prefix: obj
671            .get("idPrefix")
672            .and_then(|v| v.as_str())
673            .unwrap_or("")
674            .to_string(),
675        timestamps,
676        fields,
677        relations,
678        computes,
679        read: str_list(obj.get("read")),
680        write: str_list(obj.get("write")),
681        indexes: obj
682            .get("indexes")
683            .and_then(|v| v.as_array())
684            .cloned()
685            .unwrap_or_default(),
686        source: if loc.source == crate::datasource::DEFAULT_SOURCE {
687            None
688        } else {
689            Some(loc.source.clone())
690        },
691        database: loc.database.clone().filter(|s| !s.is_empty()),
692        schema: loc.schema.clone().filter(|s| !s.is_empty()),
693    })
694}
695
696/// `timestamps` 仅接受 true/false/'ms'/'s';单位换算在 Host(core 无时钟,now 由 Host 传入)
697fn parse_timestamps(obj: &Map<String, Value>) -> Result<bool, String> {
698    match obj.get("timestamps") {
699        None | Some(Value::Null) | Some(Value::Bool(true)) => Ok(true),
700        Some(Value::Bool(false)) => Ok(false),
701        Some(Value::String(s)) if s == "ms" || s == "s" => Ok(true),
702        Some(Value::String(s)) => Err(format!(
703            "timestamps 仅支持 true/false/'ms'/'s',实际 {:?}",
704            s
705        )),
706        Some(other) => Err(format!(
707            "timestamps 仅支持 true/false/'ms'/'s',实际 {}",
708            other
709        )),
710    }
711}
712
713/// 自动补时间戳字段(`timestamps !== false` 时;已声明则不覆盖)
714fn add_timestamp_fields(fields: &mut HashMap<String, FieldDef>) {
715    for key in ["createdAt", "updatedAt"] {
716        fields.entry(key.to_string()).or_insert_with(|| FieldDef {
717            field_type: "number".to_string(),
718            required: false,
719            default: None,
720            read: None,
721            write: None,
722            fields: None,
723            strategy: None,
724        });
725    }
726}
727
728fn parse_relations(obj: &Map<String, Value>) -> HashMap<String, RelationDef> {
729    let mut relations = HashMap::new();
730    if let Some(Value::Object(rm)) = obj.get("relations") {
731        for (key, val) in rm {
732            let vo = val.as_object();
733            let get_str = |k: &str| -> Option<String> {
734                vo.and_then(|o| o.get(k))
735                    .and_then(|v| v.as_str())
736                    .filter(|s| !s.is_empty())
737                    .map(String::from)
738            };
739            relations.insert(
740                key.clone(),
741                RelationDef {
742                    model: get_str("model").unwrap_or_default(),
743                    rel_type: get_str("type").unwrap_or_else(|| "many".to_string()),
744                    local_field: get_str("localField").unwrap_or_else(|| "_id".to_string()),
745                    foreign_field: get_str("foreignField").unwrap_or_else(|| key.clone()),
746                    read: str_list(vo.and_then(|o| o.get("read"))),
747                },
748            );
749        }
750    }
751    relations
752}
753
754fn parse_computes(
755    obj: &Map<String, Value>,
756    relations: &HashMap<String, RelationDef>,
757) -> Result<Vec<(String, ComputeDef)>, String> {
758    let mut computes = Vec::new();
759    if let Some(Value::Object(cm)) = obj.get("computes") {
760        for (key, val) in cm {
761            let vo = val.as_object();
762            let get = |k: &str| vo.and_then(|o| o.get(k)).cloned();
763            let has_fn = get("fn").map(|v| is_truthy(&v)).unwrap_or(false);
764            let has_async_fn = get("asyncFn").map(|v| is_truthy(&v)).unwrap_or(false);
765            let agg = parse_agg(key, get("agg").as_ref(), relations, has_fn || has_async_fn)?;
766            computes.push((
767                key.clone(),
768                ComputeDef {
769                    comp_type: get("type")
770                        .and_then(|v| v.as_str().map(String::from))
771                        .filter(|s| !s.is_empty())
772                        .unwrap_or_else(|| "any".to_string()),
773                    has_fn,
774                    has_async_fn,
775                    fn_ref: get("fnRef")
776                        .and_then(|v| v.as_str().map(String::from))
777                        .filter(|s| !s.is_empty()),
778                    agg,
779                    depends: str_list(get("depends").as_ref()).unwrap_or_default(),
780                    read: str_list(get("read").as_ref()),
781                },
782            ));
783        }
784    }
785    Ok(computes)
786}
787
788/// 解析并校验归一聚合计算列(§9.2(2)):`{"$count":"lessons"}` / `{"$sum":"lessons.duration"}`。
789///
790/// - 算子白名单 `$count/$sum/$avg/$min/$max`,不在白名单 → Err(绝不静默);
791/// - 取值必须是**关系路径**:`$count` 仅关系整行计数(`lessons`,不接受字段),
792///   其余算子必须带字段(`lessons.duration`);
793/// - 二级关系路径(`orders.items.price`)首批不支持 → Err;
794/// - 与 `fn` / `asyncFn` 互斥。
795fn parse_agg(
796    key: &str,
797    raw: Option<&Value>,
798    relations: &HashMap<String, RelationDef>,
799    has_callback: bool,
800) -> Result<Option<Value>, String> {
801    let Some(v) = raw.filter(|v| is_truthy(v)) else {
802        return Ok(None);
803    };
804    if has_callback {
805        return Err(format!(
806            "计算列 \"{key}\" 不能同时声明 agg 与 fn/asyncFn(二选一)"
807        ));
808    }
809    let obj = v.as_object().ok_or_else(|| {
810        format!("计算列 \"{key}\" 的 agg 必须是对象,如 {{\"$count\": \"lessons\"}}")
811    })?;
812    let mut it = obj.iter();
813    let (op, arg) = match (it.next(), it.next()) {
814        (Some(kv), None) => kv,
815        _ => return Err(format!("计算列 \"{key}\" 的 agg 必须且只能声明一个算子键")),
816    };
817    if !AGG_OPS.contains(&op.as_str()) {
818        return Err(format!(
819            "计算列 \"{key}\" 的 agg 算子 {op} 不在白名单($count/$sum/$avg/$min/$max)"
820        ));
821    }
822    let path = arg
823        .as_str()
824        .filter(|s| !s.is_empty() && *s != "*")
825        .ok_or_else(|| {
826            format!("计算列 \"{key}\" 的 agg 取值必须是关系路径字符串(如 \"lessons\" / \"lessons.duration\")")
827        })?;
828    let (rel, field) = match path.split_once('.') {
829        Some((r, f)) => (r, Some(f)),
830        None => (path, None),
831    };
832    if !relations.contains_key(rel) {
833        return Err(format!(
834            "计算列 \"{key}\" 的 agg 引用的关系 \"{rel}\" 未在 schema 中定义"
835        ));
836    }
837    if field
838        .map(|f| f.is_empty() || f.contains('.'))
839        .unwrap_or(false)
840    {
841        return Err(format!(
842            "计算列 \"{key}\" 的 agg 关系路径 \"{path}\" 非法:仅支持单级「关系.字段」(二级路径首批不支持)"
843        ));
844    }
845    match op.as_str() {
846        "$count" if field.is_some() => Err(format!(
847            "计算列 \"{key}\" 的 $count 仅支持关系整行计数(如 \"lessons\"),按字段计数待 P5"
848        )),
849        "$count" => Ok(Some(v.clone())),
850        _ if field.is_none() => Err(format!(
851            "计算列 \"{key}\" 的 agg 算子 {op} 必须引用关系字段(如 \"lessons.duration\")"
852        )),
853        _ => Ok(Some(v.clone())),
854    }
855}
856
857/// 构造 `<Name>Deleted` 归档表 schema 定义(字段 = 原字段 + `deletedAt`)。
858/// 归档表与原表同落点 —— 落点不由定义文件携带,改由批次继承主链路(见 `register_batch`)。
859fn archive_defn(obj: &Map<String, Value>, name: &str, collection: &str) -> Value {
860    let mut arch_fields = obj
861        .get("fields")
862        .and_then(|v| v.as_object())
863        .cloned()
864        .unwrap_or_default();
865    arch_fields.insert("deletedAt".to_string(), json!({ "type": "number" }));
866    // 剔除 `_id` 的自增策略:归档表显式拷贝源行 `_id` 值(upsert-by-_id 幂等),
867    // 不走数据库自增;DDL 侧也据此保持归档表 `_id` 为普通主键列
868    if let Some(idf) = arch_fields.get_mut("_id") {
869        if let Some(o) = idf.as_object_mut() {
870            o.remove("strategy");
871        }
872    }
873    let arch = json!({
874        "name": format!("{}Deleted", name),
875        "collection": format!("{}_deleted", collection),
876        "idPrefix": "",
877        "_isArchive": true,
878        "fields": Value::Object(arch_fields),
879        "indexes": obj.get("indexes").cloned().unwrap_or_else(|| json!([])),
880    });
881    arch
882}
883
884#[cfg(test)]
885mod tests {
886    use super::*;
887
888    #[test]
889    fn write_link_policy_defaults_to_reject() {
890        let r = Registry::new();
891        assert_eq!(r.write_link_policy(), WriteLinkPolicy::Reject);
892        let mut r2 = Registry::new();
893        assert_eq!(r2.write_link_policy(), WriteLinkPolicy::Reject);
894        r2.set_write_link_policy(WriteLinkPolicy::PrimaryOnly);
895        assert_eq!(r2.write_link_policy(), WriteLinkPolicy::PrimaryOnly);
896    }
897}