medoo_rs 0.1.0

Query builder dinámico multi-backend (Postgres/MySQL/SQLite) inspirado en Medoo (PHP). Núcleo sin dependencias, pool async opcional.
Documentation
use medoo_rs::{Backend, Db, JsonPath, QueryError, Value};

#[test]
fn json_path_parses_dotted_and_indices() {
    let p = JsonPath::parse("$.user.tags[0].name").unwrap();
    assert_eq!(p.render_mysql(), "$.user.tags[0].name");
    assert_eq!(p.render_postgres(), "{user,tags,0,name}");
}

#[test]
fn json_path_accepts_no_dollar_prefix() {
    let p = JsonPath::parse("user.id").unwrap();
    assert_eq!(p.render_mysql(), "$.user.id");
}

#[test]
fn json_path_rejects_injection() {
    for evil in ["$.a' OR 1=1--", "$.a;b", "$.a-b", "$.a.b[xx]", "$.", "$..a", ""] {
        let r = JsonPath::parse(evil);
        assert!(matches!(r, Err(QueryError::InvalidIdentifier(_))), "should reject: {:?}", evil);
    }
}

#[test]
fn where_json_mysql_uses_unquote_extract() {
    let db = Db::new(Backend::MySql);
    let (sql, params) = db
        .select("users")
        .where_json("meta", "$.role", "=", "admin")
        .to_sql()
        .unwrap();
    assert_eq!(
        sql,
        "SELECT * FROM `users` WHERE JSON_UNQUOTE(JSON_EXTRACT(`meta`, '$.role')) = ?"
    );
    assert_eq!(params, vec![Value::Text("admin".into())]);
}

#[test]
fn where_json_postgres_uses_text_path_op() {
    let db = Db::new(Backend::Postgres);
    let (sql, _) = db
        .select("users")
        .where_json("meta", "user.id", ">", 10)
        .to_sql()
        .unwrap();
    assert_eq!(
        sql,
        r#"SELECT * FROM "users" WHERE "meta" #>> '{user,id}' > $1"#
    );
}

#[test]
fn where_json_sqlite_uses_json_extract() {
    let db = Db::new(Backend::Sqlite);
    let (sql, _) = db
        .select("rows")
        .where_json("data", "$.tags[2]", "[~]", "%foo%")
        .to_sql()
        .unwrap();
    assert_eq!(
        sql,
        r#"SELECT * FROM "rows" WHERE json_extract("data", '$.tags[2]') LIKE ?"#
    );
}

#[test]
fn where_json_contains_mysql() {
    let db = Db::new(Backend::MySql);
    let needle = Value::json(r#"{"role":"admin"}"#);
    let (sql, params) = db
        .select("users")
        .where_json_contains("meta", needle.clone())
        .to_sql()
        .unwrap();
    assert_eq!(sql, "SELECT * FROM `users` WHERE JSON_CONTAINS(`meta`, ?)");
    assert_eq!(params, vec![needle]);
}

#[test]
fn where_json_contains_postgres_casts_jsonb() {
    let db = Db::new(Backend::Postgres);
    let (sql, _) = db
        .select("users")
        .where_json_contains("meta", Value::json(r#"{"role":"admin"}"#))
        .to_sql()
        .unwrap();
    assert_eq!(
        sql,
        r#"SELECT * FROM "users" WHERE "meta" @> $1::jsonb"#
    );
}

#[test]
fn where_json_contains_sqlite_errors() {
    let db = Db::new(Backend::Sqlite);
    let err = db
        .select("t")
        .where_json_contains("meta", Value::json("{}"))
        .to_sql()
        .unwrap_err();
    assert!(matches!(err, QueryError::InvalidOperator(_)));
}

#[test]
fn where_json_null_handling() {
    let db = Db::new(Backend::MySql);
    let v: Option<&str> = None;
    let (sql, params) = db
        .select("users")
        .where_json("meta", "$.deleted_at", "=", v)
        .to_sql()
        .unwrap();
    assert_eq!(
        sql,
        "SELECT * FROM `users` WHERE JSON_UNQUOTE(JSON_EXTRACT(`meta`, '$.deleted_at')) IS NULL"
    );
    assert!(params.is_empty());
}

#[test]
fn where_json_combined_with_regular_filters() {
    let db = Db::new(Backend::Postgres);
    let (sql, params) = db
        .select("users")
        .where_eq("active", true)
        .where_json("meta", "$.role", "=", "admin")
        .where_json("meta", "$.score", ">=", 10)
        .order_desc("created_at")
        .limit(5)
        .to_sql()
        .unwrap();
    assert_eq!(
        sql,
        r#"SELECT * FROM "users" WHERE "active" = $1 AND "meta" #>> '{role}' = $2 AND "meta" #>> '{score}' >= $3 ORDER BY "created_at" DESC LIMIT 5"#
    );
    assert_eq!(params.len(), 3);
}