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