use serde_json::{json, Map, Value};
use crate::local::value::{apply_projection, MAX_LOCAL_COLLECTION_DOCS};
use crate::local::{filter, pipeline, update};
pub fn eval_command(collections: &Value, command: &Value) -> Result<Value, String> {
let cmd = command
.as_object()
.ok_or_else(|| format!("command 必须是对象,收到 {}", type_name(command)))?;
let kind = cmd
.get("kind")
.and_then(Value::as_str)
.ok_or_else(|| "command 缺少 kind".to_string())?;
let name = cmd
.get("collection")
.and_then(Value::as_str)
.ok_or_else(|| "command 缺少 collection".to_string())?;
let mut map = collections.as_object().cloned().ok_or_else(|| {
format!(
"collections 必须是对象 {{集合名: [文档…]}},收到 {}",
type_name(collections)
)
})?;
match kind {
"find" => {
let rows = rows_of(&map, name)?;
let condition = cmd.get("filter").cloned().unwrap_or(Value::Null);
let projection = cmd.get("projection").cloned().unwrap_or(Value::Null);
let mut out = Vec::new();
for row in &rows {
if filter::matches(row, &condition)? {
out.push(project(row, &projection)?);
}
}
return Ok(envelope(Value::Array(out), Vec::new(), collections));
}
"aggregate" => {
let stages = cmd
.get("pipeline")
.and_then(Value::as_array)
.ok_or_else(|| "aggregate 需要 pipeline 数组".to_string())?;
let out = pipeline::run(collections, name, stages)?;
return Ok(envelope(Value::Array(out), Vec::new(), collections));
}
"countDocuments" => {
let rows = rows_of(&map, name)?;
let condition = cmd.get("filter").cloned().unwrap_or(Value::Null);
let mut n = 0_usize;
for row in &rows {
if filter::matches(row, &condition)? {
n += 1;
}
}
return Ok(envelope(json!(n), Vec::new(), collections));
}
"findOne" => {
let rows = rows_of(&map, name)?;
let condition = cmd.get("filter").cloned().unwrap_or(Value::Null);
let projection = cmd.get("projection").cloned().unwrap_or(Value::Null);
let mut found = Value::Null;
for row in &rows {
if filter::matches(row, &condition)? {
found = project(row, &projection)?;
break;
}
}
return Ok(envelope(found, Vec::new(), collections));
}
_ => {}
}
let mut rows = rows_of(&map, name)?;
let result = match kind {
"insertOne" => {
let doc = cmd
.get("doc")
.ok_or_else(|| "insertOne 缺少 doc".to_string())?;
update::insert_one(&mut rows, doc)?
}
"insertMany" => {
let docs = cmd
.get("docs")
.and_then(Value::as_array)
.ok_or_else(|| "insertMany 需要 docs 数组".to_string())?;
let upsert_by_id = cmd
.get("upsertById")
.and_then(Value::as_bool)
.unwrap_or(false);
update::insert_many(&mut rows, docs, upsert_by_id)?
}
"updateMany" => {
let condition = cmd.get("filter").cloned().unwrap_or(Value::Null);
let spec = cmd
.get("update")
.ok_or_else(|| "updateMany 缺少 update".to_string())?;
update::update_many(&mut rows, &condition, spec)?
}
"findOneAndUpdate" => {
let condition = cmd.get("filter").cloned().unwrap_or(Value::Null);
let spec = cmd
.get("update")
.ok_or_else(|| "findOneAndUpdate 缺少 update".to_string())?;
let options = cmd.get("options").cloned().unwrap_or(Value::Null);
update::find_one_and_update(&mut rows, &condition, spec, &options)?
}
"deleteMany" => {
let condition = cmd.get("filter").cloned().unwrap_or(Value::Null);
update::delete_many(&mut rows, &condition)?
}
other => {
return Err(format!(
"未支持的命令: {other}(本地求值器拒绝静默;支持 find/aggregate/countDocuments/findOne/insertOne/insertMany/updateMany/findOneAndUpdate/deleteMany)"
));
}
};
guard_docs(name, &rows)?;
map.insert(name.to_string(), Value::Array(rows));
Ok(envelope(
result,
vec![name.to_string()],
&Value::Object(map),
))
}
fn envelope(result: Value, changed: Vec<String>, collections: &Value) -> Value {
json!({
"result": result,
"changed": changed,
"collections": collections,
})
}
fn rows_of(map: &Map<String, Value>, name: &str) -> Result<Vec<Value>, String> {
match map.get(name) {
None => Ok(Vec::new()),
Some(Value::Array(a)) => Ok(a.clone()),
Some(other) => Err(format!(
"集合 \"{name}\" 必须是文档数组,收到 {}",
type_name(other)
)),
}
}
fn project(doc: &Value, projection: &Value) -> Result<Value, String> {
if projection.is_null() {
return Ok(doc.clone());
}
apply_projection(doc, projection)
}
fn guard_docs(name: &str, rows: &[Value]) -> Result<(), String> {
if rows.len() > MAX_LOCAL_COLLECTION_DOCS {
return Err(format!(
"集合 \"{name}\" 文档数 {} 超过护栏 MAX_LOCAL_COLLECTION_DOCS={}(拒绝静默截断)",
rows.len(),
MAX_LOCAL_COLLECTION_DOCS
));
}
Ok(())
}
fn type_name(v: &Value) -> &'static str {
match v {
Value::Null => "null",
Value::Bool(_) => "bool",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn read_commands_have_empty_changed_and_passthrough() {
let collections = json!({ "users": [{"_id": "u1", "name": "Ada", "age": 36}] });
let out = eval_command(
&collections,
&json!({ "kind": "find", "collection": "users", "filter": { "age": { "$gt": 30 } } }),
)
.unwrap();
assert_eq!(
out["result"],
json!([{"_id": "u1", "name": "Ada", "age": 36}])
);
assert_eq!(out["changed"], json!([]));
assert_eq!(out["collections"], collections);
let c = eval_command(
&collections,
&json!({ "kind": "countDocuments", "collection": "users", "filter": null }),
)
.unwrap();
assert_eq!(c["result"], json!(1));
assert_eq!(c["changed"], json!([]));
let one = eval_command(
&collections,
&json!({ "kind": "findOne", "collection": "users", "filter": { "_id": "nope" } }),
)
.unwrap();
assert_eq!(one["result"], Value::Null);
}
#[test]
fn find_projection_applied() {
let collections = json!({ "users": [{"_id": "u1", "name": "Ada", "age": 36}] });
let out = eval_command(
&collections,
&json!({
"kind": "find",
"collection": "users",
"filter": null,
"projection": { "name": 1, "_id": 0 }
}),
)
.unwrap();
assert_eq!(out["result"], json!([{"name": "Ada"}]));
}
#[test]
fn insert_then_query_roundtrip() {
let empty = json!({});
let ins = eval_command(
&empty,
&json!({ "kind": "insertOne", "collection": "users", "doc": { "_id": "u1", "name": "Ada" } }),
)
.unwrap();
assert_eq!(ins["result"], json!({ "_id": "u1", "name": "Ada" }));
assert_eq!(ins["changed"], json!(["users"]));
let snapshot = ins["collections"].clone();
let q = eval_command(
&snapshot,
&json!({ "kind": "find", "collection": "users", "filter": { "_id": "u1" } }),
)
.unwrap();
assert_eq!(q["result"], json!([{ "_id": "u1", "name": "Ada" }]));
}
#[test]
fn aggregate_relation_golden() {
let collections = json!({
"probeUsers": [
{"_id": "pu1", "name": "Ada"},
{"_id": "pu2", "name": "Bob"}
],
"probePosts": [
{"_id": "pp1", "title": "P1", "userId": "pu1"},
{"_id": "pp2", "title": "P2", "userId": "pu2"}
]
});
let command = json!({
"kind": "aggregate",
"collection": "probePosts",
"pipeline": [
{"$match": {}},
{"$lookup": {
"as": "author",
"from": "probeUsers",
"let": {"rel_userId": {"$ifNull": ["$userId", null]}},
"pipeline": [
{"$match": {"$expr": {"$eq": ["$_id", "$$rel_userId"]}}},
{"$project": {"_id": 1, "name": 1}}
]
}},
{"$unwind": {"path": "$author", "preserveNullAndEmptyArrays": true}},
{"$project": {"_id": 1, "author": 1, "title": 1}}
]
});
let out = eval_command(&collections, &command).unwrap();
assert_eq!(
out["result"],
json!([
{"_id": "pp1", "author": {"_id": "pu1", "name": "Ada"}, "title": "P1"},
{"_id": "pp2", "author": {"_id": "pu2", "name": "Bob"}, "title": "P2"}
])
);
assert_eq!(out["changed"], json!([]));
assert_eq!(out["collections"], collections);
}
#[test]
fn write_commands_envelope_and_counts() {
let mut snapshot = json!({ "posts": [
{"_id": "p1", "title": "P1", "userId": "u1"},
{"_id": "p2", "title": "P2", "userId": "u2"}
]});
let ins = eval_command(
&snapshot,
&json!({ "kind": "insertMany", "collection": "posts", "docs": [{ "_id": "p3" }] }),
)
.unwrap();
assert_eq!(ins["result"], json!({ "insertedCount": 1 }));
assert_eq!(ins["changed"], json!(["posts"]));
snapshot = ins["collections"].clone();
let upd = eval_command(
&snapshot,
&json!({
"kind": "updateMany",
"collection": "posts",
"filter": { "userId": "u1" },
"update": { "$set": { "seen": true } }
}),
)
.unwrap();
assert_eq!(upd["result"], json!({ "modifiedCount": 1 }));
snapshot = upd["collections"].clone();
let del = eval_command(
&snapshot,
&json!({ "kind": "deleteMany", "collection": "posts", "filter": { "_id": "p3" } }),
)
.unwrap();
assert_eq!(del["result"], json!({ "deletedCount": 1 }));
}
#[test]
fn archive_upsert_by_id_through_eval() {
let snapshot = json!({ "usersDeleted": [] });
let out = eval_command(
&snapshot,
&json!({
"kind": "insertMany",
"collection": "usersDeleted",
"upsertById": true,
"docs": [{ "_id": "u1", "deletedAt": 100 }]
}),
)
.unwrap();
assert_eq!(out["result"], json!({ "insertedCount": 1 }));
assert_eq!(
out["collections"],
json!({ "usersDeleted": [{"_id": "u1", "deletedAt": 100}] })
);
}
#[test]
fn unknown_command_and_collection_type_error() {
assert!(eval_command(&json!({}), &json!({ "kind": "drop", "collection": "t" })).is_err());
assert!(eval_command(&json!({}), &json!({ "collection": "t" })).is_err());
assert!(eval_command(&json!({}), &json!({ "kind": "find" })).is_err());
assert!(eval_command(
&json!({ "t": 1 }),
&json!({ "kind": "find", "collection": "t" })
)
.is_err());
assert!(eval_command(&json!(5), &json!({ "kind": "find", "collection": "t" })).is_err());
}
#[test]
fn guard_docs_over_limit_errors() {
let docs: Vec<Value> = (0..MAX_LOCAL_COLLECTION_DOCS)
.map(|i| json!({ "_id": i }))
.collect();
let snapshot = json!({ "big": docs });
let command = json!({ "kind": "insertOne", "collection": "big", "doc": { "_id": "x" } });
let err = eval_command(&snapshot, &command).unwrap_err();
assert!(err.contains("MAX_LOCAL_COLLECTION_DOCS"), "err = {err}");
}
}