use super::type_def::{ToField, ToInsertRow, ToPatch, ToUpdateRow};
use sqlx::Arguments;
use std::fmt::{Debug, Display};
pub trait BindArgs {
fn bind(&self, args: &mut sqlx::postgres::PgArguments);
fn bind_len(&self) -> usize;
}
pub type AsSqlParts = (Vec<String>, Option<Vec<String>>);
pub trait ToSqlParts {
fn to_sql_parts(&self) -> AsSqlParts;
}
pub fn concat_sql_parts(parts: Vec<(Vec<String>, Option<Vec<String>>)>) -> AsSqlParts {
let mut cols = Vec::new();
let mut binds: Option<Vec<String>> = None;
for (c, b) in parts {
cols.extend(c);
if let Some(bv) = b {
binds.get_or_insert_with(Vec::new).extend(bv);
}
}
(cols, binds)
}
pub enum SqlStatement<I: ToInsertRow, U: ToUpdateRow, F: ToField, P: ToPatch> {
Select(Vec<F>),
InsertOne(I),
InsertMany(Vec<I>),
Update(U),
Patch(Vec<P>)
}
impl<I: ToInsertRow, U: ToUpdateRow, F: ToField, P: ToPatch> BindArgs for SqlStatement<I, U, F, P> {
fn bind(&self, args: &mut sqlx::postgres::PgArguments) {
match self {
Self::Select(v) => {
for ele in v {
let _ = args.add(ele.to_string());
}
}
Self::InsertOne(v) => v.bind(args),
Self::InsertMany(v) => v.iter().for_each(|f| f.bind(args)),
Self::Update(v) => v.bind(args),
Self::Patch(v) => v.iter().for_each(|f| f.bind(args))
}
}
fn bind_len(&self) -> usize {
match self {
Self::Update(v) => v.bind_len(),
Self::InsertOne(v) => v.bind_len(),
Self::Select(_) => 0,
Self::InsertMany(v) => v.iter().map(|v| v.bind_len()).sum(),
Self::Patch(v) => v.len()
}
}
}
impl<I: ToInsertRow, U: ToUpdateRow, F: ToField, P: ToPatch> Debug for SqlStatement<I, U, F, P> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SqlStatement::Select(_fields) => {
std::fmt::Result::Ok(())
}
SqlStatement::InsertOne(row) => std::fmt::Debug::fmt(&row, f),
SqlStatement::InsertMany(_row) => {
todo!()
}
SqlStatement::Update(row) => std::fmt::Debug::fmt(&row, f),
SqlStatement::Patch(fields) => {
for (i, field) in fields.iter().enumerate() {
write!(f, "\n\t${} = ", i + 1)?;
std::fmt::Debug::fmt(field, f)?;
}
std::fmt::Result::Ok(())
}
}
}
}
pub enum Filter {
Equals(i32),
NotEquals(i32),
In(Vec<i32>),
NotIn(Vec<i32>),
Like(String),
NotLike(String),
Ilike(String),
NotIlike(String),
StringIs(String),
StringIsNot(String),
Gt(i32),
Gte(i32),
Lt(i32),
Lte(i32),
IsNull
}
impl BindArgs for Filter {
fn bind(&self, args: &mut sqlx::postgres::PgArguments) {
let _ = match self {
Self::Equals(v) => args.add(v),
Self::NotEquals(v) => args.add(v),
Self::In(v) => args.add(v),
Self::NotIn(v) => args.add(v),
Self::Like(v) => args.add(v),
Self::NotLike(v) => args.add(v),
Self::Ilike(v) => args.add(v),
Self::NotIlike(v) => args.add(v),
Self::StringIs(v) => args.add(v.to_owned()),
Self::StringIsNot(v) => args.add(v),
Self::Gt(v) => args.add(v),
Self::Gte(v) => args.add(v),
Self::Lt(v) => args.add(v),
Self::Lte(v) => args.add(v),
Self::IsNull => Ok(())
};
}
fn bind_len(&self) -> usize {
match self {
Self::IsNull => 0,
_ => 1
}
}
}
impl std::fmt::Debug for Filter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Equals(v) => write!(f, "{:?}", v),
Self::NotEquals(v) => write!(f, "{:?}", v),
Self::In(v) => write!(f, "{:?}", v),
Self::NotIn(v) => write!(f, "{:?}", v),
Self::Like(v) => write!(f, "{:?}", v),
Self::NotLike(v) => write!(f, "{:?}", v),
Self::Ilike(v) => write!(f, "{:?}", v),
Self::NotIlike(v) => write!(f, "{:?}", v),
Self::StringIs(v) => write!(f, "{:?}", v),
Self::StringIsNot(v) => write!(f, "{:?}", v),
Self::Gt(v) => write!(f, "{:?}", v),
Self::Gte(v) => write!(f, "{:?}", v),
Self::Lt(v) => write!(f, "{:?}", v),
Self::Lte(v) => write!(f, "{:?}", v),
Self::IsNull => Ok(())
}
}
}
impl Display for Filter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Filter::Equals(_) => write!(f, "="),
Filter::NotEquals(_) => write!(f, "!="),
Filter::In(_) => write!(f, "IN"),
Filter::NotIn(_) => write!(f, "NOT IN"),
Filter::Like(_) => write!(f, "LIKE"),
Filter::NotLike(_) => write!(f, "NOT LIKE"),
Filter::Ilike(_) => write!(f, "ILIKE"),
Filter::NotIlike(_) => write!(f, "NOT ILIKE"),
Filter::StringIs(_) => write!(f, "="),
Filter::StringIsNot(_) => write!(f, "!="),
Filter::Gt(_) => write!(f, ">"),
Filter::Gte(_) => write!(f, ">="),
Filter::Lt(_) => write!(f, "<"),
Filter::Lte(_) => write!(f, "<="),
Filter::IsNull => write!(f, "IS NULL")
}
}
}
pub enum FilterOp<F: ToField> {
And(F, Filter),
Or(F, Filter),
Begin(F, Filter)
}
impl<F: ToField> Display for FilterOp<F> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FilterOp::Begin(field, cond) => write!(f, "{} {}", field, cond),
FilterOp::And(field, cond) => write!(f, "AND {} {}", field, cond),
FilterOp::Or(field, cond) => write!(f, "OR {} {}", field, cond)
}
}
}
impl<F: ToField> Debug for FilterOp<F> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FilterOp::Begin(_, cond) => write!(f, "{:?}", cond),
FilterOp::And(_, cond) => write!(f, "{:?}", cond),
FilterOp::Or(_, cond) => write!(f, "{:?}", cond)
}
}
}
impl<F: ToField> BindArgs for FilterOp<F> {
fn bind(&self, args: &mut sqlx::postgres::PgArguments) {
match self {
Self::Begin(_, w) => w.bind(args),
Self::And(_, w) => w.bind(args),
Self::Or(_, w) => w.bind(args)
}
}
fn bind_len(&self) -> usize {
match self {
Self::Begin(_, w) => w.bind_len(),
Self::And(_, w) => w.bind_len(),
Self::Or(_, w) => w.bind_len()
}
}
}
pub fn sql_where<F: ToField>(
w: &[FilterOp<F>],
idx: usize,
from_update_patch: Option<String>
) -> String {
let update_batch_ref_table = match from_update_patch {
Some(t) => format!("{t}."),
None => "".into()
};
let mut f_idx = 0;
let whr = w
.iter()
.map(|f| {
let (kw, field_str) = match f {
FilterOp::Begin(c, _) => ("", format!("{c}")),
FilterOp::And(c, _) => ("AND ", format!("{c}")),
FilterOp::Or(c, _) => ("OR ", format!("{c}"))
};
let filter_val = match f {
FilterOp::Begin(_, v) | FilterOp::And(_, v) | FilterOp::Or(_, v) => v
};
match filter_val {
Filter::IsNull => {
format!("\n\t{}{}{} IS NULL", kw, update_batch_ref_table, field_str)
}
v => {
f_idx += f.bind_len();
format!(
"\n\t{}{}{} {} ${}",
kw,
update_batch_ref_table,
field_str,
v,
f_idx + idx
)
}
}
})
.collect::<Vec<_>>()
.join(" ");
if !whr.is_empty() {
return format!("\nWHERE{}", whr);
}
whr
}
#[cfg(all(test, feature = "test-utils"))]
mod tests {
use super::*;
use crate::repo::filter::{Filter, FilterOp};
use crate::testing::must::must_be_true;
#[derive(Clone, Copy)]
enum TestField {
Name,
Age
}
impl Display for TestField {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TestField::Name => write!(f, "name"),
TestField::Age => write!(f, "age")
}
}
}
impl ToSqlParts for TestField {
fn to_sql_parts(&self) -> AsSqlParts {
(vec![format!("{}", self)], None)
}
}
#[test]
fn sql_where_alias_precedes_keyword_not_field() {
let filters = vec![
FilterOp::Begin(TestField::Name, Filter::Equals(1)),
FilterOp::And(TestField::Age, Filter::Equals(30)),
];
let sql = sql_where(&filters, 0, Some("_x_".into()));
must_be_true(sql.contains("AND _x_.age"));
must_be_true(!sql.contains("_x_.AND"));
}
#[test]
fn sql_where_or_alias_precedes_keyword_not_field() {
let filters = vec![
FilterOp::Begin(TestField::Name, Filter::Equals(1)),
FilterOp::Or(TestField::Age, Filter::Equals(30)),
];
let sql = sql_where(&filters, 0, Some("_x_".into()));
must_be_true(sql.contains("OR _x_.age"));
must_be_true(!sql.contains("_x_.OR"));
}
#[test]
fn sql_where_begin_with_alias() {
let filters = vec![FilterOp::Begin(TestField::Name, Filter::Equals(1))];
let sql = sql_where(&filters, 0, Some("_x_".into()));
must_be_true(sql.contains("_x_.name"));
}
#[test]
fn sql_where_is_null_with_alias_and_keyword() {
let filters = vec![
FilterOp::Begin(TestField::Name, Filter::Equals(1)),
FilterOp::And(TestField::Age, Filter::IsNull),
];
let sql = sql_where(&filters, 0, Some("_x_".into()));
must_be_true(sql.contains("AND _x_.age IS NULL"));
must_be_true(!sql.contains("_x_.AND"));
}
#[test]
fn sql_where_no_alias_and_keyword() {
let filters = vec![
FilterOp::Begin(TestField::Name, Filter::Equals(1)),
FilterOp::And(TestField::Age, Filter::Equals(30)),
];
let sql = sql_where(&filters, 0, None);
must_be_true(sql.contains("AND age"));
}
}