Skip to main content

rust_store_core/command/
cmd.rs

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