use serde_json::Value;
use std::collections::HashMap;
pub fn has_many_in_sql(child_table: &str, foreign_key: &str, parent_pk_values: &[&str]) -> String {
if parent_pk_values.is_empty() {
return format!(
"SELECT * FROM {} WHERE {} IN (NULL)",
child_table, foreign_key
);
}
format!(
"SELECT * FROM {} WHERE {} IN ({})",
child_table,
foreign_key,
parent_pk_values.join(", ")
)
}
pub fn has_one_in_sql(child_table: &str, foreign_key: &str, parent_pk_values: &[&str]) -> String {
has_many_in_sql(child_table, foreign_key, parent_pk_values)
}
pub fn belongs_to_in_sql(parent_table: &str, parent_pk: &str, fk_values: &[&str]) -> String {
if fk_values.is_empty() {
return format!(
"SELECT * FROM {} WHERE {} IN (NULL)",
parent_table, parent_pk
);
}
format!(
"SELECT * FROM {} WHERE {} IN ({})",
parent_table,
parent_pk,
fk_values.join(", ")
)
}
pub fn belongs_to_many_in_sql(
target_table: &str,
junction_table: &str,
target_pk: &str,
other_key: &str,
foreign_key: &str,
current_pk_values: &[&str],
) -> String {
if current_pk_values.is_empty() {
return format!(
"SELECT t.* FROM {} t INNER JOIN {} j ON t.{} = j.{} WHERE j.{} IN (NULL)",
target_table, junction_table, target_pk, other_key, foreign_key
);
}
format!(
"SELECT t.* FROM {} t INNER JOIN {} j ON t.{} = j.{} WHERE j.{} IN ({})",
target_table,
junction_table,
target_pk,
other_key,
foreign_key,
current_pk_values.join(", ")
)
}
pub fn sanitize_pk_value(value: &str) -> String {
if value.parse::<i64>().is_ok() || value.parse::<f64>().is_ok() {
value.to_string()
} else {
format!("'{}'", value.replace('\'', "''"))
}
}
pub fn collect_pk_values(rows: &[Value], pk_field: &str) -> Vec<String> {
rows.iter()
.filter_map(|row| {
row.get(pk_field)
.filter(|v| !v.is_null())
.map(value_to_pk_string)
})
.collect()
}
pub fn group_by_fk(rows: Vec<Value>, fk_field: &str) -> HashMap<String, Vec<Value>> {
let mut grouped: HashMap<String, Vec<Value>> = HashMap::new();
for row in rows {
let fk_value = row
.get(fk_field)
.filter(|v| !v.is_null())
.map(value_to_pk_string)
.unwrap_or_default();
grouped.entry(fk_value).or_default().push(row);
}
grouped
}
pub fn parse_with_notation(with: &str) -> (&str, Option<&str>) {
match with.split_once('.') {
Some((relation, sub)) => (relation, Some(sub)),
None => (with, None),
}
}
fn value_to_pk_string(value: &Value) -> String {
match value {
Value::Number(n) => n.to_string(),
Value::String(s) => s.clone(),
Value::Bool(b) => b.to_string(),
_ => String::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_has_many_in_sql_multiple_pks() {
let sql = has_many_in_sql("orders", "user_id", &["1", "2", "3"]);
assert_eq!(sql, "SELECT * FROM orders WHERE user_id IN (1, 2, 3)");
}
#[test]
fn test_has_many_in_sql_single_pk() {
let sql = has_many_in_sql("orders", "user_id", &["1"]);
assert_eq!(sql, "SELECT * FROM orders WHERE user_id IN (1)");
}
#[test]
fn test_has_many_in_sql_empty_pks() {
let sql = has_many_in_sql("orders", "user_id", &[]);
assert_eq!(sql, "SELECT * FROM orders WHERE user_id IN (NULL)");
}
#[test]
fn test_has_many_in_sql_string_pks() {
let sql = has_many_in_sql("orders", "user_id", &["'abc-1'", "'abc-2'"]);
assert_eq!(
sql,
"SELECT * FROM orders WHERE user_id IN ('abc-1', 'abc-2')"
);
}
#[test]
fn test_has_many_in_sql_custom_foreign_key() {
let sql = has_many_in_sql("orders", "uid", &["1", "2"]);
assert_eq!(sql, "SELECT * FROM orders WHERE uid IN (1, 2)");
}
#[test]
fn test_has_many_in_sql_multi_word_table() {
let sql = has_many_in_sql("order_items", "order_id", &["1", "2"]);
assert_eq!(sql, "SELECT * FROM order_items WHERE order_id IN (1, 2)");
}
#[test]
fn test_has_one_in_sql_multiple_pks() {
let sql = has_one_in_sql("profiles", "user_id", &["1", "2", "3"]);
assert_eq!(sql, "SELECT * FROM profiles WHERE user_id IN (1, 2, 3)");
}
#[test]
fn test_has_one_in_sql_empty_pks() {
let sql = has_one_in_sql("profiles", "user_id", &[]);
assert_eq!(sql, "SELECT * FROM profiles WHERE user_id IN (NULL)");
}
#[test]
fn test_has_one_in_sql_equals_has_many_in_sql() {
let has_one = has_one_in_sql("profiles", "user_id", &["1", "2"]);
let has_many = has_many_in_sql("profiles", "user_id", &["1", "2"]);
assert_eq!(has_one, has_many);
}
#[test]
fn test_belongs_to_in_sql_multiple_fks() {
let sql = belongs_to_in_sql("depts", "id", &["1", "2", "3"]);
assert_eq!(sql, "SELECT * FROM depts WHERE id IN (1, 2, 3)");
}
#[test]
fn test_belongs_to_in_sql_single_fk() {
let sql = belongs_to_in_sql("depts", "id", &["1"]);
assert_eq!(sql, "SELECT * FROM depts WHERE id IN (1)");
}
#[test]
fn test_belongs_to_in_sql_empty_fks() {
let sql = belongs_to_in_sql("depts", "id", &[]);
assert_eq!(sql, "SELECT * FROM depts WHERE id IN (NULL)");
}
#[test]
fn test_belongs_to_in_sql_custom_parent_pk() {
let sql = belongs_to_in_sql("categories", "cid", &["1", "2"]);
assert_eq!(sql, "SELECT * FROM categories WHERE cid IN (1, 2)");
}
#[test]
fn test_belongs_to_in_sql_multi_word_table() {
let sql = belongs_to_in_sql("user_profiles", "id", &["1", "2"]);
assert_eq!(sql, "SELECT * FROM user_profiles WHERE id IN (1, 2)");
}
#[test]
fn test_belongs_to_many_in_sql_multiple_pks() {
let sql = belongs_to_many_in_sql(
"roles",
"user_role",
"id",
"role_id",
"user_id",
&["1", "2", "3"],
);
assert_eq!(
sql,
"SELECT t.* FROM roles t INNER JOIN user_role j ON t.id = j.role_id WHERE j.user_id IN (1, 2, 3)"
);
}
#[test]
fn test_belongs_to_many_in_sql_single_pk() {
let sql = belongs_to_many_in_sql("roles", "user_role", "id", "role_id", "user_id", &["1"]);
assert_eq!(
sql,
"SELECT t.* FROM roles t INNER JOIN user_role j ON t.id = j.role_id WHERE j.user_id IN (1)"
);
}
#[test]
fn test_belongs_to_many_in_sql_empty_pks() {
let sql = belongs_to_many_in_sql("roles", "user_role", "id", "role_id", "user_id", &[]);
assert_eq!(
sql,
"SELECT t.* FROM roles t INNER JOIN user_role j ON t.id = j.role_id WHERE j.user_id IN (NULL)"
);
}
#[test]
fn test_belongs_to_many_in_sql_custom_keys() {
let sql = belongs_to_many_in_sql("roles", "user_role", "pk", "rid", "uid", &["1", "2"]);
assert_eq!(
sql,
"SELECT t.* FROM roles t INNER JOIN user_role j ON t.pk = j.rid WHERE j.uid IN (1, 2)"
);
}
#[test]
fn test_belongs_to_many_in_sql_multi_word_tables() {
let sql = belongs_to_many_in_sql(
"order_items",
"order_item_tag",
"id",
"tag_id",
"order_item_id",
&["1", "2"],
);
assert_eq!(
sql,
"SELECT t.* FROM order_items t INNER JOIN order_item_tag j ON t.id = j.tag_id WHERE j.order_item_id IN (1, 2)"
);
}
#[test]
fn test_belongs_to_many_in_sql_aligns_php_pattern() {
let sql = belongs_to_many_in_sql(
"roles",
"user_role",
"id",
"role_id",
"user_id",
&["1", "2"],
);
assert!(sql.starts_with("SELECT t.* FROM roles t INNER JOIN user_role j ON t.id = j.role_id WHERE j.user_id IN ("));
}
#[test]
fn test_sanitize_pk_value_numeric_integer() {
assert_eq!(sanitize_pk_value("1"), "1");
assert_eq!(sanitize_pk_value("12345"), "12345");
assert_eq!(sanitize_pk_value("-100"), "-100");
}
#[test]
fn test_sanitize_pk_value_numeric_float() {
assert_eq!(sanitize_pk_value("1.5"), "1.5");
assert_eq!(sanitize_pk_value("-0.5"), "-0.5");
}
#[test]
fn test_sanitize_pk_value_string() {
assert_eq!(sanitize_pk_value("abc"), "'abc'");
assert_eq!(sanitize_pk_value("uuid-123"), "'uuid-123'");
}
#[test]
fn test_sanitize_pk_value_string_with_quote() {
assert_eq!(sanitize_pk_value("a'b"), "'a''b'");
assert_eq!(sanitize_pk_value("'"), "''''");
}
#[test]
fn test_sanitize_pk_value_empty_string() {
assert_eq!(sanitize_pk_value(""), "''");
}
#[test]
fn test_collect_pk_values_integer_pks() {
let rows = vec![
json!({"id": 1, "name": "Alice"}),
json!({"id": 2, "name": "Bob"}),
json!({"id": 3, "name": "Charlie"}),
];
let pks = collect_pk_values(&rows, "id");
assert_eq!(pks, vec!["1", "2", "3"]);
}
#[test]
fn test_collect_pk_values_string_pks() {
let rows = vec![
json!({"id": "uuid-1", "name": "Alice"}),
json!({"id": "uuid-2", "name": "Bob"}),
];
let pks = collect_pk_values(&rows, "id");
assert_eq!(pks, vec!["uuid-1", "uuid-2"]);
}
#[test]
fn test_collect_pk_values_skip_null() {
let rows = vec![
json!({"id": 1, "name": "Alice"}),
json!({"id": null, "name": "Bob"}), json!({"id": 3, "name": "Charlie"}),
];
let pks = collect_pk_values(&rows, "id");
assert_eq!(pks, vec!["1", "3"]);
}
#[test]
fn test_collect_pk_values_skip_missing_field() {
let rows = vec![
json!({"id": 1, "name": "Alice"}),
json!({"name": "Bob"}), json!({"id": 3, "name": "Charlie"}),
];
let pks = collect_pk_values(&rows, "id");
assert_eq!(pks, vec!["1", "3"]);
}
#[test]
fn test_collect_pk_values_empty_rows() {
let rows: Vec<Value> = vec![];
let pks = collect_pk_values(&rows, "id");
assert!(pks.is_empty());
}
#[test]
fn test_collect_pk_values_custom_pk_field() {
let rows = vec![
json!({"uid": 1, "name": "Alice"}),
json!({"uid": 2, "name": "Bob"}),
];
let pks = collect_pk_values(&rows, "uid");
assert_eq!(pks, vec!["1", "2"]);
}
#[test]
fn test_collect_pk_values_dedup_not_applied() {
let rows = vec![json!({"id": 1}), json!({"id": 1}), json!({"id": 2})];
let pks = collect_pk_values(&rows, "id");
assert_eq!(pks, vec!["1", "1", "2"]);
}
#[test]
fn test_group_by_fk_basic_grouping() {
let rows = vec![
json!({"id": 101, "user_id": 1, "name": "Order A"}),
json!({"id": 102, "user_id": 2, "name": "Order B"}),
json!({"id": 103, "user_id": 1, "name": "Order C"}),
];
let grouped = group_by_fk(rows, "user_id");
assert_eq!(grouped.get("1").unwrap().len(), 2);
assert_eq!(grouped.get("2").unwrap().len(), 1);
}
#[test]
fn test_group_by_fk_string_fk() {
let rows = vec![
json!({"id": 101, "user_id": "uuid-1"}),
json!({"id": 102, "user_id": "uuid-2"}),
json!({"id": 103, "user_id": "uuid-1"}),
];
let grouped = group_by_fk(rows, "user_id");
assert_eq!(grouped.get("uuid-1").unwrap().len(), 2);
assert_eq!(grouped.get("uuid-2").unwrap().len(), 1);
}
#[test]
fn test_group_by_fk_skip_null() {
let rows = vec![
json!({"id": 101, "user_id": 1}),
json!({"id": 102, "user_id": null}),
json!({"id": 103, "user_id": 1}),
];
let grouped = group_by_fk(rows, "user_id");
assert_eq!(grouped.get("1").unwrap().len(), 2);
assert_eq!(grouped.get("").unwrap().len(), 1);
}
#[test]
fn test_group_by_fk_empty_rows() {
let rows: Vec<Value> = vec![];
let grouped = group_by_fk(rows, "user_id");
assert!(grouped.is_empty());
}
#[test]
fn test_group_by_fk_custom_fk_field() {
let rows = vec![json!({"id": 101, "uid": 1}), json!({"id": 102, "uid": 2})];
let grouped = group_by_fk(rows, "uid");
assert_eq!(grouped.get("1").unwrap().len(), 1);
assert_eq!(grouped.get("2").unwrap().len(), 1);
}
#[test]
fn test_group_by_fk_preserves_row_data() {
let rows = vec![json!({"id": 101, "user_id": 1, "name": "Order A", "amount": 100.5})];
let grouped = group_by_fk(rows, "user_id");
let bucket = grouped.get("1").unwrap();
assert_eq!(bucket.len(), 1);
assert_eq!(bucket[0]["id"], json!(101));
assert_eq!(bucket[0]["name"], json!("Order A"));
assert_eq!(bucket[0]["amount"], json!(100.5));
}
#[test]
fn test_parse_with_notation_simple_relation() {
assert_eq!(parse_with_notation("category"), ("category", None));
assert_eq!(parse_with_notation("dept"), ("dept", None));
}
#[test]
fn test_parse_with_notation_nested_relation() {
assert_eq!(
parse_with_notation("items.product"),
("items", Some("product"))
);
assert_eq!(
parse_with_notation("user.profile"),
("user", Some("profile"))
);
}
#[test]
fn test_parse_with_notation_deep_nested() {
assert_eq!(parse_with_notation("a.b.c"), ("a", Some("b.c")));
assert_eq!(
parse_with_notation("user.orders.items"),
("user", Some("orders.items"))
);
}
#[test]
fn test_parse_with_notation_empty_string() {
assert_eq!(parse_with_notation(""), ("", None));
}
#[test]
fn test_parse_with_notation_trailing_dot() {
assert_eq!(parse_with_notation("relation."), ("relation", Some("")));
}
#[test]
fn test_parse_with_notation_leading_dot() {
assert_eq!(parse_with_notation(".sub"), ("", Some("sub")));
}
#[test]
fn test_value_to_pk_string_integer() {
assert_eq!(value_to_pk_string(&json!(1)), "1");
assert_eq!(value_to_pk_string(&json!(-100)), "-100");
}
#[test]
fn test_value_to_pk_string_float() {
assert_eq!(value_to_pk_string(&json!(1.5)), "1.5");
}
#[test]
fn test_value_to_pk_string_string() {
assert_eq!(value_to_pk_string(&json!("uuid-123")), "uuid-123");
}
#[test]
fn test_value_to_pk_string_bool() {
assert_eq!(value_to_pk_string(&json!(true)), "true");
assert_eq!(value_to_pk_string(&json!(false)), "false");
}
#[test]
fn test_value_to_pk_string_null_returns_empty() {
assert_eq!(value_to_pk_string(&Value::Null), "");
}
#[test]
fn test_value_to_pk_string_object_returns_empty() {
assert_eq!(value_to_pk_string(&json!({"a": 1})), "");
}
#[test]
fn test_value_to_pk_string_array_returns_empty() {
assert_eq!(value_to_pk_string(&json!([1, 2, 3])), "");
}
#[test]
fn test_r5_php_with_in_query_pattern_has_many() {
let sql = has_many_in_sql("orders", "user_id", &["1", "2", "3"]);
assert_eq!(sql, "SELECT * FROM orders WHERE user_id IN (1, 2, 3)");
}
#[test]
fn test_r5_php_with_in_query_pattern_belongs_to() {
let sql = belongs_to_in_sql("depts", "id", &["1", "2", "3"]);
assert_eq!(sql, "SELECT * FROM depts WHERE id IN (1, 2, 3)");
}
#[test]
fn test_r5_php_with_in_query_pattern_belongs_to_many() {
let sql = belongs_to_many_in_sql(
"roles",
"user_role",
"id",
"role_id",
"user_id",
&["1", "2"],
);
assert!(sql.contains("INNER JOIN user_role j"));
assert!(sql.contains("WHERE j.user_id IN (1, 2)"));
}
#[test]
fn test_r5_php_collect_range_skips_null() {
let rows = vec![
json!({"id": 1}),
json!({"id": null}), json!({"id": 3}),
];
let pks = collect_pk_values(&rows, "id");
assert_eq!(pks, vec!["1", "3"]);
}
#[test]
fn test_r5_php_group_by_fk_matches_php_data_bucket() {
let rows = vec![
json!({"id": 101, "user_id": 1, "name": "A"}),
json!({"id": 102, "user_id": 2, "name": "B"}),
json!({"id": 103, "user_id": 1, "name": "C"}),
];
let grouped = group_by_fk(rows, "user_id");
assert_eq!(grouped.get("1").unwrap().len(), 2);
assert_eq!(grouped.get("2").unwrap().len(), 1);
}
#[test]
fn test_r5_php_parse_with_notation_explode_dot() {
assert_eq!(parse_with_notation("category"), ("category", None));
assert_eq!(
parse_with_notation("items.product"),
("items", Some("product"))
);
assert_eq!(parse_with_notation("a.b.c"), ("a", Some("b.c")));
}
#[test]
fn test_r5_php_has_one_in_sql_same_as_has_many() {
let has_one = has_one_in_sql("profiles", "user_id", &["1", "2"]);
let has_many = has_many_in_sql("profiles", "user_id", &["1", "2"]);
assert_eq!(has_one, has_many);
}
#[test]
fn test_r5_php_empty_range_returns_in_null() {
let sql_has_many = has_many_in_sql("orders", "user_id", &[]);
let sql_belongs_to = belongs_to_in_sql("depts", "id", &[]);
let sql_belongs_to_many =
belongs_to_many_in_sql("roles", "user_role", "id", "role_id", "user_id", &[]);
assert!(sql_has_many.contains("IN (NULL)"));
assert!(sql_belongs_to.contains("IN (NULL)"));
assert!(sql_belongs_to_many.contains("IN (NULL)"));
}
#[test]
fn test_r5_php_sanitize_pk_value_sql_escaping() {
assert_eq!(sanitize_pk_value("1"), "1");
assert_eq!(sanitize_pk_value("abc"), "'abc'");
assert_eq!(sanitize_pk_value("a'b"), "'a''b'");
}
#[test]
fn test_integration_with_has_many_user_orders() {
let users = vec![
json!({"id": 1, "name": "Alice"}),
json!({"id": 2, "name": "Bob"}),
json!({"id": 3, "name": "Charlie"}),
];
let user_pks = collect_pk_values(&users, "id");
assert_eq!(user_pks, vec!["1", "2", "3"]);
let pk_refs: Vec<&str> = user_pks.iter().map(|s| s.as_str()).collect();
let sql = has_many_in_sql("orders", "user_id", &pk_refs);
assert_eq!(sql, "SELECT * FROM orders WHERE user_id IN (1, 2, 3)");
let orders = vec![
json!({"id": 101, "user_id": 1, "amount": 100}),
json!({"id": 102, "user_id": 2, "amount": 200}),
json!({"id": 103, "user_id": 1, "amount": 150}),
];
let grouped = group_by_fk(orders, "user_id");
assert_eq!(grouped.get("1").unwrap().len(), 2);
assert_eq!(grouped.get("2").unwrap().len(), 1);
assert!(!grouped.contains_key("3")); }
#[test]
fn test_integration_with_belongs_to_order_user() {
let orders = vec![
json!({"id": 101, "user_id": 1, "amount": 100}),
json!({"id": 102, "user_id": 2, "amount": 200}),
json!({"id": 103, "user_id": 1, "amount": 150}),
];
let user_fks = collect_pk_values(&orders, "user_id");
assert_eq!(user_fks, vec!["1", "2", "1"]);
let unique_fks: Vec<&str> = vec!["1", "2"];
let sql = belongs_to_in_sql("users", "id", &unique_fks);
assert_eq!(sql, "SELECT * FROM users WHERE id IN (1, 2)");
let users = vec![
json!({"id": 1, "name": "Alice"}),
json!({"id": 2, "name": "Bob"}),
];
let grouped = group_by_fk(users, "id");
assert_eq!(grouped.get("1").unwrap().len(), 1);
assert_eq!(grouped.get("2").unwrap().len(), 1);
}
#[test]
fn test_integration_with_belongs_to_many_user_roles() {
let users = vec![
json!({"id": 1, "name": "Alice"}),
json!({"id": 2, "name": "Bob"}),
];
let user_pks = collect_pk_values(&users, "id");
let pk_refs: Vec<&str> = user_pks.iter().map(|s| s.as_str()).collect();
let sql =
belongs_to_many_in_sql("roles", "user_role", "id", "role_id", "user_id", &pk_refs);
assert_eq!(
sql,
"SELECT t.* FROM roles t INNER JOIN user_role j ON t.id = j.role_id WHERE j.user_id IN (1, 2)"
);
}
#[test]
fn test_integration_with_nested_notation_user_orders_items() {
let (relation, sub) = parse_with_notation("orders.items");
assert_eq!(relation, "orders");
assert_eq!(sub, Some("items"));
let users = vec![json!({"id": 1, "name": "Alice"})];
let user_pks = collect_pk_values(&users, "id");
let pk_refs: Vec<&str> = user_pks.iter().map(|s| s.as_str()).collect();
let sql = has_many_in_sql("orders", "user_id", &pk_refs);
assert_eq!(sql, "SELECT * FROM orders WHERE user_id IN (1)");
let orders = vec![
json!({"id": 101, "user_id": 1}),
json!({"id": 102, "user_id": 1}),
];
let order_pks = collect_pk_values(&orders, "id");
let pk_refs2: Vec<&str> = order_pks.iter().map(|s| s.as_str()).collect();
let sql2 = has_many_in_sql("order_items", "order_id", &pk_refs2);
assert_eq!(
sql2,
"SELECT * FROM order_items WHERE order_id IN (101, 102)"
);
}
#[test]
fn test_integration_with_has_one_user_profile() {
let users = vec![
json!({"id": 1, "name": "Alice"}),
json!({"id": 2, "name": "Bob"}),
];
let user_pks = collect_pk_values(&users, "id");
let pk_refs: Vec<&str> = user_pks.iter().map(|s| s.as_str()).collect();
let sql = has_one_in_sql("profiles", "user_id", &pk_refs);
assert_eq!(sql, "SELECT * FROM profiles WHERE user_id IN (1, 2)");
let profiles = vec![
json!({"id": 11, "user_id": 1, "bio": "Alice's bio"}),
json!({"id": 22, "user_id": 2, "bio": "Bob's bio"}),
];
let grouped = group_by_fk(profiles, "user_id");
let alice_profile = &grouped.get("1").unwrap()[0];
assert_eq!(alice_profile["bio"], json!("Alice's bio"));
}
#[test]
fn test_integration_sanitize_pk_values_for_in_query() {
let raw_pks = ["1", "abc", "2", "x'y"];
let sanitized: Vec<String> = raw_pks.iter().map(|s| sanitize_pk_value(s)).collect();
let joined = sanitized.join(", ");
assert_eq!(joined, "1, 'abc', 2, 'x''y'");
let sanitized_refs: Vec<&str> = sanitized.iter().map(|s| s.as_str()).collect();
let sql = has_many_in_sql("orders", "user_id", &sanitized_refs);
assert_eq!(
sql,
"SELECT * FROM orders WHERE user_id IN (1, 'abc', 2, 'x''y')"
);
}
}