sqlx-query-dsl 0.1.1

A query DSL extension for SQLx
Documentation

use sqlx::{MySql, QueryBuilder};
use crate::params::{FilterExpr, Op, ExistsValue};
use crate::whitelist::FieldWhitelist;

/// 递归构建过滤条件的 SQL WHERE 子句
///
/// # Arguments
/// * `qb` - sqlx 的 QueryBuilder,用于追加 SQL 片段和绑定参数
/// * `expr` - 过滤表达式,定义了具体的过滤逻辑(支持嵌套)
/// * `whitelist` - 字段白名单,用于校验字段名是否合法,防止 SQL 注入
pub fn build_filter<'a>(
    qb: &mut QueryBuilder<'a, MySql>,
    expr: &'a FilterExpr,
    whitelist: &FieldWhitelist,
) -> Result<(), String> {
    match expr {
        FilterExpr::And { and } => {
            qb.push("(");
            for (i, e) in and.iter().enumerate() {
                if i > 0 { qb.push(" AND "); }
                build_filter(qb, e, whitelist)?;
            }
            qb.push(")");
        }
        FilterExpr::Or { or } => {
            qb.push("(");
            for (i, e) in or.iter().enumerate() {
                if i > 0 { qb.push(" OR "); }
                build_filter(qb, e, whitelist)?;
            }
            qb.push(")");
        }
        FilterExpr::Cond(c) => match c.op {
            Op::IsNull => {
                let f = c.field.as_ref().unwrap();
                whitelist.check(f)?;
                qb.push(f).push(" IS NULL");
            }
            Op::IsNotNull => {
                let f = c.field.as_ref().unwrap();
                whitelist.check(f)?;
                qb.push(f).push(" IS NOT NULL");
            }
            Op::Exists | Op::NotExists => {
                let v: ExistsValue =
                    serde_json::from_value(c.value.clone().unwrap()).unwrap();
                qb.push(if matches!(c.op, Op::Exists) { "EXISTS (" } else { "NOT EXISTS (" });
                qb.push(&v.sql);
                if let Some(bind) = v.bind {
                    for b in bind {
                        qb.push_bind(b);
                    }
                }
                qb.push(")");
            }
            _ => {
                let f = c.field.as_ref().unwrap();
                whitelist.check(f)?;
                qb.push(f);
                match c.op {
                    Op::Eq => { qb.push(" = "); qb.push_bind(c.value.as_ref().unwrap()); }
                    Op::Ne => { qb.push(" != "); qb.push_bind(c.value.as_ref().unwrap()); }
                    Op::Like => {
                        qb.push(" LIKE ");
                        qb.push_bind(format!("%{}%", c.value.as_ref().unwrap().as_str().unwrap()));
                    }
                    Op::Gt => { qb.push(" > "); qb.push_bind(c.value.as_ref().unwrap()); }
                    Op::Lt => { qb.push(" < "); qb.push_bind(c.value.as_ref().unwrap()); }
                    Op::Gte => { qb.push(" >= "); qb.push_bind(c.value.as_ref().unwrap()); }
                    Op::Lte => { qb.push(" <= "); qb.push_bind(c.value.as_ref().unwrap()); }
                    Op::In | Op::NotIn => {
                        let arr = c.value.as_ref().unwrap().as_array().unwrap();
                        qb.push(if matches!(c.op, Op::In) { " IN (" } else { " NOT IN (" });
                        for (i, v) in arr.iter().enumerate() {
                            if i > 0 { qb.push(", "); }
                            qb.push_bind(v);
                        }
                        qb.push(")");
                    }
                    Op::Between => {
                        let arr = c.value.as_ref().unwrap().as_array().unwrap();
                        qb.push(" BETWEEN ");
                        qb.push_bind(&arr[0]);
                        qb.push(" AND ");
                        qb.push_bind(&arr[1]);
                    }
                    Op::FindInSet => {
                        qb.push(" FIND_IN_SET(");
                        qb.push_bind(c.value.as_ref().unwrap());
                        qb.push(", ").push(f).push(")");
                    }
                    Op::JsonContains => {
                        qb.push(" JSON_CONTAINS(");
                        qb.push(f).push(", ");
                        qb.push_bind(c.value.as_ref().unwrap().to_string());
                        qb.push(")");
                    }
                    _ => {}
                }
            }
        },
    }
    Ok(())
}