1use serde_json::{json, Map, Value};
15
16use crate::local::value::{apply_projection, MAX_LOCAL_COLLECTION_DOCS};
17use crate::local::{filter, pipeline, update};
18
19pub fn eval_command(collections: &Value, command: &Value) -> Result<Value, String> {
25 let cmd = command
26 .as_object()
27 .ok_or_else(|| format!("command 必须是对象,收到 {}", type_name(command)))?;
28 let kind = cmd
29 .get("kind")
30 .and_then(Value::as_str)
31 .ok_or_else(|| "command 缺少 kind".to_string())?;
32 let name = cmd
33 .get("collection")
34 .and_then(Value::as_str)
35 .ok_or_else(|| "command 缺少 collection".to_string())?;
36 let mut map = collections
37 .as_object()
38 .cloned()
39 .ok_or_else(|| format!("collections 必须是对象 {{集合名: [文档…]}},收到 {}", type_name(collections)))?;
40
41 match kind {
43 "find" => {
44 let rows = rows_of(&map, name)?;
45 let condition = cmd.get("filter").cloned().unwrap_or(Value::Null);
46 let projection = cmd.get("projection").cloned().unwrap_or(Value::Null);
47 let mut out = Vec::new();
48 for row in &rows {
49 if filter::matches(row, &condition)? {
50 out.push(project(row, &projection)?);
51 }
52 }
53 return Ok(envelope(Value::Array(out), Vec::new(), collections));
54 }
55 "aggregate" => {
56 let stages = cmd
57 .get("pipeline")
58 .and_then(Value::as_array)
59 .ok_or_else(|| "aggregate 需要 pipeline 数组".to_string())?;
60 let out = pipeline::run(collections, name, stages)?;
61 return Ok(envelope(Value::Array(out), Vec::new(), collections));
62 }
63 "countDocuments" => {
64 let rows = rows_of(&map, name)?;
65 let condition = cmd.get("filter").cloned().unwrap_or(Value::Null);
66 let mut n = 0_usize;
67 for row in &rows {
68 if filter::matches(row, &condition)? {
69 n += 1;
70 }
71 }
72 return Ok(envelope(json!(n), Vec::new(), collections));
73 }
74 "findOne" => {
75 let rows = rows_of(&map, name)?;
76 let condition = cmd.get("filter").cloned().unwrap_or(Value::Null);
77 let projection = cmd.get("projection").cloned().unwrap_or(Value::Null);
78 let mut found = Value::Null;
79 for row in &rows {
80 if filter::matches(row, &condition)? {
81 found = project(row, &projection)?;
82 break;
83 }
84 }
85 return Ok(envelope(found, Vec::new(), collections));
86 }
87 _ => {}
88 }
89
90 let mut rows = rows_of(&map, name)?;
92 let result = match kind {
93 "insertOne" => {
94 let doc = cmd
95 .get("doc")
96 .ok_or_else(|| "insertOne 缺少 doc".to_string())?;
97 update::insert_one(&mut rows, doc)?
98 }
99 "insertMany" => {
100 let docs = cmd
101 .get("docs")
102 .and_then(Value::as_array)
103 .ok_or_else(|| "insertMany 需要 docs 数组".to_string())?;
104 let upsert_by_id = cmd
105 .get("upsertById")
106 .and_then(Value::as_bool)
107 .unwrap_or(false);
108 update::insert_many(&mut rows, docs, upsert_by_id)?
109 }
110 "updateMany" => {
111 let condition = cmd.get("filter").cloned().unwrap_or(Value::Null);
112 let spec = cmd
113 .get("update")
114 .ok_or_else(|| "updateMany 缺少 update".to_string())?;
115 update::update_many(&mut rows, &condition, spec)?
116 }
117 "findOneAndUpdate" => {
118 let condition = cmd.get("filter").cloned().unwrap_or(Value::Null);
119 let spec = cmd
120 .get("update")
121 .ok_or_else(|| "findOneAndUpdate 缺少 update".to_string())?;
122 let options = cmd.get("options").cloned().unwrap_or(Value::Null);
123 update::find_one_and_update(&mut rows, &condition, spec, &options)?
124 }
125 "deleteMany" => {
126 let condition = cmd.get("filter").cloned().unwrap_or(Value::Null);
127 update::delete_many(&mut rows, &condition)?
128 }
129 other => {
130 return Err(format!(
131 "未支持的命令: {other}(本地求值器拒绝静默;支持 find/aggregate/countDocuments/findOne/insertOne/insertMany/updateMany/findOneAndUpdate/deleteMany)"
132 ));
133 }
134 };
135
136 guard_docs(name, &rows)?;
137 map.insert(name.to_string(), Value::Array(rows));
138 Ok(envelope(result, vec![name.to_string()], &Value::Object(map)))
139}
140
141fn envelope(result: Value, changed: Vec<String>, collections: &Value) -> Value {
143 json!({
144 "result": result,
145 "changed": changed,
146 "collections": collections,
147 })
148}
149
150fn rows_of(map: &Map<String, Value>, name: &str) -> Result<Vec<Value>, String> {
152 match map.get(name) {
153 None => Ok(Vec::new()),
154 Some(Value::Array(a)) => Ok(a.clone()),
155 Some(other) => Err(format!(
156 "集合 \"{name}\" 必须是文档数组,收到 {}",
157 type_name(other)
158 )),
159 }
160}
161
162fn project(doc: &Value, projection: &Value) -> Result<Value, String> {
164 if projection.is_null() {
165 return Ok(doc.clone());
166 }
167 apply_projection(doc, projection)
168}
169
170fn guard_docs(name: &str, rows: &[Value]) -> Result<(), String> {
172 if rows.len() > MAX_LOCAL_COLLECTION_DOCS {
173 return Err(format!(
174 "集合 \"{name}\" 文档数 {} 超过护栏 MAX_LOCAL_COLLECTION_DOCS={}(拒绝静默截断)",
175 rows.len(),
176 MAX_LOCAL_COLLECTION_DOCS
177 ));
178 }
179 Ok(())
180}
181
182fn type_name(v: &Value) -> &'static str {
183 match v {
184 Value::Null => "null",
185 Value::Bool(_) => "bool",
186 Value::Number(_) => "number",
187 Value::String(_) => "string",
188 Value::Array(_) => "array",
189 Value::Object(_) => "object",
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use serde_json::json;
197
198 #[test]
199 fn read_commands_have_empty_changed_and_passthrough() {
200 let collections = json!({ "users": [{"_id": "u1", "name": "Ada", "age": 36}] });
201 let out = eval_command(
203 &collections,
204 &json!({ "kind": "find", "collection": "users", "filter": { "age": { "$gt": 30 } } }),
205 )
206 .unwrap();
207 assert_eq!(out["result"], json!([{"_id": "u1", "name": "Ada", "age": 36}]));
208 assert_eq!(out["changed"], json!([]));
209 assert_eq!(out["collections"], collections);
210 let c = eval_command(
212 &collections,
213 &json!({ "kind": "countDocuments", "collection": "users", "filter": null }),
214 )
215 .unwrap();
216 assert_eq!(c["result"], json!(1));
217 assert_eq!(c["changed"], json!([]));
218 let one = eval_command(
220 &collections,
221 &json!({ "kind": "findOne", "collection": "users", "filter": { "_id": "nope" } }),
222 )
223 .unwrap();
224 assert_eq!(one["result"], Value::Null);
225 }
226
227 #[test]
228 fn find_projection_applied() {
229 let collections = json!({ "users": [{"_id": "u1", "name": "Ada", "age": 36}] });
230 let out = eval_command(
231 &collections,
232 &json!({
233 "kind": "find",
234 "collection": "users",
235 "filter": null,
236 "projection": { "name": 1, "_id": 0 }
237 }),
238 )
239 .unwrap();
240 assert_eq!(out["result"], json!([{"name": "Ada"}]));
241 }
242
243 #[test]
244 fn insert_then_query_roundtrip() {
245 let empty = json!({});
246 let ins = eval_command(
247 &empty,
248 &json!({ "kind": "insertOne", "collection": "users", "doc": { "_id": "u1", "name": "Ada" } }),
249 )
250 .unwrap();
251 assert_eq!(ins["result"], json!({ "_id": "u1", "name": "Ada" }));
252 assert_eq!(ins["changed"], json!(["users"]));
253 let snapshot = ins["collections"].clone();
254 let q = eval_command(
256 &snapshot,
257 &json!({ "kind": "find", "collection": "users", "filter": { "_id": "u1" } }),
258 )
259 .unwrap();
260 assert_eq!(q["result"], json!([{ "_id": "u1", "name": "Ada" }]));
261 }
262
263 #[test]
265 fn aggregate_relation_golden() {
266 let collections = json!({
267 "probeUsers": [
268 {"_id": "pu1", "name": "Ada"},
269 {"_id": "pu2", "name": "Bob"}
270 ],
271 "probePosts": [
272 {"_id": "pp1", "title": "P1", "userId": "pu1"},
273 {"_id": "pp2", "title": "P2", "userId": "pu2"}
274 ]
275 });
276 let command = json!({
277 "kind": "aggregate",
278 "collection": "probePosts",
279 "pipeline": [
280 {"$match": {}},
281 {"$lookup": {
282 "as": "author",
283 "from": "probeUsers",
284 "let": {"rel_userId": {"$ifNull": ["$userId", null]}},
285 "pipeline": [
286 {"$match": {"$expr": {"$eq": ["$_id", "$$rel_userId"]}}},
287 {"$project": {"_id": 1, "name": 1}}
288 ]
289 }},
290 {"$unwind": {"path": "$author", "preserveNullAndEmptyArrays": true}},
291 {"$project": {"_id": 1, "author": 1, "title": 1}}
292 ]
293 });
294 let out = eval_command(&collections, &command).unwrap();
295 assert_eq!(
296 out["result"],
297 json!([
298 {"_id": "pp1", "author": {"_id": "pu1", "name": "Ada"}, "title": "P1"},
299 {"_id": "pp2", "author": {"_id": "pu2", "name": "Bob"}, "title": "P2"}
300 ])
301 );
302 assert_eq!(out["changed"], json!([]));
303 assert_eq!(out["collections"], collections);
304 }
305
306 #[test]
307 fn write_commands_envelope_and_counts() {
308 let mut snapshot = json!({ "posts": [
309 {"_id": "p1", "title": "P1", "userId": "u1"},
310 {"_id": "p2", "title": "P2", "userId": "u2"}
311 ]});
312 let ins = eval_command(
314 &snapshot,
315 &json!({ "kind": "insertMany", "collection": "posts", "docs": [{ "_id": "p3" }] }),
316 )
317 .unwrap();
318 assert_eq!(ins["result"], json!({ "insertedCount": 1 }));
319 assert_eq!(ins["changed"], json!(["posts"]));
320 snapshot = ins["collections"].clone();
321 let upd = eval_command(
323 &snapshot,
324 &json!({
325 "kind": "updateMany",
326 "collection": "posts",
327 "filter": { "userId": "u1" },
328 "update": { "$set": { "seen": true } }
329 }),
330 )
331 .unwrap();
332 assert_eq!(upd["result"], json!({ "modifiedCount": 1 }));
333 snapshot = upd["collections"].clone();
334 let del = eval_command(
336 &snapshot,
337 &json!({ "kind": "deleteMany", "collection": "posts", "filter": { "_id": "p3" } }),
338 )
339 .unwrap();
340 assert_eq!(del["result"], json!({ "deletedCount": 1 }));
341 }
342
343 #[test]
344 fn archive_upsert_by_id_through_eval() {
345 let snapshot = json!({ "usersDeleted": [] });
346 let out = eval_command(
347 &snapshot,
348 &json!({
349 "kind": "insertMany",
350 "collection": "usersDeleted",
351 "upsertById": true,
352 "docs": [{ "_id": "u1", "deletedAt": 100 }]
353 }),
354 )
355 .unwrap();
356 assert_eq!(out["result"], json!({ "insertedCount": 1 }));
357 assert_eq!(
358 out["collections"],
359 json!({ "usersDeleted": [{"_id": "u1", "deletedAt": 100}] })
360 );
361 }
362
363 #[test]
364 fn unknown_command_and_collection_type_error() {
365 assert!(eval_command(&json!({}), &json!({ "kind": "drop", "collection": "t" })).is_err());
367 assert!(eval_command(&json!({}), &json!({ "collection": "t" })).is_err());
369 assert!(eval_command(&json!({}), &json!({ "kind": "find" })).is_err());
370 assert!(eval_command(&json!({ "t": 1 }), &json!({ "kind": "find", "collection": "t" })).is_err());
372 assert!(eval_command(&json!(5), &json!({ "kind": "find", "collection": "t" })).is_err());
374 }
375
376 #[test]
377 fn guard_docs_over_limit_errors() {
378 let docs: Vec<Value> = (0..MAX_LOCAL_COLLECTION_DOCS)
380 .map(|i| json!({ "_id": i }))
381 .collect();
382 let snapshot = json!({ "big": docs });
383 let command = json!({ "kind": "insertOne", "collection": "big", "doc": { "_id": "x" } });
384 let err = eval_command(&snapshot, &command).unwrap_err();
385 assert!(err.contains("MAX_LOCAL_COLLECTION_DOCS"), "err = {err}");
386 }
387}