Skip to main content

rust_store_core/command/
cmd.rs

1//! 命令构造 + 数值工具
2//!
3//! 所有命令构造器收 [`Schema`](而非裸 collection 名):`source` / `namespace` /
4//! `collection` 定位三元组由 schema 一次性填入,从类型上消除「漏填定位」的可能
5//! (见 `multi-datasource-routing-plan.md`)。命令的定位字段是 Host 路由的唯一依据。
6
7use serde_json::{json, Map, Number, Value};
8
9use crate::schema::Schema;
10
11/// 命令体的定位字段(所有 `cmd_*` 共用)
12fn with_loc(mut m: Map<String, Value>, schema: &Schema) -> Map<String, Value> {
13    m.insert("source".to_string(), json!(schema.source()));
14    m.insert(
15        "namespace".to_string(),
16        schema.ns().map(|s| json!(s)).unwrap_or(Value::Null),
17    );
18    m
19}
20
21pub fn cmd_find(schema: &Schema, filter: &Value, projection: Option<&Value>) -> Value {
22    with_loc(
23        json!({
24            "kind": "find",
25            "collection": schema.collection,
26            "filter": filter,
27            "projection": projection.cloned().unwrap_or(Value::Null),
28        })
29        .as_object()
30        .cloned()
31        .unwrap_or_default(),
32        schema,
33    )
34    .into()
35}
36
37pub fn cmd_aggregate(schema: &Schema, pipeline: &[Value]) -> Value {
38    with_loc(
39        json!({
40            "kind": "aggregate",
41            "collection": schema.collection,
42            "pipeline": pipeline,
43        })
44        .as_object()
45        .cloned()
46        .unwrap_or_default(),
47        schema,
48    )
49    .into()
50}
51
52pub fn cmd_count_documents(schema: &Schema, filter: &Value) -> Value {
53    with_loc(
54        json!({
55            "kind": "countDocuments",
56            "collection": schema.collection,
57            "filter": filter,
58        })
59        .as_object()
60        .cloned()
61        .unwrap_or_default(),
62        schema,
63    )
64    .into()
65}
66
67pub fn cmd_find_one(schema: &Schema, filter: &Value, projection: Option<&Value>) -> Value {
68    with_loc(
69        json!({
70            "kind": "findOne",
71            "collection": schema.collection,
72            "filter": filter,
73            "projection": projection.cloned().unwrap_or(Value::Null),
74        })
75        .as_object()
76        .cloned()
77        .unwrap_or_default(),
78        schema,
79    )
80    .into()
81}
82
83pub fn cmd_insert_one(schema: &Schema, doc: &Value) -> Value {
84    with_loc(
85        json!({
86            "kind": "insertOne",
87            "collection": schema.collection,
88            "doc": doc,
89        })
90        .as_object()
91        .cloned()
92        .unwrap_or_default(),
93        schema,
94    )
95    .into()
96}
97
98pub fn cmd_insert_many(schema: &Schema, docs: &[Value]) -> Value {
99    with_loc(
100        json!({
101            "kind": "insertMany",
102            "collection": schema.collection,
103            "docs": docs,
104        })
105        .as_object()
106        .cloned()
107        .unwrap_or_default(),
108        schema,
109    )
110    .into()
111}
112
113pub fn cmd_find_one_and_update(
114    schema: &Schema,
115    filter: &Value,
116    update: &Value,
117    options: &Value,
118) -> Value {
119    with_loc(
120        json!({
121            "kind": "findOneAndUpdate",
122            "collection": schema.collection,
123            "filter": filter,
124            "update": update,
125            "options": options,
126        })
127        .as_object()
128        .cloned()
129        .unwrap_or_default(),
130        schema,
131    )
132    .into()
133}
134
135pub fn cmd_update_many(schema: &Schema, filter: &Value, update: &Value) -> Value {
136    with_loc(
137        json!({
138            "kind": "updateMany",
139            "collection": schema.collection,
140            "filter": filter,
141            "update": update,
142        })
143        .as_object()
144        .cloned()
145        .unwrap_or_default(),
146        schema,
147    )
148    .into()
149}
150
151pub fn cmd_delete_many(schema: &Schema, filter: &Value) -> Value {
152    with_loc(
153        json!({
154            "kind": "deleteMany",
155            "collection": schema.collection,
156            "filter": filter,
157        })
158        .as_object()
159        .cloned()
160        .unwrap_or_default(),
161        schema,
162    )
163    .into()
164}
165
166// ─── 多租户路由 override(方案 §6) ──────────────────────────
167
168/// 把计划内**所有命令体**的定位字段替换为 `routeOverride` 声明。
169///
170/// - 只作用于命令体(带 `kind` + `collection` 的对象):计划里的 `sources[]` /
171///   `edges` / `postprocess` 等元数据不受影响;
172/// - `override.source` / `override.namespace` **键出现才替换**(`namespace` 可显式
173///   置 null = 连接默认);缺省回落 schema 原声明;
174/// - 权限 / 计算列 / 字段校验仍按结构 schema(与定位正交,语义不变);
175/// - source 存在性由执行层 `getConnection` 兜底,namespace/表不存在由驱动报错
176///   (core 无 IO,不做存在性预检,铁律 1)。
177pub fn apply_route_override(plan: &mut Value, route_override: &Value) {
178    let ov = match route_override.as_object() {
179        Some(o) if !o.is_empty() => o,
180        _ => return,
181    };
182    let src = ov
183        .get("source")
184        .and_then(|v| v.as_str())
185        .filter(|s| !s.is_empty());
186    let ns_present = ov.contains_key("namespace");
187    let ns = ov.get("namespace");
188    override_walk(plan, src, ns_present, ns);
189}
190
191fn override_walk(v: &mut Value, src: Option<&str>, ns_present: bool, ns: Option<&Value>) {
192    match v {
193        Value::Object(map) => {
194            let is_cmd = map.contains_key("kind") && map.contains_key("collection");
195            if is_cmd {
196                if let Some(s) = src {
197                    map.insert("source".to_string(), json!(s));
198                }
199                if ns_present {
200                    map.insert("namespace".to_string(), ns.cloned().unwrap_or(Value::Null));
201                }
202            }
203            for (_, child) in map.iter_mut() {
204                override_walk(child, src, ns_present, ns);
205            }
206        }
207        Value::Array(items) => {
208            for it in items.iter_mut() {
209                override_walk(it, src, ns_present, ns);
210            }
211        }
212        _ => {}
213    }
214}
215
216// ─── 数值工具(对齐 JS `_toNumber` / Number()) ──────────────
217
218/// 对齐 JS `Number()` + `Math.trunc`:无法转换时返回 0
219pub(super) fn to_number(v: Option<&Value>) -> f64 {
220    match v {
221        Some(Value::Number(n)) => {
222            let f = n.as_f64().unwrap_or(0.0);
223            if f.is_nan() {
224                0.0
225            } else {
226                f.trunc()
227            }
228        }
229        Some(Value::Bool(b)) => {
230            if *b {
231                1.0
232            } else {
233                0.0
234            }
235        }
236        Some(Value::String(s)) => {
237            let t = s.trim();
238            if t.is_empty() {
239                return 0.0;
240            }
241            if is_int_str(t) {
242                t.parse::<i64>().map(|i| i as f64).unwrap_or(0.0)
243            } else {
244                match t.parse::<f64>() {
245                    Ok(f) if !f.is_nan() => f,
246                    _ => 0.0,
247                }
248            }
249        }
250        _ => 0.0,
251    }
252}
253
254fn is_int_str(s: &str) -> bool {
255    let body = s.strip_prefix(['+', '-']).unwrap_or(s);
256    !body.is_empty() && body.bytes().all(|b| b.is_ascii_digit())
257}
258
259/// 整数形式的 f64 → JSON 整数,保证与 JS `JSON.stringify` 逐字节一致
260pub(super) fn num_value(f: f64) -> Value {
261    if !f.is_finite() {
262        return json!(0);
263    }
264    if f.fract() == 0.0 && f.abs() <= 9_007_199_254_740_992.0 {
265        json!(f as i64)
266    } else {
267        Value::Number(Number::from_f64(f).unwrap_or_else(|| Number::from(0)))
268    }
269}