sqlutil 0.1.7

A utility library for building SQL queries
Documentation
use std::collections::HashMap;

use crate::{build_update_sql, make_where};
use serde_json::Value;

pub fn make_update<K: AsRef<str> + Eq + std::hash::Hash>(
    table: &str,
    data: &HashMap<K, Value>,
    wheres: Option<&[&str]>,
) -> String {
    build_update_sql(table, data, wheres.map(make_where).as_deref())
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    // ── No WHERE clause ──────────────────────────────────────────────────────

    #[test]
    fn test_update_no_where_none() {
        let mut data = HashMap::new();
        data.insert("active", json!(false));
        let sql = make_update("users", &data, None);
        assert_eq!(sql, r#"UPDATE "users" SET "active" = false"#);
    }

    #[test]
    fn test_update_no_where_empty_slice() {
        // Some(&[]) → make_where returns "" → build_update_sql trims and skips it
        let mut data = HashMap::new();
        data.insert("active", json!(true));
        let sql = make_update("users", &data, Some(&[]));
        assert_eq!(sql, r#"UPDATE "users" SET "active" = true"#);
    }

    // ── Single WHERE condition ────────────────────────────────────────────────

    #[test]
    fn test_update_single_where() {
        let mut data = HashMap::new();
        data.insert("name", json!("Alice"));
        let sql = make_update("users", &data, Some(&["id = 1"]));
        assert_eq!(sql, r#"UPDATE "users" SET "name" = 'Alice' WHERE id = 1"#);
    }

    #[test]
    fn test_update_single_where_numeric() {
        let mut data = HashMap::new();
        data.insert("score", json!(99));
        let sql = make_update("players", &data, Some(&["player_id = 42"]));
        assert_eq!(sql, r#"UPDATE "players" SET "score" = 99 WHERE player_id = 42"#);
    }

    // ── Multiple WHERE conditions (joined with AND) ───────────────────────────

    #[test]
    fn test_update_two_where_conditions() {
        let mut data = HashMap::new();
        data.insert("status", json!("inactive"));
        let sql = make_update("users", &data, Some(&["active = 0", "age < 18"]));
        assert_eq!(
            sql,
            r#"UPDATE "users" SET "status" = 'inactive' WHERE active = 0 AND age < 18"#
        );
    }

    #[test]
    fn test_update_three_where_conditions() {
        let mut data = HashMap::new();
        data.insert("role", json!("guest"));
        let sql = make_update(
            "users",
            &data,
            Some(&["active = 1", "age > 18", "verified = 1"]),
        );
        assert_eq!(
            sql,
            r#"UPDATE "users" SET "role" = 'guest' WHERE active = 1 AND age > 18 AND verified = 1"#
        );
    }

    // ── Value types ───────────────────────────────────────────────────────────

    #[test]
    fn test_update_string_value() {
        let mut data = HashMap::new();
        data.insert("name", json!("Bob"));
        let sql = make_update("users", &data, Some(&["id = 7"]));
        assert_eq!(sql, r#"UPDATE "users" SET "name" = 'Bob' WHERE id = 7"#);
    }

    #[test]
    fn test_update_integer_value() {
        let mut data = HashMap::new();
        data.insert("age", json!(25));
        let sql = make_update("users", &data, Some(&["id = 3"]));
        assert_eq!(sql, r#"UPDATE "users" SET "age" = 25 WHERE id = 3"#);
    }

    #[test]
    fn test_update_float_value() {
        let mut data = HashMap::new();
        data.insert("price", json!(9.99));
        let sql = make_update("products", &data, Some(&["id = 10"]));
        assert_eq!(sql, r#"UPDATE "products" SET "price" = 9.99 WHERE id = 10"#);
    }

    #[test]
    fn test_update_bool_true_value() {
        let mut data = HashMap::new();
        data.insert("active", json!(true));
        let sql = make_update("users", &data, Some(&["id = 1"]));
        assert_eq!(sql, r#"UPDATE "users" SET "active" = true WHERE id = 1"#);
    }

    #[test]
    fn test_update_bool_false_value() {
        let mut data = HashMap::new();
        data.insert("active", json!(false));
        let sql = make_update("users", &data, Some(&["id = 2"]));
        assert_eq!(sql, r#"UPDATE "users" SET "active" = false WHERE id = 2"#);
    }

    #[test]
    fn test_update_null_value() {
        let mut data = HashMap::new();
        data.insert("deleted_at", json!(null));
        let sql = make_update("users", &data, Some(&["id = 5"]));
        assert_eq!(sql, r#"UPDATE "users" SET "deleted_at" = NULL WHERE id = 5"#);
    }

    #[test]
    fn test_update_json_object_value() {
        let mut data = HashMap::new();
        data.insert("meta", json!({"role": "admin"}));
        let sql = make_update("users", &data, Some(&["id = 1"]));
        assert!(sql.starts_with(r#"UPDATE "users" SET "meta" = '"#));
        assert!(sql.contains("role"));
        assert!(sql.contains("admin"));
        assert!(sql.ends_with("WHERE id = 1"));
    }

    #[test]
    fn test_update_json_array_value() {
        let mut data = HashMap::new();
        data.insert("tags", json!(["rust", "sql"]));
        let sql = make_update("posts", &data, Some(&["id = 3"]));
        assert!(sql.starts_with(r#"UPDATE "posts" SET "tags" = '"#));
        assert!(sql.contains("rust"));
        assert!(sql.contains("sql"));
        assert!(sql.ends_with("WHERE id = 3"));
    }

    // ── SQL injection / escaping ──────────────────────────────────────────────

    #[test]
    fn test_update_string_with_single_quote_escaped() {
        let mut data = HashMap::new();
        data.insert("name", json!("O'Brien"));
        let sql = make_update("users", &data, Some(&["id = 1"]));
        assert_eq!(sql, r#"UPDATE "users" SET "name" = 'O''Brien' WHERE id = 1"#);
    }

    #[test]
    fn test_update_column_with_double_quote_escaped() {
        let mut data = HashMap::new();
        data.insert(r#"col"name"#, json!("value"));
        let sql = make_update("users", &data, Some(&["id = 1"]));
        assert!(sql.contains(r#""col""name""#));
    }

    #[test]
    fn test_update_table_name_with_double_quote_escaped() {
        let mut data = HashMap::new();
        data.insert("x", json!(1));
        let sql = make_update(r#"my"table"#, &data, Some(&["id = 1"]));
        assert!(sql.starts_with(r#"UPDATE "my""table""#));
    }

    // ── Table name trimming ───────────────────────────────────────────────────

    #[test]
    fn test_update_table_name_is_trimmed() {
        let mut data = HashMap::new();
        data.insert("name", json!("Carol"));
        let sql = make_update("  users  ", &data, Some(&["id = 4"]));
        assert_eq!(sql, r#"UPDATE "users" SET "name" = 'Carol' WHERE id = 4"#);
    }

    // ── WHERE clause edge cases ───────────────────────────────────────────────

    #[test]
    fn test_update_where_whitespace_only_is_skipped() {
        let mut data = HashMap::new();
        data.insert("active", json!(true));
        let sql = make_update("users", &data, Some(&["   "]));
        assert_eq!(sql, r#"UPDATE "users" SET "active" = true"#);
    }

    #[test]
    fn test_update_where_in_clause() {
        let mut data = HashMap::new();
        data.insert("status", json!("archived"));
        let sql = make_update("posts", &data, Some(&["id IN (1, 2, 3)"]));
        assert_eq!(
            sql,
            r#"UPDATE "posts" SET "status" = 'archived' WHERE id IN (1, 2, 3)"#
        );
    }

    #[test]
    fn test_update_where_is_null() {
        let mut data = HashMap::new();
        data.insert("processed", json!(true));
        let sql = make_update("events", &data, Some(&["processed_at IS NULL"]));
        assert_eq!(
            sql,
            r#"UPDATE "events" SET "processed" = true WHERE processed_at IS NULL"#
        );
    }

    // ── Multiple fields (HashMap ordering is non-deterministic) ──────────────

    #[test]
    fn test_update_multiple_fields_with_where() {
        let mut data = HashMap::new();
        data.insert("name", json!("Dave"));
        data.insert("age", json!(40));
        let sql = make_update("users", &data, Some(&["id = 9"]));

        // Both column orderings are valid
        let v1 = r#"UPDATE "users" SET "name" = 'Dave' , "age" = 40 WHERE id = 9"#;
        let v2 = r#"UPDATE "users" SET "age" = 40 , "name" = 'Dave' WHERE id = 9"#;
        assert!(sql == v1 || sql == v2, "Unexpected SQL: {sql}");
    }

    #[test]
    fn test_update_multiple_fields_no_where() {
        let mut data = HashMap::new();
        data.insert("x", json!(1));
        data.insert("y", json!(2));
        let sql = make_update("coords", &data, None);

        assert!(sql.starts_with(r#"UPDATE "coords" SET "#));
        assert!(sql.contains(r#""x" = 1"#));
        assert!(sql.contains(r#""y" = 2"#));
        assert!(!sql.contains("WHERE"));
    }

    // ── String keys (&str) ────────────────────────────────────────────────────

    #[test]
    fn test_update_str_keys() {
        let mut data: HashMap<&str, Value> = HashMap::new();
        data.insert("email", json!("test@example.com"));
        let sql = make_update("users", &data, Some(&["id = 10"]));
        assert_eq!(
            sql,
            r#"UPDATE "users" SET "email" = 'test@example.com' WHERE id = 10"#
        );
    }

    // ── Output invariants ─────────────────────────────────────────────────────

    #[test]
    fn test_update_always_starts_with_update() {
        let mut data = HashMap::new();
        data.insert("x", json!(1));
        let sql = make_update("t", &data, None);
        assert!(sql.starts_with("UPDATE "));
    }

    #[test]
    fn test_update_always_contains_set() {
        let mut data = HashMap::new();
        data.insert("x", json!(1));
        let sql = make_update("t", &data, Some(&["id = 1"]));
        assert!(sql.contains(" SET "));
    }
}