use serde_json::Value as JsonValue;
#[derive(Debug, Clone)]
pub enum SqlValue {
Int(i64),
Float(f64),
Text(String),
Bool(bool),
Null,
Json(serde_json::Value),
Bytes(Vec<u8>),
}
impl From<i64> for SqlValue {
fn from(v: i64) -> Self {
SqlValue::Int(v)
}
}
impl From<i32> for SqlValue {
fn from(v: i32) -> Self {
SqlValue::Int(v as i64)
}
}
impl From<u32> for SqlValue {
fn from(v: u32) -> Self {
SqlValue::Int(v as i64)
}
}
impl From<f64> for SqlValue {
fn from(v: f64) -> Self {
SqlValue::Float(v)
}
}
impl From<String> for SqlValue {
fn from(v: String) -> Self {
SqlValue::Text(v)
}
}
impl From<&str> for SqlValue {
fn from(v: &str) -> Self {
SqlValue::Text(v.to_string())
}
}
impl From<bool> for SqlValue {
fn from(v: bool) -> Self {
SqlValue::Bool(v)
}
}
impl From<serde_json::Value> for SqlValue {
fn from(v: serde_json::Value) -> Self {
SqlValue::Json(v)
}
}
#[derive(Debug, Clone)]
pub struct FilterExpr {
pub sql: String,
pub bindings: Vec<SqlValue>,
}
impl FilterExpr {
pub fn new(sql: impl Into<String>, bindings: Vec<SqlValue>) -> Self {
Self {
sql: sql.into(),
bindings,
}
}
pub fn raw(sql: impl Into<String>) -> Self {
Self {
sql: sql.into(),
bindings: vec![],
}
}
pub fn and(self, other: FilterExpr) -> FilterExpr {
FilterExpr {
sql: format!("({}) AND ({})", self.sql, other.sql),
bindings: [self.bindings, other.bindings].concat(),
}
}
pub fn or(self, other: FilterExpr) -> FilterExpr {
FilterExpr {
sql: format!("({}) OR ({})", self.sql, other.sql),
bindings: [self.bindings, other.bindings].concat(),
}
}
pub fn not(self) -> FilterExpr {
FilterExpr {
sql: format!("NOT ({})", self.sql),
bindings: self.bindings,
}
}
}
pub fn reindex_params(sql: &str, offset: usize) -> String {
if offset == 0 {
return sql.to_string();
}
let mut out = String::with_capacity(sql.len() + 8);
let bytes = sql.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'$' {
let start = i + 1;
let mut end = start;
while end < bytes.len() && bytes[end].is_ascii_digit() {
end += 1;
}
if end > start {
let num: usize = sql[start..end].parse().unwrap_or(0);
out.push('$');
out.push_str(&(num + offset).to_string());
i = end;
continue;
}
}
out.push(bytes[i] as char);
i += 1;
}
out
}
#[derive(Debug, Clone)]
pub struct ColumnExpr {
pub col: String,
}
impl ColumnExpr {
pub fn new(col: impl Into<String>) -> Self {
Self { col: col.into() }
}
fn placeholder(&self, idx: usize) -> String {
format!("${}", idx)
}
pub fn eq<V: Into<SqlValue>>(&self, val: V) -> FilterExpr {
FilterExpr::new(format!("\"{}\" = $1", self.col), vec![val.into()])
}
pub fn ne<V: Into<SqlValue>>(&self, val: V) -> FilterExpr {
FilterExpr::new(format!("\"{}\" != $1", self.col), vec![val.into()])
}
pub fn gt<V: Into<SqlValue>>(&self, val: V) -> FilterExpr {
FilterExpr::new(format!("\"{}\" > $1", self.col), vec![val.into()])
}
pub fn gte<V: Into<SqlValue>>(&self, val: V) -> FilterExpr {
FilterExpr::new(format!("\"{}\" >= $1", self.col), vec![val.into()])
}
pub fn lt<V: Into<SqlValue>>(&self, val: V) -> FilterExpr {
FilterExpr::new(format!("\"{}\" < $1", self.col), vec![val.into()])
}
pub fn lte<V: Into<SqlValue>>(&self, val: V) -> FilterExpr {
FilterExpr::new(format!("\"{}\" <= $1", self.col), vec![val.into()])
}
pub fn like(&self, pattern: impl Into<String>) -> FilterExpr {
FilterExpr::new(
format!("\"{}\" LIKE $1", self.col),
vec![SqlValue::Text(pattern.into())],
)
}
pub fn ilike(&self, pattern: impl Into<String>) -> FilterExpr {
FilterExpr::new(
format!("\"{}\" ILIKE $1", self.col),
vec![SqlValue::Text(pattern.into())],
)
}
pub fn starts_with(&self, prefix: impl Into<String>) -> FilterExpr {
let p = format!("{}%", prefix.into());
FilterExpr::new(
format!("\"{}\" ILIKE $1", self.col),
vec![SqlValue::Text(p)],
)
}
pub fn ends_with(&self, suffix: impl Into<String>) -> FilterExpr {
let s = format!("%{}", suffix.into());
FilterExpr::new(
format!("\"{}\" ILIKE $1", self.col),
vec![SqlValue::Text(s)],
)
}
pub fn contains(&self, substr: impl Into<String>) -> FilterExpr {
let s = format!("%{}%", substr.into());
FilterExpr::new(
format!("\"{}\" ILIKE $1", self.col),
vec![SqlValue::Text(s)],
)
}
pub fn matches_regex(&self, pattern: impl Into<String>) -> FilterExpr {
FilterExpr::new(
format!("\"{}\" ~ $1", self.col),
vec![SqlValue::Text(pattern.into())],
)
}
pub fn in_<V: Into<SqlValue> + Clone>(
&self,
values: impl IntoIterator<Item = V>,
) -> FilterExpr {
let vals: Vec<SqlValue> = values.into_iter().map(|v| v.into()).collect();
if vals.is_empty() {
return FilterExpr::raw("FALSE");
}
let placeholders: Vec<String> = (1..=vals.len()).map(|i| format!("${}", i)).collect();
FilterExpr::new(
format!("\"{}\" IN ({})", self.col, placeholders.join(", ")),
vals,
)
}
pub fn not_in<V: Into<SqlValue>>(&self, values: impl IntoIterator<Item = V>) -> FilterExpr {
let vals: Vec<SqlValue> = values.into_iter().map(|v| v.into()).collect();
if vals.is_empty() {
return FilterExpr::raw("TRUE");
}
let placeholders: Vec<String> = (1..=vals.len()).map(|i| format!("${}", i)).collect();
FilterExpr::new(
format!("\"{}\" NOT IN ({})", self.col, placeholders.join(", ")),
vals,
)
}
pub fn between<V: Into<SqlValue>>(&self, low: V, high: V) -> FilterExpr {
FilterExpr::new(
format!("\"{}\" BETWEEN $1 AND $2", self.col),
vec![low.into(), high.into()],
)
}
pub fn is_null(&self) -> FilterExpr {
FilterExpr::raw(format!("\"{}\" IS NULL", self.col))
}
pub fn is_not_null(&self) -> FilterExpr {
FilterExpr::raw(format!("\"{}\" IS NOT NULL", self.col))
}
pub fn is_true(&self) -> FilterExpr {
FilterExpr::raw(format!("\"{}\" = TRUE", self.col))
}
pub fn is_false(&self) -> FilterExpr {
FilterExpr::raw(format!("\"{}\" = FALSE", self.col))
}
pub fn before<V: Into<SqlValue>>(&self, date: V) -> FilterExpr {
FilterExpr::new(format!("\"{}\" < $1", self.col), vec![date.into()])
}
pub fn after<V: Into<SqlValue>>(&self, date: V) -> FilterExpr {
FilterExpr::new(format!("\"{}\" > $1", self.col), vec![date.into()])
}
pub fn in_last(&self, n: u32, unit: TimeUnit) -> FilterExpr {
let interval = format!("{} {}", n, unit.as_str());
FilterExpr::raw(format!(
"\"{}\" > NOW() - INTERVAL '{}'",
self.col, interval
))
}
pub fn this_week(&self) -> FilterExpr {
FilterExpr::raw(format!(
"date_trunc('week', \"{}\") = date_trunc('week', NOW())",
self.col
))
}
pub fn this_month(&self) -> FilterExpr {
FilterExpr::raw(format!(
"date_trunc('month', \"{}\") = date_trunc('month', NOW())",
self.col
))
}
pub fn this_year(&self) -> FilterExpr {
FilterExpr::raw(format!(
"date_trunc('year', \"{}\") = date_trunc('year', NOW())",
self.col
))
}
pub fn json_eq(&self, key: &str, val: impl Into<String>) -> FilterExpr {
FilterExpr::new(
format!("\"{}\"->>'{}' = $1", self.col, key),
vec![SqlValue::Text(val.into())],
)
}
pub fn json_has_key(&self, key: &str) -> FilterExpr {
FilterExpr::new(
format!("\"{}\" ? $1", self.col),
vec![SqlValue::Text(key.to_string())],
)
}
pub fn json_contains(&self, val: serde_json::Value) -> FilterExpr {
FilterExpr::new(
format!("\"{}\" @> $1::jsonb", self.col),
vec![SqlValue::Json(val)],
)
}
pub fn asc(&self) -> OrderExpr {
OrderExpr {
sql: format!("\"{}\" ASC", self.col),
}
}
pub fn desc(&self) -> OrderExpr {
OrderExpr {
sql: format!("\"{}\" DESC", self.col),
}
}
pub fn expr(&self) -> String {
format!("\"{}\"", self.col)
}
}
#[derive(Debug, Clone)]
pub struct OrderExpr {
pub sql: String,
}
impl OrderExpr {
pub fn nulls_last(self) -> Self {
Self {
sql: format!("{} NULLS LAST", self.sql),
}
}
pub fn nulls_first(self) -> Self {
Self {
sql: format!("{} NULLS FIRST", self.sql),
}
}
pub fn then(self, other: OrderExpr) -> Self {
Self {
sql: format!("{}, {}", self.sql, other.sql),
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum TimeUnit {
Seconds,
Minutes,
Hours,
Days,
Weeks,
Months,
Years,
}
impl TimeUnit {
pub fn as_str(self) -> &'static str {
match self {
TimeUnit::Seconds => "seconds",
TimeUnit::Minutes => "minutes",
TimeUnit::Hours => "hours",
TimeUnit::Days => "days",
TimeUnit::Weeks => "weeks",
TimeUnit::Months => "months",
TimeUnit::Years => "years",
}
}
}