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/// claim 命令:并发领取原语(三 SQL 后端方言路径;Mongo 执行器侧显式拒绝)。
171/// `sort` 为 Mongo 数对数组 `[["field", 1], ...]`;`nowStamp` 为 Host 注入的
172/// 当前时间戳(core 无时钟,仅供宿主在事务内透传给业务字段)。
173pub fn cmd_claim(
174    schema: &Schema,
175    filter: &Value,
176    sort: &Value,
177    limit: i64,
178    update: &Value,
179    now_stamp: i64,
180) -> Value {
181    with_loc(
182        json!({
183            "kind": "claim",
184            "collection": schema.collection,
185            "filter": filter,
186            "sort": sort,
187            "limit": limit,
188            "update": update,
189            "nowStamp": now_stamp,
190        })
191        .as_object()
192        .cloned()
193        .unwrap_or_default(),
194        schema,
195    )
196    .into()
197}
198
199/// compareAndSet 命令:比较并写原语(matched 语义由宿主从 rowCount / changes() 读出)
200pub fn cmd_compare_and_set(
201    schema: &Schema,
202    filter: &Value,
203    update: &Value,
204    now_stamp: i64,
205) -> Value {
206    with_loc(
207        json!({
208            "kind": "compareAndSet",
209            "collection": schema.collection,
210            "filter": filter,
211            "update": update,
212            "nowStamp": now_stamp,
213        })
214        .as_object()
215        .cloned()
216        .unwrap_or_default(),
217        schema,
218    )
219    .into()
220}
221
222// ─── 多租户路由 override(方案 §6) ──────────────────────────
223
224/// 把计划内**所有命令体**的定位字段替换为 `routeOverride` 声明。
225///
226/// - 只作用于命令体(带 `kind` + `collection` 的对象):计划里的 `sources[]` /
227///   `edges` / `postprocess` 等元数据不受影响;
228/// - `override.source` / `override.database` / `override.schema` **键出现才替换**
229///   (`database` / `schema` 可显式置 null = 连接默认);缺省回落 schema 原声明;
230/// - 权限 / 计算列 / 字段校验仍按结构 schema(与定位正交,语义不变);
231/// - source 存在性由执行层 `getConnection` 兜底,database/schema/表不存在由驱动报错
232///   (core 无 IO,不做存在性预检,铁律 1)。
233pub fn apply_route_override(plan: &mut Value, route_override: &Value) {
234    let ov = match route_override.as_object() {
235        Some(o) if !o.is_empty() => o,
236        _ => return,
237    };
238    let src = ov
239        .get("source")
240        .and_then(|v| v.as_str())
241        .filter(|s| !s.is_empty());
242    let db_present = ov.contains_key("database");
243    let db = ov.get("database");
244    let sc_present = ov.contains_key("schema");
245    let sc = ov.get("schema");
246    override_walk(plan, src, db_present, db, sc_present, sc);
247}
248
249fn override_walk(
250    v: &mut Value,
251    src: Option<&str>,
252    db_present: bool,
253    db: Option<&Value>,
254    sc_present: bool,
255    sc: Option<&Value>,
256) {
257    match v {
258        Value::Object(map) => {
259            let is_cmd = map.contains_key("kind") && map.contains_key("collection");
260            if is_cmd {
261                if let Some(s) = src {
262                    map.insert("source".to_string(), json!(s));
263                }
264                if db_present {
265                    map.insert("database".to_string(), db.cloned().unwrap_or(Value::Null));
266                }
267                if sc_present {
268                    map.insert("schema".to_string(), sc.cloned().unwrap_or(Value::Null));
269                }
270            }
271            for (_, child) in map.iter_mut() {
272                override_walk(child, src, db_present, db, sc_present, sc);
273            }
274        }
275        Value::Array(items) => {
276            for it in items.iter_mut() {
277                override_walk(it, src, db_present, db, sc_present, sc);
278            }
279        }
280        _ => {}
281    }
282}
283
284// ─── 数值工具(对齐 JS `_toNumber` / Number()) ──────────────
285
286/// 对齐 JS `Number()` + `Math.trunc`:无法转换时返回 0
287pub(super) fn to_number(v: Option<&Value>) -> f64 {
288    match v {
289        Some(Value::Number(n)) => {
290            let f = n.as_f64().unwrap_or(0.0);
291            if f.is_nan() {
292                0.0
293            } else {
294                f.trunc()
295            }
296        }
297        Some(Value::Bool(b)) => {
298            if *b {
299                1.0
300            } else {
301                0.0
302            }
303        }
304        Some(Value::String(s)) => {
305            let t = s.trim();
306            if t.is_empty() {
307                return 0.0;
308            }
309            if is_int_str(t) {
310                t.parse::<i64>().map(|i| i as f64).unwrap_or(0.0)
311            } else {
312                match t.parse::<f64>() {
313                    Ok(f) if !f.is_nan() => f,
314                    _ => 0.0,
315                }
316            }
317        }
318        _ => 0.0,
319    }
320}
321
322fn is_int_str(s: &str) -> bool {
323    let body = s.strip_prefix(['+', '-']).unwrap_or(s);
324    !body.is_empty() && body.bytes().all(|b| b.is_ascii_digit())
325}
326
327/// 整数形式的 f64 → JSON 整数,保证与 JS `JSON.stringify` 逐字节一致
328pub(super) fn num_value(f: f64) -> Value {
329    if !f.is_finite() {
330        return json!(0);
331    }
332    if f.fract() == 0.0 && f.abs() <= 9_007_199_254_740_992.0 {
333        json!(f as i64)
334    } else {
335        Value::Number(Number::from_f64(f).unwrap_or_else(|| Number::from(0)))
336    }
337}