use chrono::NaiveDateTime;
use serde_json::Value as JsonValue;
#[derive(Debug, Clone)]
pub enum SqlValue {
Null,
Bool(bool),
Int(i64),
Float(f64),
String(String),
Bytes(Vec<u8>),
Json(JsonValue),
DateTime(NaiveDateTime),
Timestamp(i64),
}
#[derive(Debug, Clone)]
pub enum Condition {
Eq(String, SqlValue),
Ne(String, SqlValue),
Gt(String, SqlValue),
Lt(String, SqlValue),
Gte(String, SqlValue),
Lte(String, SqlValue),
In(String, Vec<SqlValue>),
Between(String, SqlValue, SqlValue),
Like(String, String),
And(Vec<Condition>),
Or(Vec<Condition>),
}
impl From<i32> for SqlValue {
fn from(v: i32) -> Self {
SqlValue::Int(v as i64)
}
}
impl From<i64> for SqlValue {
fn from(v: i64) -> Self {
SqlValue::Int(v)
}
}
impl From<u64> for SqlValue {
fn from(v: u64) -> Self {
SqlValue::Int(v as i64)
}
}
impl From<f64> for SqlValue {
fn from(v: f64) -> Self {
SqlValue::Float(v)
}
}
impl From<f32> for SqlValue {
fn from(v: f32) -> Self {
SqlValue::Float(v as f64)
}
}
impl From<String> for SqlValue {
fn from(v: String) -> Self {
SqlValue::String(v)
}
}
impl From<&str> for SqlValue {
fn from(v: &str) -> Self {
SqlValue::String(v.to_string())
}
}
impl From<bool> for SqlValue {
fn from(v: bool) -> Self {
SqlValue::Bool(v)
}
}
impl From<Vec<u8>> for SqlValue {
fn from(v: Vec<u8>) -> Self {
SqlValue::Bytes(v)
}
}
impl From<JsonValue> for SqlValue {
fn from(v: JsonValue) -> Self {
SqlValue::Json(v)
}
}
impl From<NaiveDateTime> for SqlValue {
fn from(v: NaiveDateTime) -> Self {
SqlValue::DateTime(v)
}
}
impl<T> From<Option<T>> for SqlValue
where
T: Into<SqlValue>,
{
fn from(v: Option<T>) -> Self {
match v {
Some(val) => val.into(),
None => SqlValue::Null,
}
}
}
pub fn condition_to_sql(condition: &Condition, params: &mut Vec<SqlValue>) -> String {
match condition {
Condition::Eq(field, value) => {
params.push(value.clone());
format!("{} = ?", field)
}
Condition::Ne(field, value) => {
params.push(value.clone());
format!("{} != ?", field)
}
Condition::Gt(field, value) => {
params.push(value.clone());
format!("{} > ?", field)
}
Condition::Lt(field, value) => {
params.push(value.clone());
format!("{} < ?", field)
}
Condition::Gte(field, value) => {
params.push(value.clone());
format!("{} >= ?", field)
}
Condition::Lte(field, value) => {
params.push(value.clone());
format!("{} <= ?", field)
}
Condition::In(field, values) => {
if values.is_empty() {
return "1 = 0".to_string();
}
let placeholders = values
.iter()
.map(|v| {
params.push(v.clone());
"?"
})
.collect::<Vec<_>>()
.join(", ");
format!("{} IN ({})", field, placeholders)
}
Condition::Between(field, start, end) => {
params.push(start.clone());
params.push(end.clone());
format!("{} BETWEEN ? AND ?", field)
}
Condition::Like(field, pattern) => {
params.push(SqlValue::String(pattern.clone()));
format!("{} LIKE ?", field)
}
Condition::And(conditions) => {
if conditions.is_empty() {
return "1 = 1".to_string();
}
if conditions.len() == 1 {
return condition_to_sql(&conditions[0], params);
}
let parts: Vec<String> = conditions
.iter()
.map(|c| condition_to_sql(c, params))
.collect();
format!("({})", parts.join(" AND "))
}
Condition::Or(conditions) => {
if conditions.is_empty() {
return "1 = 0".to_string();
}
if conditions.len() == 1 {
return condition_to_sql(&conditions[0], params);
}
let parts: Vec<String> = conditions
.iter()
.map(|c| condition_to_sql(c, params))
.collect();
format!("({})", parts.join(" OR "))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::NaiveDate;
#[test]
fn test_from_i32() {
let value: SqlValue = 42i32.into();
match value {
SqlValue::Int(v) => assert_eq!(v, 42),
_ => panic!("期望 SqlValue::Int"),
}
}
#[test]
fn test_from_i64() {
let value: SqlValue = 9223372036854775807i64.into();
match value {
SqlValue::Int(v) => assert_eq!(v, 9223372036854775807),
_ => panic!("期望 SqlValue::Int"),
}
}
#[test]
fn test_from_f32() {
let value: SqlValue = 3.5f32.into();
match value {
SqlValue::Float(v) => assert!((v - 3.5).abs() < 0.01),
_ => panic!("期望 SqlValue::Float"),
}
}
#[test]
fn test_from_f64() {
let value: SqlValue = 2.5f64.into();
match value {
SqlValue::Float(v) => assert!((v - 2.5).abs() < 0.000001),
_ => panic!("期望 SqlValue::Float"),
}
}
#[test]
fn test_from_string() {
let value: SqlValue = String::from("测试字符串").into();
match value {
SqlValue::String(s) => assert_eq!(s, "测试字符串"),
_ => panic!("期望 SqlValue::String"),
}
}
#[test]
fn test_from_str() {
let value: SqlValue = "hello world".into();
match value {
SqlValue::String(s) => assert_eq!(s, "hello world"),
_ => panic!("期望 SqlValue::String"),
}
}
#[test]
fn test_from_bool_true() {
let value: SqlValue = true.into();
match value {
SqlValue::Bool(b) => assert!(b),
_ => panic!("期望 SqlValue::Bool"),
}
}
#[test]
fn test_from_bool_false() {
let value: SqlValue = false.into();
match value {
SqlValue::Bool(b) => assert!(!b),
_ => panic!("期望 SqlValue::Bool"),
}
}
#[test]
fn test_from_vec_u8() {
let bytes = vec![0x48, 0x65, 0x6c, 0x6c, 0x6f]; let value: SqlValue = bytes.clone().into();
match value {
SqlValue::Bytes(b) => assert_eq!(b, bytes),
_ => panic!("期望 SqlValue::Bytes"),
}
}
#[test]
fn test_from_json_value() {
let json = serde_json::json!({
"name": "测试",
"age": 25,
"active": true
});
let value: SqlValue = json.clone().into();
match value {
SqlValue::Json(j) => assert_eq!(j, json),
_ => panic!("期望 SqlValue::Json"),
}
}
#[test]
fn test_from_naive_datetime() {
let dt = NaiveDate::from_ymd_opt(2024, 1, 15)
.unwrap()
.and_hms_opt(10, 30, 45)
.unwrap();
let value: SqlValue = dt.into();
match value {
SqlValue::DateTime(d) => assert_eq!(d, dt),
_ => panic!("期望 SqlValue::DateTime"),
}
}
#[test]
fn test_from_option_some() {
let value: SqlValue = Some(42i32).into();
match value {
SqlValue::Int(v) => assert_eq!(v, 42),
_ => panic!("期望 SqlValue::Int"),
}
}
#[test]
fn test_from_option_none() {
let value: SqlValue = None::<i32>.into();
match value {
SqlValue::Null => (),
_ => panic!("期望 SqlValue::Null"),
}
}
#[test]
fn test_from_option_string_some() {
let value: SqlValue = Some(String::from("测试")).into();
match value {
SqlValue::String(s) => assert_eq!(s, "测试"),
_ => panic!("期望 SqlValue::String"),
}
}
#[test]
fn test_from_option_string_none() {
let value: SqlValue = None::<String>.into();
match value {
SqlValue::Null => (),
_ => panic!("期望 SqlValue::Null"),
}
}
#[test]
fn test_negative_integers() {
let value: SqlValue = (-100i32).into();
match value {
SqlValue::Int(v) => assert_eq!(v, -100),
_ => panic!("期望 SqlValue::Int"),
}
}
#[test]
fn test_negative_floats() {
let value: SqlValue = (-3.5f64).into();
match value {
SqlValue::Float(v) => assert!((v + 3.5).abs() < 0.01),
_ => panic!("期望 SqlValue::Float"),
}
}
#[test]
fn test_empty_string() {
let value: SqlValue = "".into();
match value {
SqlValue::String(s) => assert_eq!(s, ""),
_ => panic!("期望 SqlValue::String"),
}
}
#[test]
fn test_empty_bytes() {
let value: SqlValue = Vec::<u8>::new().into();
match value {
SqlValue::Bytes(b) => assert!(b.is_empty()),
_ => panic!("期望 SqlValue::Bytes"),
}
}
#[test]
fn test_json_null() {
let json = serde_json::Value::Null;
let value: SqlValue = json.into();
match value {
SqlValue::Json(j) => assert!(j.is_null()),
_ => panic!("期望 SqlValue::Json"),
}
}
#[test]
fn test_json_array() {
let json = serde_json::json!([1, 2, 3, 4, 5]);
let value: SqlValue = json.clone().into();
match value {
SqlValue::Json(j) => assert_eq!(j, json),
_ => panic!("期望 SqlValue::Json"),
}
}
#[test]
fn test_unicode_string() {
let value: SqlValue = "你好世界 🌍".into();
match value {
SqlValue::String(s) => assert_eq!(s, "你好世界 🌍"),
_ => panic!("期望 SqlValue::String"),
}
}
#[test]
fn test_zero_values() {
let int_value: SqlValue = 0i32.into();
match int_value {
SqlValue::Int(v) => assert_eq!(v, 0),
_ => panic!("期望 SqlValue::Int"),
}
let float_value: SqlValue = 0.0f64.into();
match float_value {
SqlValue::Float(v) => assert_eq!(v, 0.0),
_ => panic!("期望 SqlValue::Float"),
}
}
#[test]
fn test_condition_eq() {
let mut params = Vec::new();
let cond = Condition::Eq("name".to_string(), SqlValue::String("test".to_string()));
let sql = super::condition_to_sql(&cond, &mut params);
assert_eq!(sql, "name = ?");
assert_eq!(params.len(), 1);
}
#[test]
fn test_condition_ne() {
let mut params = Vec::new();
let cond = Condition::Ne("status".to_string(), SqlValue::Int(1));
let sql = super::condition_to_sql(&cond, &mut params);
assert_eq!(sql, "status != ?");
assert_eq!(params.len(), 1);
}
#[test]
fn test_condition_gt() {
let mut params = Vec::new();
let cond = Condition::Gt("age".to_string(), SqlValue::Int(18));
let sql = super::condition_to_sql(&cond, &mut params);
assert_eq!(sql, "age > ?");
assert_eq!(params.len(), 1);
}
#[test]
fn test_condition_lt() {
let mut params = Vec::new();
let cond = Condition::Lt("price".to_string(), SqlValue::Float(100.0));
let sql = super::condition_to_sql(&cond, &mut params);
assert_eq!(sql, "price < ?");
assert_eq!(params.len(), 1);
}
#[test]
fn test_condition_gte() {
let mut params = Vec::new();
let cond = Condition::Gte("score".to_string(), SqlValue::Int(60));
let sql = super::condition_to_sql(&cond, &mut params);
assert_eq!(sql, "score >= ?");
assert_eq!(params.len(), 1);
}
#[test]
fn test_condition_lte() {
let mut params = Vec::new();
let cond = Condition::Lte("count".to_string(), SqlValue::Int(10));
let sql = super::condition_to_sql(&cond, &mut params);
assert_eq!(sql, "count <= ?");
assert_eq!(params.len(), 1);
}
#[test]
fn test_condition_in() {
let mut params = Vec::new();
let cond = Condition::In(
"id".to_string(),
vec![SqlValue::Int(1), SqlValue::Int(2), SqlValue::Int(3)],
);
let sql = super::condition_to_sql(&cond, &mut params);
assert_eq!(sql, "id IN (?, ?, ?)");
assert_eq!(params.len(), 3);
}
#[test]
fn test_condition_in_empty() {
let mut params = Vec::new();
let cond = Condition::In("id".to_string(), vec![]);
let sql = super::condition_to_sql(&cond, &mut params);
assert_eq!(sql, "1 = 0");
assert_eq!(params.len(), 0);
}
#[test]
fn test_condition_between() {
let mut params = Vec::new();
let cond = Condition::Between("age".to_string(), SqlValue::Int(18), SqlValue::Int(65));
let sql = super::condition_to_sql(&cond, &mut params);
assert_eq!(sql, "age BETWEEN ? AND ?");
assert_eq!(params.len(), 2);
}
#[test]
fn test_condition_like() {
let mut params = Vec::new();
let cond = Condition::Like("name".to_string(), "%test%".to_string());
let sql = super::condition_to_sql(&cond, &mut params);
assert_eq!(sql, "name LIKE ?");
assert_eq!(params.len(), 1);
}
#[test]
fn test_condition_and() {
let mut params = Vec::new();
let cond = Condition::And(vec![
Condition::Eq("name".to_string(), SqlValue::String("test".to_string())),
Condition::Gt("age".to_string(), SqlValue::Int(18)),
]);
let sql = super::condition_to_sql(&cond, &mut params);
assert_eq!(sql, "(name = ? AND age > ?)");
assert_eq!(params.len(), 2);
}
#[test]
fn test_condition_or() {
let mut params = Vec::new();
let cond = Condition::Or(vec![
Condition::Eq("status".to_string(), SqlValue::Int(1)),
Condition::Eq("status".to_string(), SqlValue::Int(2)),
]);
let sql = super::condition_to_sql(&cond, &mut params);
assert_eq!(sql, "(status = ? OR status = ?)");
assert_eq!(params.len(), 2);
}
#[test]
fn test_condition_and_or_priority() {
let mut params = Vec::new();
let cond = Condition::And(vec![
Condition::Or(vec![
Condition::Eq("name".to_string(), SqlValue::String("test".to_string())),
Condition::Eq("name".to_string(), SqlValue::String("demo".to_string())),
]),
Condition::Gt("age".to_string(), SqlValue::Int(18)),
]);
let sql = super::condition_to_sql(&cond, &mut params);
assert_eq!(sql, "((name = ? OR name = ?) AND age > ?)");
assert_eq!(params.len(), 3);
}
#[test]
fn test_condition_empty_and() {
let mut params = Vec::new();
let cond = Condition::And(vec![]);
let sql = super::condition_to_sql(&cond, &mut params);
assert_eq!(sql, "1 = 1");
assert_eq!(params.len(), 0);
}
#[test]
fn test_condition_empty_or() {
let mut params = Vec::new();
let cond = Condition::Or(vec![]);
let sql = super::condition_to_sql(&cond, &mut params);
assert_eq!(sql, "1 = 0");
assert_eq!(params.len(), 0);
}
#[test]
fn test_condition_single_and() {
let mut params = Vec::new();
let cond = Condition::And(vec![Condition::Eq("id".to_string(), SqlValue::Int(1))]);
let sql = super::condition_to_sql(&cond, &mut params);
assert_eq!(sql, "id = ?");
assert_eq!(params.len(), 1);
}
#[test]
fn test_condition_single_or() {
let mut params = Vec::new();
let cond = Condition::Or(vec![Condition::Eq("id".to_string(), SqlValue::Int(1))]);
let sql = super::condition_to_sql(&cond, &mut params);
assert_eq!(sql, "id = ?");
assert_eq!(params.len(), 1);
}
}
#[cfg(test)]
mod property_tests {
use super::*;
use proptest::prelude::*;
fn field_name_strategy() -> impl Strategy<Value = String> {
"[a-z][a-z0-9_]{0,30}"
}
fn sql_value_strategy() -> impl Strategy<Value = SqlValue> {
prop_oneof![
Just(SqlValue::Null),
any::<bool>().prop_map(SqlValue::Bool),
any::<i64>().prop_map(SqlValue::Int),
any::<f64>().prop_map(|f| {
if f.is_finite() {
SqlValue::Float(f)
} else {
SqlValue::Float(0.0)
}
}),
"[a-zA-Z0-9_\\s]{0,50}".prop_map(SqlValue::String),
]
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_operator_eq_support(
field in field_name_strategy(),
value in sql_value_strategy()
) {
let mut params = Vec::new();
let cond = Condition::Eq(field.clone(), value);
let sql = condition_to_sql(&cond, &mut params);
let expected = format!("{} = {{placeholder}}", field).replace("{placeholder}", "?");
prop_assert!(sql.contains(&expected));
prop_assert_eq!(params.len(), 1);
}
#[test]
fn prop_operator_ne_support(
field in field_name_strategy(),
value in sql_value_strategy()
) {
let mut params = Vec::new();
let cond = Condition::Ne(field.clone(), value);
let sql = condition_to_sql(&cond, &mut params);
let expected = format!("{} != {{placeholder}}", field).replace("{placeholder}", "?");
prop_assert!(sql.contains(&expected));
prop_assert_eq!(params.len(), 1);
}
#[test]
fn prop_operator_gt_support(
field in field_name_strategy(),
value in sql_value_strategy()
) {
let mut params = Vec::new();
let cond = Condition::Gt(field.clone(), value);
let sql = condition_to_sql(&cond, &mut params);
let expected = format!("{} > {{placeholder}}", field).replace("{placeholder}", "?");
prop_assert!(sql.contains(&expected));
prop_assert_eq!(params.len(), 1);
}
#[test]
fn prop_operator_lt_support(
field in field_name_strategy(),
value in sql_value_strategy()
) {
let mut params = Vec::new();
let cond = Condition::Lt(field.clone(), value);
let sql = condition_to_sql(&cond, &mut params);
let expected = format!("{} < {{placeholder}}", field).replace("{placeholder}", "?");
prop_assert!(sql.contains(&expected));
prop_assert_eq!(params.len(), 1);
}
#[test]
fn prop_operator_gte_support(
field in field_name_strategy(),
value in sql_value_strategy()
) {
let mut params = Vec::new();
let cond = Condition::Gte(field.clone(), value);
let sql = condition_to_sql(&cond, &mut params);
let expected = format!("{} >= {{placeholder}}", field).replace("{placeholder}", "?");
prop_assert!(sql.contains(&expected));
prop_assert_eq!(params.len(), 1);
}
#[test]
fn prop_operator_lte_support(
field in field_name_strategy(),
value in sql_value_strategy()
) {
let mut params = Vec::new();
let cond = Condition::Lte(field.clone(), value);
let sql = condition_to_sql(&cond, &mut params);
let expected = format!("{} <= {{placeholder}}", field).replace("{placeholder}", "?");
prop_assert!(sql.contains(&expected));
prop_assert_eq!(params.len(), 1);
}
#[test]
fn prop_operator_in_support(
field in field_name_strategy(),
values in prop::collection::vec(sql_value_strategy(), 1..10)
) {
let mut params = Vec::new();
let values_len = values.len();
let cond = Condition::In(field.clone(), values);
let sql = condition_to_sql(&cond, &mut params);
let expected = format!("{} IN", field);
prop_assert!(sql.contains(&expected));
prop_assert_eq!(params.len(), values_len);
}
#[test]
fn prop_operator_between_support(
field in field_name_strategy(),
start in sql_value_strategy(),
end in sql_value_strategy()
) {
let mut params = Vec::new();
let cond = Condition::Between(field.clone(), start, end);
let sql = condition_to_sql(&cond, &mut params);
let expected = format!("{} BETWEEN {{p1}} AND {{p2}}", field)
.replace("{p1}", "?")
.replace("{p2}", "?");
prop_assert!(sql.contains(&expected));
prop_assert_eq!(params.len(), 2);
}
#[test]
fn prop_operator_like_support(
field in field_name_strategy(),
pattern in "[a-zA-Z0-9_%]{1,20}"
) {
let mut params = Vec::new();
let cond = Condition::Like(field.clone(), pattern);
let sql = condition_to_sql(&cond, &mut params);
let expected = format!("{} LIKE {{placeholder}}", field).replace("{placeholder}", "?");
prop_assert!(sql.contains(&expected));
prop_assert_eq!(params.len(), 1);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_and_or_priority_handling(
field1 in field_name_strategy(),
field2 in field_name_strategy(),
value1 in sql_value_strategy(),
value2 in sql_value_strategy(),
value3 in sql_value_strategy()
) {
let mut params = Vec::new();
let cond = Condition::And(vec![
Condition::Or(vec![
Condition::Eq(field1.clone(), value1),
Condition::Eq(field1.clone(), value2),
]),
Condition::Eq(field2.clone(), value3),
]);
let sql = condition_to_sql(&cond, &mut params);
prop_assert!(sql.starts_with('('));
prop_assert!(sql.ends_with(')'));
prop_assert!(sql.contains(" OR "));
prop_assert!(sql.contains(" AND "));
prop_assert_eq!(params.len(), 3);
}
#[test]
fn prop_nested_and_or_brackets(
field in field_name_strategy(),
values in prop::collection::vec(sql_value_strategy(), 2..5)
) {
let mut params = Vec::new();
let or_conditions: Vec<Condition> = values
.iter()
.map(|v| Condition::Eq(field.clone(), v.clone()))
.collect();
let cond = Condition::And(vec![
Condition::Or(or_conditions.clone()),
Condition::Gt(field.clone(), SqlValue::Int(0)),
]);
let sql = condition_to_sql(&cond, &mut params);
let open_count = sql.chars().filter(|&c| c == '(').count();
let close_count = sql.chars().filter(|&c| c == ')').count();
prop_assert_eq!(open_count, close_count);
prop_assert_eq!(params.len(), values.len() + 1);
}
#[test]
fn prop_multiple_and_conditions(
field in field_name_strategy(),
values in prop::collection::vec(sql_value_strategy(), 2..5)
) {
let mut params = Vec::new();
let and_conditions: Vec<Condition> = values
.iter()
.enumerate()
.map(|(i, v)| {
if i % 2 == 0 {
Condition::Eq(field.clone(), v.clone())
} else {
Condition::Ne(field.clone(), v.clone())
}
})
.collect();
let cond = Condition::And(and_conditions);
let sql = condition_to_sql(&cond, &mut params);
let and_count = sql.matches(" AND ").count();
prop_assert_eq!(and_count, values.len() - 1);
prop_assert_eq!(params.len(), values.len());
}
#[test]
fn prop_or_conditions_always_bracketed(
field in field_name_strategy(),
values in prop::collection::vec(sql_value_strategy(), 2..5)
) {
let mut params = Vec::new();
let or_conditions: Vec<Condition> = values
.iter()
.map(|v| Condition::Eq(field.clone(), v.clone()))
.collect();
let cond = Condition::Or(or_conditions);
let sql = condition_to_sql(&cond, &mut params);
prop_assert!(sql.starts_with('('));
prop_assert!(sql.ends_with(')'));
let or_count = sql.matches(" OR ").count();
prop_assert_eq!(or_count, values.len() - 1);
}
}
}