use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq)]
pub struct MockTable {
pub name: String,
pub rows: Vec<BTreeMap<String, String>>,
}
impl MockTable {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
rows: Vec::new(),
}
}
pub fn add_row(&mut self, row: BTreeMap<String, String>) {
self.rows.push(row);
}
pub fn to_sql(&self) -> String {
if self.rows.is_empty() {
return format!("SELECT NULL WHERE 1=0 /* empty mock: {} */", self.name);
}
let columns: Vec<&String> = self.rows[0].keys().collect();
let mut parts = Vec::new();
for (i, row) in self.rows.iter().enumerate() {
let values: Vec<String> = columns
.iter()
.map(|col| {
let val = row.get(*col).map(|s| s.as_str()).unwrap_or("NULL");
if val == "NULL" {
if i == 0 {
format!("NULL AS {col}")
} else {
"NULL".to_string()
}
} else if i == 0 {
format!("'{}' AS {col}", val.replace('\'', "''"))
} else {
format!("'{}'", val.replace('\'', "''"))
}
})
.collect();
parts.push(format!("SELECT {}", values.join(", ")));
}
parts.join("\nUNION ALL\n")
}
}
#[macro_export]
macro_rules! mock_rows {
[$( {$($key:literal => $val:literal),* $(,)?} ),* $(,)?] => {
vec![
$(
{
let mut row = ::std::collections::BTreeMap::new();
$( row.insert($key.to_string(), $val.to_string()); )*
row
}
),*
]
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mock_table_single_row() {
let mut mock = MockTable::new("users");
let mut row = BTreeMap::new();
row.insert("id".to_string(), "1".to_string());
row.insert("name".to_string(), "Alice".to_string());
mock.add_row(row);
let sql = mock.to_sql();
assert_eq!(sql, "SELECT '1' AS id, 'Alice' AS name");
}
#[test]
fn test_mock_table_multiple_rows() {
let mut mock = MockTable::new("users");
let mut row1 = BTreeMap::new();
row1.insert("id".to_string(), "1".to_string());
row1.insert("name".to_string(), "Alice".to_string());
mock.add_row(row1);
let mut row2 = BTreeMap::new();
row2.insert("id".to_string(), "2".to_string());
row2.insert("name".to_string(), "Bob".to_string());
mock.add_row(row2);
let sql = mock.to_sql();
assert_eq!(
sql,
"SELECT '1' AS id, 'Alice' AS name\nUNION ALL\nSELECT '2', 'Bob'"
);
}
#[test]
fn test_mock_table_with_null() {
let mut mock = MockTable::new("users");
let mut row = BTreeMap::new();
row.insert("id".to_string(), "1".to_string());
row.insert("email".to_string(), "NULL".to_string());
mock.add_row(row);
let sql = mock.to_sql();
assert!(sql.contains("NULL AS email"));
assert!(!sql.contains("'NULL'"));
}
#[test]
fn test_mock_table_empty() {
let mock = MockTable::new("users");
let sql = mock.to_sql();
assert!(sql.contains("WHERE 1=0"));
}
#[test]
fn test_mock_table_sql_injection_safe() {
let mut mock = MockTable::new("users");
let mut row = BTreeMap::new();
row.insert("name".to_string(), "O'Brien".to_string());
mock.add_row(row);
let sql = mock.to_sql();
assert!(sql.contains("O''Brien"));
}
#[test]
fn test_mock_rows_macro() {
let rows = mock_rows![
{"id" => "1", "name" => "Alice"},
{"id" => "2", "name" => "Bob"},
];
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].get("id").unwrap(), "1");
assert_eq!(rows[1].get("name").unwrap(), "Bob");
}
}