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    /// 向量维度:`field_type == "vector"` 时必填,正整数(VR-01)
112    pub dim: Option<u32>,
113    /// 向量距离度量:cosine / l2 / ip,缺省 cosine(仅 vector 字段允许出现)
114    pub metric: Option<String>,
115}
116
117impl FieldDef {
118    /// 向量距离度量(缺省 cosine);非 vector 字段调用无意义
119    pub fn metric_or_default(&self) -> &str {
120        self.metric.as_deref().unwrap_or("cosine")
121    }
122}
123
124/// 向量维度统一上限:与 pgvector `vector` 类型上限一致,保证同一 schema 可跨源迁移。
125/// 数值源唯一:05 的 `capability::APPLICATION_MAX_DIM` 引用本常量,禁止出现第二个 16000 字面量。
126pub const MAX_VECTOR_DIM: u32 = 16000;
127
128/// 注册期向量字段校验(VR-01 / A1):vector 必带合法 dim;非 vector 不接受 dim / metric。
129pub(super) fn validate_vector_fields(fields: &HashMap<String, FieldDef>) -> Result<(), String> {
130    for (name, f) in fields {
131        let is_vector = f.field_type == "vector";
132        if is_vector {
133            let dim = match f.dim {
134                Some(d) => d,
135                None => {
136                    return Err(format!(
137                        "字段 {name} 类型为 vector,但缺少必填 dim(正整数)"
138                    ))
139                }
140            };
141            if dim > MAX_VECTOR_DIM {
142                return Err(format!(
143                    "字段 {name} 的 dim 超过上限:{dim} > {MAX_VECTOR_DIM}"
144                ));
145            }
146        } else {
147            if f.dim.is_some() {
148                return Err(format!(
149                    "字段 {name} 类型为 {},不接受 dim(仅 vector 允许)",
150                    f.field_type
151                ));
152            }
153            if f.metric.is_some() {
154                return Err(format!(
155                    "字段 {name} 类型为 {},不接受 metric(仅 vector 允许)",
156                    f.field_type
157                ));
158            }
159        }
160    }
161    Ok(())
162}
163
164#[derive(Debug, Clone)]
165pub struct RelationDef {
166    pub model: String,
167    pub rel_type: String,
168    pub local_field: String,
169    pub foreign_field: String,
170    pub read: Option<Vec<String>>,
171}
172
173#[derive(Debug, Clone)]
174pub struct ComputeDef {
175    pub comp_type: String,
176    pub has_fn: bool,
177    pub has_async_fn: bool,
178    /// 回调标识:Host 经 FnRegistry 把它绑定到实现;缺省 = 计算列 key 名
179    pub fn_ref: Option<String>,
180    /// 归一聚合算子(§9.2(2)):`{"$count": "lessons"}` / `{"$sum": "lessons.duration"}`。
181    /// 取代旧的 `lookup` + `addFields` Mongo 专用形态;SQL / Mongo 走同一语法(执行下推优先)。
182    pub agg: Option<Value>,
183    pub depends: Vec<String>,
184    pub read: Option<Vec<String>>,
185}
186
187#[derive(Debug, Clone)]
188pub struct Schema {
189    pub name: String,
190    pub collection: String,
191    pub id_prefix: String,
192    pub timestamps: bool,
193    pub fields: HashMap<String, FieldDef>,
194    pub relations: HashMap<String, RelationDef>,
195    /// 保持注册顺序:`build_agg_stages` 的输出顺序依赖它
196    pub computes: Vec<(String, ComputeDef)>,
197    /// 写触发器声明(事件 → 触发列表;解析见 `super::triggers`,规划见 `crate::command::triggers`)
198    pub triggers: super::triggers::Triggers,
199    pub read: Option<Vec<String>>,
200    pub write: Option<Vec<String>>,
201    /// 原始索引定义(`[{keys: {...}, options: {...}}]`),upsert 条件构建依赖 unique 索引
202    pub indexes: Vec<Value>,
203    /// 数据源名(由 `register_batch` 的 [`Location`] 注入;`None` = 缺省 `default`)。
204    /// 见 [`crate::datasource`]
205    pub source: Option<String>,
206    /// 连接内「库」(由 `register_batch` 注入;`None` = 连接默认:Mongo db 实例的库名、
207    /// PG 的库、MySQL 的连接库、SQLite 的 main)。
208    pub database: Option<String>,
209    /// 连接内「schema」(仅 PG;由 `register_batch` 注入;`None` = 连接默认 search_path)。
210    pub schema: Option<String>,
211}
212
213impl Schema {
214    pub fn compute(&self, name: &str) -> Option<&ComputeDef> {
215        self.computes
216            .iter()
217            .find(|(k, _)| k == name)
218            .map(|(_, c)| c)
219    }
220
221    /// 解析后的数据源名(缺省 `default`)
222    pub fn source(&self) -> &str {
223        self.source
224            .as_deref()
225            .unwrap_or(crate::datasource::DEFAULT_SOURCE)
226    }
227
228    /// 解析后的 database(空串归一为 `None`)
229    pub fn database(&self) -> Option<&str> {
230        self.database.as_deref().filter(|s| !s.is_empty())
231    }
232
233    /// 解析后的 schema(空串归一为 `None`;仅 PG)
234    pub fn schema(&self) -> Option<&str> {
235        self.schema.as_deref().filter(|s| !s.is_empty())
236    }
237
238    /// 主落点(由 `source`/`database`/`schema` 组装)。
239    ///
240    /// 等价于 `Registry::primary_location(name)` 的投影;链路缺失时的回落。
241    pub fn location(&self) -> Location {
242        Location {
243            source: self.source().to_string(),
244            database: self.database().map(String::from),
245            schema: self.schema().map(String::from),
246            role: None,
247            priority: None,
248        }
249    }
250
251    /// `_id` 是否声明 `strategy: "autoincrement"`(阶段2:数据库自增主键)
252    pub fn id_is_autoincrement(&self) -> bool {
253        self.fields.get("_id").and_then(|f| f.strategy.as_deref()) == Some("autoincrement")
254    }
255}
256
257/// 规范化 fields 定义(字符串简写 → `{type, required:false}`)
258pub(super) fn normalize_fields(v: Option<&Value>) -> Result<HashMap<String, FieldDef>, String> {
259    let mut out = HashMap::new();
260    let Some(Value::Object(map)) = v else {
261        return Ok(out);
262    };
263    for (key, val) in map {
264        let fd = if let Some(s) = val.as_str() {
265            FieldDef {
266                field_type: s.to_string(),
267                required: false,
268                default: None,
269                read: None,
270                write: None,
271                fields: None,
272                strategy: None,
273                dim: None,
274                metric: None,
275            }
276        } else if let Some(o) = val.as_object() {
277            // 向量属性必须显式提取:未提取的键会被静默丢弃(VR-01 / 取证 3.5)
278            let dim = match o.get("dim") {
279                None | Some(Value::Null) => None,
280                Some(Value::Number(n)) => Some(
281                    n.as_u64()
282                        .filter(|v| *v > 0 && *v <= u32::MAX as u64)
283                        .map(|v| v as u32)
284                        .ok_or_else(|| format!("字段 {key} 的 dim 必须是正整数,实际 {n}"))?,
285                ),
286                Some(other) => return Err(format!("字段 {key} 的 dim 必须是正整数,实际 {other}")),
287            };
288            let metric = match o.get("metric") {
289                None | Some(Value::Null) => None,
290                Some(Value::String(s)) if ["cosine", "l2", "ip"].contains(&s.as_str()) => {
291                    Some(s.clone())
292                }
293                Some(other) => {
294                    return Err(format!(
295                        "字段 {key} 的 metric 仅支持 cosine/l2/ip,实际 {other}"
296                    ))
297                }
298            };
299            FieldDef {
300                field_type: o
301                    .get("type")
302                    .and_then(|v| v.as_str())
303                    .unwrap_or("")
304                    .to_string(),
305                required: o.get("required").and_then(|v| v.as_bool()).unwrap_or(false),
306                default: o.get("default").filter(|v| !v.is_null()).cloned(),
307                read: str_list(o.get("read")),
308                write: str_list(o.get("write")),
309                fields: o.get("fields").filter(|v| is_truthy(v)).cloned(),
310                strategy: match o.get("strategy").and_then(|v| v.as_str()) {
311                    None => None,
312                    Some(v @ "autoincrement") => Some(v.to_string()),
313                    // no-error-masking:未知策略显式拒绝,禁止静默吞掉后按默认行为跑
314                    Some(other) => {
315                        return Err(format!(
316                            "字段 {key} 的 strategy \"{other}\" 非法(仅支持 \"autoincrement\",且仅限 _id 字段)"
317                        ))
318                    }
319                },
320                dim,
321                metric,
322            }
323        } else {
324            // 非字符串、非对象的定义(如 `fields: { price: 123 }`)= 脏 schema,
325            // fail-fast 报错而非静默产出空 field_type(后续 object/array 展平、
326            // 标量列判定都会走错且无信号)
327            return Err(format!("字段 {} 定义类型非法(须为类型字符串或对象)", key));
328        };
329        out.insert(key.clone(), fd);
330    }
331    Ok(out)
332}
333
334/// 便捷构造:`Map` from `[(&str, Value)]`
335pub fn map_of(pairs: Vec<(&str, Value)>) -> Map<String, Value> {
336    let mut m = Map::new();
337    for (k, v) in pairs {
338        m.insert(k.to_string(), v);
339    }
340    m
341}