sqlutil 0.1.6

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

use serde_json::Value;

use crate::{quote, quote_identifier};

pub fn build_insert_sql<K>(table: &str, data: HashMap<K, Value>) -> String
where
    K: AsRef<str> + Eq + Hash,
{
    // Prepare columns and values
    let mut columns = Vec::new();
    // Prepare values vector
    let mut values = Vec::new();

    // Iterate over the data to populate columns and values
    for (key, value) in data {
        columns.push(quote_identifier(key.as_ref()));
        
        let val_str = match value {
            Value::Object(_) => quote(serde_json::to_string(&value).unwrap().as_str()), 
            Value::Array(_) => quote(serde_json::to_string(&value).unwrap().as_str()),
            Value::String(s) => quote(&s),
            Value::Number(n) => n.to_string(),
            Value::Bool(b) => b.to_string(),
            Value::Null => "NULL".to_string(),
        };
        values.push(val_str);
    }

    // Construct the final SQL INSERT statement
    format!(
        "INSERT INTO {} ({}) VALUES ({})",
        quote_identifier(table.trim()),
        columns.join(", "),
        values.join(", ")
    )
}

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

    #[test]
    fn test_insert_single() {
        let mut data = HashMap::new();
        data.insert("name".to_string(), json!("John"));
        let sql = build_insert_sql("users", data);
        assert_eq!(sql, "INSERT INTO \"users\" (\"name\") VALUES ('John')");
    }

    #[test]
    fn test_insert_multiple() {
        let mut data = HashMap::new();
        data.insert("name".to_string(), json!("John"));
        data.insert("age".to_string(), json!(30));
        let sql = build_insert_sql("users", data);
        
        // Check for possible valid outputs
        let valid1 = "INSERT INTO \"users\" (\"name\", \"age\") VALUES ('John', 30)";
        let valid2 = "INSERT INTO \"users\" (\"age\", \"name\") VALUES (30, 'John')";
        assert!(sql == valid1 || sql == valid2, "Generated SQL: {}", sql);
    }
    
    #[test]
    fn test_insert_types() {
        let mut data = HashMap::new();
        data.insert("is_active".to_string(), json!(true));
        data.insert("score".to_string(), json!(99.5));
        data.insert("description".to_string(), json!(null));
        
        let sql = build_insert_sql("stats", data);
        assert!(sql.contains("INSERT INTO \"stats\""));
        assert!(sql.contains("is_active"));
        assert!(sql.contains("true"));
        assert!(sql.contains("score"));
        assert!(sql.contains("99.5"));
        assert!(sql.contains("description"));
        assert!(sql.contains("NULL"));
    }

    #[test]
    fn test_insert_str_keys() {
        let mut data = HashMap::new();
        data.insert("name", json!("Jane"));
        let sql = build_insert_sql("users", data);
        assert_eq!(sql, "INSERT INTO \"users\" (\"name\") VALUES ('Jane')");
    }

    #[test]
    fn test_insert_string_with_single_quote() {
        let mut data = HashMap::new();
        data.insert("name".to_string(), json!("O'Reilly"));
        let sql = build_insert_sql("users", data);
        assert_eq!(sql, "INSERT INTO \"users\" (\"name\") VALUES ('O''Reilly')");
    }

    #[test]
    fn test_insert_null_value() {
        let mut data = HashMap::new();
        data.insert("deleted_at".to_string(), json!(null));
        let sql = build_insert_sql("users", data);
        assert_eq!(sql, "INSERT INTO \"users\" (\"deleted_at\") VALUES (NULL)");
    }

    #[test]
    fn test_insert_bool_false() {
        let mut data = HashMap::new();
        data.insert("active".to_string(), json!(false));
        let sql = build_insert_sql("users", data);
        assert_eq!(sql, "INSERT INTO \"users\" (\"active\") VALUES (false)");
    }

    #[test]
    fn test_insert_json_object() {
        let mut data = HashMap::new();
        data.insert("meta".to_string(), json!({"key": "value"}));
        let sql = build_insert_sql("users", data);
        // JSON object should be serialized and quoted as a string
        assert!(sql.starts_with("INSERT INTO \"users\" (\"meta\") VALUES ('"));
        assert!(sql.contains("key"));
        assert!(sql.contains("value"));
    }

    #[test]
    fn test_insert_json_array() {
        let mut data = HashMap::new();
        data.insert("tags".to_string(), json!(["rust", "sql"]));
        let sql = build_insert_sql("users", data);
        // JSON array should be serialized and quoted as a string
        assert!(sql.starts_with("INSERT INTO \"users\" (\"tags\") VALUES ('"));
        assert!(sql.contains("rust"));
        assert!(sql.contains("sql"));
    }

    #[test]
    fn test_insert_table_name_is_trimmed() {
        let mut data = HashMap::new();
        data.insert("name".to_string(), json!("Alice"));
        let sql = build_insert_sql("  users  ", data);
        assert_eq!(sql, "INSERT INTO \"users\" (\"name\") VALUES ('Alice')");
    }
}