Updated query builder with bindings
This commit is contained in:
@@ -1,9 +1,14 @@
|
||||
use std::fmt;
|
||||
use std::fmt::Display;
|
||||
use sqlx::{FromRow, Postgres};
|
||||
|
||||
pub struct QueryBuilder {
|
||||
table: String,
|
||||
columns: Vec<String>,
|
||||
condition: Option<Condition>,
|
||||
order_by: Vec<String>,
|
||||
limit: Option<usize>,
|
||||
offset: Option<usize>,
|
||||
}
|
||||
|
||||
impl QueryBuilder {
|
||||
@@ -14,6 +19,7 @@ impl QueryBuilder {
|
||||
condition: None,
|
||||
order_by: Vec::new(),
|
||||
limit: None,
|
||||
offset: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +43,30 @@ impl QueryBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> String {
|
||||
pub async fn fetch_optional<
|
||||
T: Send + Unpin + for<'r> FromRow<'r, <Postgres as sqlx::Database>::Row>,
|
||||
>(
|
||||
self,
|
||||
) -> Option<T> {
|
||||
let (query_string, values) = self.build();
|
||||
let mut query_as = sqlx::query_as(&query_string);
|
||||
for value in values {
|
||||
match value {
|
||||
Value::INT(n) => query_as = query_as.bind(n),
|
||||
Value::BIGINT(n) => query_as = query_as.bind(n),
|
||||
Value::Bool(n) => query_as = query_as.bind(n),
|
||||
Value::Text(n) => query_as = query_as.bind(n),
|
||||
}
|
||||
}
|
||||
|
||||
let pool = crate::data::pool();
|
||||
query_as.fetch_optional(pool).await.unwrap_or_else(|err| {
|
||||
log::error!("{}", err);
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build(self) -> (String, Vec<Value>) {
|
||||
let columns = if self.columns.is_empty() {
|
||||
"*".to_string()
|
||||
} else {
|
||||
@@ -46,8 +75,11 @@ impl QueryBuilder {
|
||||
|
||||
let mut query = format!("SELECT {} FROM {}", columns, self.table);
|
||||
|
||||
let mut values: Vec<Value> = Vec::new();
|
||||
if let Some(condition) = self.condition {
|
||||
query.push_str(&format!(" WHERE {}", condition.to_sql()));
|
||||
let where_condition = condition.to_sql(&mut 0);
|
||||
query.push_str(&format!(" WHERE {}", where_condition.0));
|
||||
values = where_condition.1;
|
||||
}
|
||||
|
||||
if !self.order_by.is_empty() {
|
||||
@@ -58,12 +90,35 @@ impl QueryBuilder {
|
||||
query.push_str(&format!(" LIMIT {}", limit));
|
||||
}
|
||||
|
||||
query
|
||||
if let Some(offset) = self.offset {
|
||||
query.push_str(&format!(" OFFSET {}", offset));
|
||||
}
|
||||
|
||||
(query, values)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Value {
|
||||
INT(i32),
|
||||
BIGINT(i64),
|
||||
Bool(bool),
|
||||
Text(String),
|
||||
}
|
||||
|
||||
impl Display for Value {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Value::INT(n) => write!(f, "{}", n),
|
||||
Value::BIGINT(n) => write!(f, "{}", n),
|
||||
Value::Bool(n) => write!(f, "{}", n),
|
||||
Value::Text(s) => write!(f, "'{}'", s), // Wrap strings in quotes for SQL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Condition {
|
||||
Simple(String),
|
||||
Simple(String, Vec<Value>),
|
||||
And(Box<Condition>, Box<Condition>),
|
||||
Or(Box<Condition>, Box<Condition>),
|
||||
Group(Box<Condition>),
|
||||
@@ -71,7 +126,7 @@ pub enum Condition {
|
||||
|
||||
impl Condition {
|
||||
pub fn new(condition: &str) -> Self {
|
||||
Condition::Simple(condition.to_string())
|
||||
Condition::Simple(condition.to_string(), vec![])
|
||||
}
|
||||
|
||||
pub fn and(self, other: Self) -> Self {
|
||||
@@ -86,78 +141,108 @@ impl Condition {
|
||||
Condition::Group(Box::new(self))
|
||||
}
|
||||
|
||||
pub fn is_equal(left: &str, right: &str) -> Self {
|
||||
Condition::Simple(format!("{} = {}", left, right))
|
||||
pub fn is_equal(left: &str, right: impl Into<Value>) -> Self {
|
||||
Condition::Simple(format!("{} = ?", left), vec![right.into()])
|
||||
}
|
||||
|
||||
pub fn not_equal(left: &str, right: &str) -> Self {
|
||||
Condition::Simple(format!("{} != {}", left, right))
|
||||
pub fn not_equal(left: &str, right: impl Into<Value>) -> Self {
|
||||
Condition::Simple(format!("{} != ?", left), vec![right.into()])
|
||||
}
|
||||
|
||||
pub fn is_null(value: &str) -> Self {
|
||||
Condition::Simple(format!("{} IS NULL", value))
|
||||
Condition::Simple(format!("{} IS NULL", value), vec![])
|
||||
}
|
||||
|
||||
pub fn not_null(value: &str) -> Self {
|
||||
Condition::Simple(format!("{} IS NOT NULL", value))
|
||||
Condition::Simple(format!("{} IS NOT NULL", value), vec![])
|
||||
}
|
||||
|
||||
pub fn is_in(left: &str, right: &[&str]) -> Self {
|
||||
pub fn is_in(left: &str, right: Vec<Value>) -> Self {
|
||||
let right_list = right
|
||||
.iter()
|
||||
.map(|v| format!("'{}'", v))
|
||||
.map(|v| "'?'".to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
Condition::Simple(format!("{} IN ({})", left, right_list))
|
||||
Condition::Simple(format!("{} IN ({})", left, right_list), right)
|
||||
}
|
||||
|
||||
pub fn not_in(left: &str, right: &[&str]) -> Self {
|
||||
pub fn not_in(left: &str, right: Vec<Value>) -> Self {
|
||||
let right_list = right
|
||||
.iter()
|
||||
.map(|v| format!("'{}'", v))
|
||||
.map(|v| "'?'".to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
Condition::Simple(format!("{} NOT IN ({})", left, right_list))
|
||||
Condition::Simple(format!("{} NOT IN ({})", left, right_list), right)
|
||||
}
|
||||
|
||||
pub fn like(left: &str, right: &str) -> Self {
|
||||
Condition::Simple(format!("{} LIKE '{}'", left, right))
|
||||
pub fn like(left: &str, right: impl Into<Value>) -> Self {
|
||||
Condition::Simple(format!("{} LIKE '?'", left), vec![right.into()])
|
||||
}
|
||||
|
||||
pub fn not_like(left: &str, right: &str) -> Self {
|
||||
Condition::Simple(format!("{} NOT LIKE '{}'", left, right))
|
||||
pub fn not_like(left: &str, right: impl Into<Value>) -> Self {
|
||||
Condition::Simple(format!("{} NOT LIKE '?'", left), vec![right.into()])
|
||||
}
|
||||
|
||||
pub fn i_like(left: &str, right: &str) -> Self {
|
||||
Condition::Simple(format!("{} ILIKE '{}'", left, right))
|
||||
pub fn i_like(left: &str, right: impl Into<Value>) -> Self {
|
||||
Condition::Simple(format!("{} ILIKE '?'", left), vec![right.into()])
|
||||
}
|
||||
|
||||
pub fn not_i_like(left: &str, right: &str) -> Self {
|
||||
Condition::Simple(format!("{} NOT ILIKE '{}'", left, right))
|
||||
pub fn not_i_like(left: &str, right: impl Into<Value>) -> Self {
|
||||
Condition::Simple(format!("{} NOT ILIKE '?'", left), vec![right.into()])
|
||||
}
|
||||
|
||||
pub fn gt(left: &str, right: &str) -> Self {
|
||||
Condition::Simple(format!("{} > {}", left, right))
|
||||
pub fn gt(left: &str, right: impl Into<Value>) -> Self {
|
||||
Condition::Simple(format!("{} > ?", left), vec![right.into()])
|
||||
}
|
||||
|
||||
pub fn gte(left: &str, right: &str) -> Self {
|
||||
Condition::Simple(format!("{} >= {}", left, right))
|
||||
pub fn gte(left: &str, right: impl Into<Value>) -> Self {
|
||||
Condition::Simple(format!("{} >= ?", left), vec![right.into()])
|
||||
}
|
||||
|
||||
pub fn lt(left: &str, right: &str) -> Self {
|
||||
Condition::Simple(format!("{} < {}", left, right))
|
||||
pub fn lt(left: &str, right: impl Into<Value>) -> Self {
|
||||
Condition::Simple(format!("{} < ?", left), vec![right.into()])
|
||||
}
|
||||
|
||||
pub fn lte(left: &str, right: &str) -> Self {
|
||||
Condition::Simple(format!("{} <= {}", left, right))
|
||||
pub fn lte(left: &str, right: impl Into<Value>) -> Self {
|
||||
Condition::Simple(format!("{} <= ?", left), vec![right.into()])
|
||||
}
|
||||
|
||||
fn to_sql(&self) -> String {
|
||||
fn to_sql(&self, mut counter: &mut usize) -> (String, Vec<Value>) {
|
||||
let mut sql = String::new();
|
||||
let mut binds = Vec::new();
|
||||
|
||||
match self {
|
||||
Condition::Simple(s) => s.to_string(),
|
||||
Condition::And(a, b) => format!("{} AND {}", a.to_sql(), b.to_sql()),
|
||||
Condition::Or(a, b) => format!("{} OR {}", a.to_sql(), b.to_sql()),
|
||||
Condition::Group(a) => format!("({})", a.to_sql()),
|
||||
}
|
||||
Condition::Simple(condition, values) => {
|
||||
// Replace all instances of '?' with an numbered bind
|
||||
let mut bind_index = *counter;
|
||||
let numbered_condition = condition.replace("?", {
|
||||
bind_index += 1;
|
||||
&format!("${}", bind_index)
|
||||
});
|
||||
sql.push_str(&numbered_condition);
|
||||
binds.extend(values.clone());
|
||||
}
|
||||
Condition::And(left, right) => {
|
||||
let (left_sql, left_binds) = left.to_sql(counter);
|
||||
let (right_sql, right_binds) = right.to_sql(counter);
|
||||
sql.push_str(&format!("{} AND {}", left_sql, right_sql));
|
||||
binds.extend(left_binds);
|
||||
binds.extend(right_binds);
|
||||
}
|
||||
Condition::Or(left, right) => {
|
||||
let (left_sql, left_binds) = left.to_sql(counter);
|
||||
let (right_sql, right_binds) = right.to_sql(counter);
|
||||
sql.push_str(&format!("{} OR {}", left_sql, right_sql));
|
||||
binds.extend(left_binds);
|
||||
binds.extend(right_binds);
|
||||
}
|
||||
Condition::Group(inner) => {
|
||||
let (inner_sql, inner_binds) = inner.to_sql(counter);
|
||||
sql.push_str(&format!("({})", inner_sql));
|
||||
binds.extend(inner_binds);
|
||||
}
|
||||
};
|
||||
|
||||
(sql, binds)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user