Skip to main content

rust_store_core/schema/
definition.rs

1//! Schema 的数据结构与字段规范化(`Registry` 见 [`super::registry`])。
2
3use std::collections::HashMap;
4
5use serde_json::{Map, Value};
6
7use crate::datasource::DEFAULT_SOURCE;
8use crate::types::{is_truthy, str_list};
9
10/// 链路角色:读解析时决定链路的优先级层级(`Primary` 最高)。
11/// 仅在 `store.config.json` 的 `links` 声明中出现,**禁写入 schema 定义文件**。
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Role {
14    /// 主链路:读路径的默认选中落点
15    Primary,
16    /// 从链路:主不可用时的备选
17    Replica,
18    /// 缓存链路
19    Cache,
20    /// 归档链路
21    Archive,
22}
23
24impl Role {
25    /// 非法字符串 ⇒ Err(禁静默回落)
26    pub fn parse(s: &str) -> Result<Role, String> {
27        match s {
28            "primary" => Ok(Role::Primary),
29            "replica" => Ok(Role::Replica),
30            "cache" => Ok(Role::Cache),
31            "archive" => Ok(Role::Archive),
32            other => Err(format!(
33                "未知链路角色: {other}(仅 primary/replica/cache/archive)"
34            )),
35        }
36    }
37
38    pub fn as_str(&self) -> &'static str {
39        match self {
40            Role::Primary => "primary",
41            Role::Replica => "replica",
42            Role::Cache => "cache",
43            Role::Archive => "archive",
44        }
45    }
46
47    /// 排序键:Primary=0, Replica=1, Cache=2, Archive=3
48    pub fn rank(&self) -> u8 {
49        match self {
50            Role::Primary => 0,
51            Role::Replica => 1,
52            Role::Cache => 2,
53            Role::Archive => 3,
54        }
55    }
56}
57
58/// 落点:定义之外,由「目录语义 + 连接配置」解析得到(设计 §5.2)。
59/// 落点维度 = `source` + `database` + `schema`(`schema` 仅 PostgreSQL);
60/// `role` / `priority` 为链路声明附着的可选属性(缺省 `None`)。
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct Location {
63    /// 连接选择(source 名);缺省 [`DEFAULT_SOURCE`]
64    pub source: String,
65    /// 连接内「库」:Mongo db / MySQL database / SQLite attached / PG 库
66    pub database: Option<String>,
67    /// 连接内「schema」:仅 PostgreSQL
68    pub schema: Option<String>,
69    /// 链路角色(由 `links` 声明附着;缺省 `None` = 未声明)
70    pub role: Option<Role>,
71    /// 链路优先级(同 role 内升序;`None` 视作最大;允许负数)
72    pub priority: Option<i64>,
73}
74
75impl Default for Location {
76    fn default() -> Self {
77        Self {
78            source: DEFAULT_SOURCE.to_string(),
79            database: None,
80            schema: None,
81            role: None,
82            priority: None,
83        }
84    }
85}
86
87impl Location {
88    /// 落点同址判据(**单点**):仅比 `(source, database, schema)`,不含 role/priority。
89    ///
90    /// `PartialEq` 由 derive 覆盖全部字段,故任何「两落点是否同址」的判断一律改用本方法。
91    pub fn same_place(&self, other: &Location) -> bool {
92        self.source == other.source
93            && self.database == other.database
94            && self.schema == other.schema
95    }
96}
97
98#[derive(Debug, Clone)]
99pub struct FieldDef {
100    pub field_type: String,
101    pub required: bool,
102    pub default: Option<Value>,
103    pub read: Option<Vec<String>>,
104    pub write: Option<Vec<String>>,
105    /// 嵌套 object 字段的原始定义(未规范化,与 JS 保持一致)
106    pub fields: Option<Value>,
107    /// ID 供给策略(阶段2):仅 `_id` 字段有意义 —— `"autoincrement"` = 数据库自增列
108    /// 赋值(SQL 后端),宿主不生成、core 不注入 `_id`;Mongo 后端无自增语义,
109    /// 执行器对此显式报错(AUTOINCREMENT_NOT_SUPPORTED)。其余值非法(注册期校验)。
110    pub strategy: Option<String>,
111}
112
113#[derive(Debug, Clone)]
114pub struct RelationDef {
115    pub model: String,
116    pub rel_type: String,
117    pub local_field: String,
118    pub foreign_field: String,
119    pub read: Option<Vec<String>>,
120}
121
122#[derive(Debug, Clone)]
123pub struct ComputeDef {
124    pub comp_type: String,
125    pub has_fn: bool,
126    pub has_async_fn: bool,
127    /// 回调标识:Host 经 FnRegistry 把它绑定到实现;缺省 = 计算列 key 名
128    pub fn_ref: Option<String>,
129    /// 归一聚合算子(§9.2(2)):`{"$count": "lessons"}` / `{"$sum": "lessons.duration"}`。
130    /// 取代旧的 `lookup` + `addFields` Mongo 专用形态;SQL / Mongo 走同一语法(执行下推优先)。
131    pub agg: Option<Value>,
132    pub depends: Vec<String>,
133    pub read: Option<Vec<String>>,
134}
135
136#[derive(Debug, Clone)]
137pub struct Schema {
138    pub name: String,
139    pub collection: String,
140    pub id_prefix: String,
141    pub timestamps: bool,
142    pub fields: HashMap<String, FieldDef>,
143    pub relations: HashMap<String, RelationDef>,
144    /// 保持注册顺序:`build_agg_stages` 的输出顺序依赖它
145    pub computes: Vec<(String, ComputeDef)>,
146    /// 写触发器声明(事件 → 触发列表;解析见 `super::triggers`,规划见 `crate::command::triggers`)
147    pub triggers: super::triggers::Triggers,
148    pub read: Option<Vec<String>>,
149    pub write: Option<Vec<String>>,
150    /// 原始索引定义(`[{keys: {...}, options: {...}}]`),upsert 条件构建依赖 unique 索引
151    pub indexes: Vec<Value>,
152    /// 数据源名(由 `register_batch` 的 [`Location`] 注入;`None` = 缺省 `default`)。
153    /// 见 [`crate::datasource`]
154    pub source: Option<String>,
155    /// 连接内「库」(由 `register_batch` 注入;`None` = 连接默认:Mongo db 实例的库名、
156    /// PG 的库、MySQL 的连接库、SQLite 的 main)。
157    pub database: Option<String>,
158    /// 连接内「schema」(仅 PG;由 `register_batch` 注入;`None` = 连接默认 search_path)。
159    pub schema: Option<String>,
160}
161
162impl Schema {
163    pub fn compute(&self, name: &str) -> Option<&ComputeDef> {
164        self.computes
165            .iter()
166            .find(|(k, _)| k == name)
167            .map(|(_, c)| c)
168    }
169
170    /// 解析后的数据源名(缺省 `default`)
171    pub fn source(&self) -> &str {
172        self.source
173            .as_deref()
174            .unwrap_or(crate::datasource::DEFAULT_SOURCE)
175    }
176
177    /// 解析后的 database(空串归一为 `None`)
178    pub fn database(&self) -> Option<&str> {
179        self.database.as_deref().filter(|s| !s.is_empty())
180    }
181
182    /// 解析后的 schema(空串归一为 `None`;仅 PG)
183    pub fn schema(&self) -> Option<&str> {
184        self.schema.as_deref().filter(|s| !s.is_empty())
185    }
186
187    /// 主落点(由 `source`/`database`/`schema` 组装)。
188    ///
189    /// 等价于 `Registry::primary_location(name)` 的投影;链路缺失时的回落。
190    pub fn location(&self) -> Location {
191        Location {
192            source: self.source().to_string(),
193            database: self.database().map(String::from),
194            schema: self.schema().map(String::from),
195            role: None,
196            priority: None,
197        }
198    }
199
200    /// `_id` 是否声明 `strategy: "autoincrement"`(阶段2:数据库自增主键)
201    pub fn id_is_autoincrement(&self) -> bool {
202        self.fields.get("_id").and_then(|f| f.strategy.as_deref()) == Some("autoincrement")
203    }
204}
205
206/// 规范化 fields 定义(字符串简写 → `{type, required:false}`)
207pub(super) fn normalize_fields(v: Option<&Value>) -> Result<HashMap<String, FieldDef>, String> {
208    let mut out = HashMap::new();
209    let Some(Value::Object(map)) = v else {
210        return Ok(out);
211    };
212    for (key, val) in map {
213        let fd = if let Some(s) = val.as_str() {
214            FieldDef {
215                field_type: s.to_string(),
216                required: false,
217                default: None,
218                read: None,
219                write: None,
220                fields: None,
221                strategy: None,
222            }
223        } else if let Some(o) = val.as_object() {
224            FieldDef {
225                field_type: o
226                    .get("type")
227                    .and_then(|v| v.as_str())
228                    .unwrap_or("")
229                    .to_string(),
230                required: o.get("required").and_then(|v| v.as_bool()).unwrap_or(false),
231                default: o.get("default").filter(|v| !v.is_null()).cloned(),
232                read: str_list(o.get("read")),
233                write: str_list(o.get("write")),
234                fields: o.get("fields").filter(|v| is_truthy(v)).cloned(),
235                strategy: match o.get("strategy").and_then(|v| v.as_str()) {
236                    None => None,
237                    Some(v @ "autoincrement") => Some(v.to_string()),
238                    // no-error-masking:未知策略显式拒绝,禁止静默吞掉后按默认行为跑
239                    Some(other) => {
240                        return Err(format!(
241                            "字段 {key} 的 strategy \"{other}\" 非法(仅支持 \"autoincrement\",且仅限 _id 字段)"
242                        ))
243                    }
244                },
245            }
246        } else {
247            // 非字符串、非对象的定义(如 `fields: { price: 123 }`)= 脏 schema,
248            // fail-fast 报错而非静默产出空 field_type(后续 object/array 展平、
249            // 标量列判定都会走错且无信号)
250            return Err(format!("字段 {} 定义类型非法(须为类型字符串或对象)", key));
251        };
252        out.insert(key.clone(), fd);
253    }
254    Ok(out)
255}
256
257/// 便捷构造:`Map` from `[(&str, Value)]`
258pub fn map_of(pairs: Vec<(&str, Value)>) -> Map<String, Value> {
259    let mut m = Map::new();
260    for (k, v) in pairs {
261        m.insert(k.to_string(), v);
262    }
263    m
264}