use serde_json::json;
use rust_store_core::dialect::{restore_rows_json, translate, Backend};
use rust_store_core::schema::Registry;
fn reg() -> Registry {
let mut r = Registry::new();
r.register(&json!({
"name": "Course",
"collection": "courses",
"timestamps": false,
"fields": {
"title": { "type": "string" },
"tags": { "type": "array" },
"meta": { "type": "object", "fields": {
"level": { "type": "string" },
"seo": { "type": "object", "fields": { "title": { "type": "string" } } }
} }
},
"relations": {}
}))
.unwrap();
r
}
#[test]
fn json_type_name_per_backend() {
assert_eq!(Backend::Mysql.json_type_name(), "JSON");
assert_eq!(Backend::Postgres.json_type_name(), "jsonb");
assert_eq!(Backend::Sqlite.json_type_name(), "TEXT");
}
#[test]
fn json_extract_scalar_multi_segment() {
assert_eq!(
Backend::Mysql.json_extract_scalar("`meta`", &["a", "b"]),
"JSON_UNQUOTE(JSON_EXTRACT(`meta`, '$.a.b'))"
);
assert_eq!(
Backend::Postgres.json_extract_scalar("\"meta\"", &["a", "b"]),
"(\"meta\" #>> '{a,b}')"
);
assert_eq!(
Backend::Sqlite.json_extract_scalar("\"meta\"", &["a", "b"]),
"json_extract(\"meta\", '$.a.b')"
);
}
#[test]
fn json_extract_scalar_single_segment() {
assert_eq!(
Backend::Mysql.json_extract_scalar("`meta`", &["title"]),
"JSON_UNQUOTE(JSON_EXTRACT(`meta`, '$.title'))"
);
assert_eq!(
Backend::Postgres.json_extract_scalar("\"meta\"", &["title"]),
"(\"meta\" #>> '{title}')"
);
assert_eq!(
Backend::Sqlite.json_extract_scalar("\"meta\"", &["title"]),
"json_extract(\"meta\", '$.title')"
);
}
#[test]
fn u3_object_dotted_filter_pushes_json_extract() {
let cmd = json!({
"kind": "find", "collection": "courses",
"filter": { "meta.seo.title": "x" },
"projection": { "_id": 1, "title": 1 }
});
let sqlite = translate(Backend::Sqlite, &cmd, ®()).unwrap();
let text = sqlite["stmts"][0]["text"].as_str().unwrap();
assert!(
text.contains("json_extract(t.\"meta\", '$.seo.title')"),
"SQLite U3: {text}"
);
assert!(sqlite["unsupported"].as_array().unwrap().is_empty());
let pg = translate(Backend::Postgres, &cmd, ®()).unwrap();
let text = pg["stmts"][0]["text"].as_str().unwrap();
assert!(
text.contains("(t.\"meta\" #>> '{seo,title}')"),
"PG U3: {text}"
);
let my = translate(Backend::Mysql, &cmd, ®()).unwrap();
let text = my["stmts"][0]["text"].as_str().unwrap();
assert!(
text.contains("JSON_UNQUOTE(JSON_EXTRACT(t.`meta`, '$.seo.title'))"),
"MySQL U3: {text}"
);
}
#[test]
fn u4_object_dotted_sort_pushes_json_extract() {
let cmd = json!({
"kind": "aggregate", "collection": "courses",
"pipeline": [ { "$sort": { "meta.level": 1 } }, { "$project": { "_id": 1 } } ]
});
let sqlite = translate(Backend::Sqlite, &cmd, ®()).unwrap();
let text = sqlite["stmts"][0]["text"].as_str().unwrap();
assert!(
text.contains("ORDER BY json_extract(t.\"meta\", '$.level') ASC"),
"SQLite U4: {text}"
);
assert!(sqlite["unsupported"].as_array().unwrap().is_empty());
let pg = translate(Backend::Postgres, &cmd, ®()).unwrap();
let text = pg["stmts"][0]["text"].as_str().unwrap();
assert!(
text.contains("ORDER BY (t.\"meta\" #>> '{level}') ASC"),
"PG U4: {text}"
);
}
#[test]
fn insert_object_field_serialized_as_json_text() {
let cmd = json!({
"kind": "insertOne", "collection": "courses",
"doc": { "_id": "c1", "title": "t", "meta": { "level": "a" } }
});
let out = translate(Backend::Sqlite, &cmd, ®()).unwrap();
let params = out["stmts"][0]["params"].as_array().unwrap();
assert!(
params
.iter()
.any(|p| p.as_str() == Some("{\"level\":\"a\"}")),
"object 字段应序列化为 JSON 文本: {params:?}"
);
}
#[test]
fn u1_array_contains_pushes_native_predicate() {
let cmd = json!({
"kind": "find", "collection": "courses",
"filter": { "tags": "python" },
"projection": { "_id": 1 }
});
let my = translate(Backend::Mysql, &cmd, ®()).unwrap();
let text = my["stmts"][0]["text"].as_str().unwrap();
assert!(
text.contains("JSON_CONTAINS(t.`tags`, CAST(? AS JSON))"),
"MySQL U1: {text}"
);
let params = my["stmts"][0]["params"].as_array().unwrap();
assert!(
params.iter().any(|p| p.as_str() == Some("\"python\"")),
"MySQL U1 参数应为值的 JSON 字面量: {params:?}"
);
let pg = translate(Backend::Postgres, &cmd, ®()).unwrap();
let text = pg["stmts"][0]["text"].as_str().unwrap();
assert!(text.contains("t.\"tags\" @> $1::jsonb"), "PG U1: {text}");
let params = pg["stmts"][0]["params"].as_array().unwrap();
assert!(
params.iter().any(|p| p.as_str() == Some("[\"python\"]")),
"PG U1 参数应为单元素数组: {params:?}"
);
let sq = translate(Backend::Sqlite, &cmd, ®()).unwrap();
let text = sq["stmts"][0]["text"].as_str().unwrap();
assert!(
text.contains("EXISTS (SELECT 1 FROM json_each(t.\"tags\") WHERE json_each.value = ?)"),
"SQLite U1: {text}"
);
let params = sq["stmts"][0]["params"].as_array().unwrap();
assert!(
params.iter().any(|p| p.as_str() == Some("python")),
"SQLite U1 参数应为标量原值: {params:?}"
);
}
#[test]
fn u1_array_whole_equality_pushes_json_value_eq() {
let cmd = json!({
"kind": "find", "collection": "courses",
"filter": { "tags": ["a", "b"] },
"projection": { "_id": 1 }
});
let my = translate(Backend::Mysql, &cmd, ®()).unwrap();
let text = my["stmts"][0]["text"].as_str().unwrap();
assert!(
text.contains("t.`tags` = CAST(? AS JSON)"),
"MySQL U1 整体等值: {text}"
);
let pg = translate(Backend::Postgres, &cmd, ®()).unwrap();
let text = pg["stmts"][0]["text"].as_str().unwrap();
assert!(
text.contains("t.\"tags\" = $1::jsonb"),
"PG U1 整体等值: {text}"
);
let sq = translate(Backend::Sqlite, &cmd, ®()).unwrap();
let text = sq["stmts"][0]["text"].as_str().unwrap();
assert!(
text.contains("json(t.\"tags\") = json(?)"),
"SQLite U1 整体等值: {text}"
);
}
#[test]
fn u2_object_equality_pushes_json_value_eq_and_warns() {
let cmd = json!({
"kind": "find", "collection": "courses",
"filter": { "meta": { "level": "a" } },
"projection": { "_id": 1 }
});
let sq = translate(Backend::Sqlite, &cmd, ®()).unwrap();
let text = sq["stmts"][0]["text"].as_str().unwrap();
assert!(
text.contains("json(t.\"meta\") = json(?)"),
"SQLite U2: {text}"
);
let params = sq["stmts"][0]["params"].as_array().unwrap();
assert!(
params
.iter()
.any(|p| p.as_str() == Some("{\"level\":\"a\"}")),
"U2 参数应为对象 JSON 文本: {params:?}"
);
let warnings = sq["warnings"].as_array().unwrap();
assert!(
warnings
.iter()
.any(|w| w.as_str().unwrap_or("").contains("键序")),
"U2 必须产出跨后端键序差异告警(禁静默): {warnings:?}"
);
let pg = translate(Backend::Postgres, &cmd, ®()).unwrap();
let text = pg["stmts"][0]["text"].as_str().unwrap();
assert!(text.contains("t.\"meta\" = $1::jsonb"), "PG U2: {text}");
}
#[test]
fn u1_array_exists_uses_present_sentinel() {
let cmd = json!({
"kind": "find", "collection": "courses",
"filter": { "tags": { "$exists": false } },
"projection": { "_id": 1 }
});
let sq = translate(Backend::Sqlite, &cmd, ®()).unwrap();
let text = sq["stmts"][0]["text"].as_str().unwrap();
assert!(
text.contains("NOT (COALESCE(t.__present, ',') LIKE '%,tags,%')"),
"U1 $exists 应走 __present 哨兵: {text}"
);
}
#[test]
fn json_field_unsupported_operator_is_error() {
let cmd = json!({
"kind": "find", "collection": "courses",
"filter": { "tags": { "$gt": "a" } },
"projection": { "_id": 1 }
});
let err = translate(Backend::Sqlite, &cmd, ®()).expect_err("U1 $gt 应显式报错");
assert!(err.contains("$gt"), "错误信息应含操作符: {err}");
}
#[test]
fn json_column_restores_nested_object() {
let cmd = json!({
"kind": "find", "collection": "courses", "filter": {},
"projection": { "_id": 1, "title": 1, "meta": 1 }
});
let out = translate(Backend::Sqlite, &cmd, ®()).unwrap();
let shape = &out["stmts"][0]["rowShape"];
let rows = json!([
{ "_id": "c1", "title": "t",
"meta": "{\"level\":\"a\",\"seo\":{\"title\":\"z\"}}",
"__present": ",title,meta," }
]);
let restored = restore_rows_json(shape, &rows).unwrap();
assert_eq!(
restored,
json!([{ "_id": "c1", "title": "t",
"meta": { "level": "a", "seo": { "title": "z" } } }])
);
}