rust-d1-orm 0.0.1

Query builder / ORM for Cloudflare D1, targeting wasm32-unknown-unknown
Documentation
use worker::wasm_bindgen::JsValue;

pub enum Order {
    Asc,
    Desc,
}

impl Order {
    fn as_sql(&self) -> &'static str {
        match self { Order::Asc => "ASC", Order::Desc => "DESC" }
    }
}

enum ConditionKind {
    Eq(String),
    Ne(String),
    Gt(String),
    Gte(String),
    Lt(String),
    Lte(String),
    IsNull(String),
    IsNotNull(String),
    FilterOptional(String),
    FilterOptionalGte(String),
    FilterOptionalLte(String),
}

struct Condition {
    kind: ConditionKind,
    value: Option<JsValue>,
}

pub struct Query {
    conditions: Vec<Condition>,
    order: Option<(String, Order)>,
    limit: Option<u64>,
    offset: Option<u64>,
}

impl Query {
    pub fn new() -> Self {
        Self { conditions: vec![], order: None, limit: None, offset: None }
    }

    pub fn eq(mut self, col: &str, val: impl Into<JsValue>) -> Self {
        self.conditions.push(Condition { kind: ConditionKind::Eq(col.to_string()), value: Some(val.into()) });
        self
    }

    pub fn ne(mut self, col: &str, val: impl Into<JsValue>) -> Self {
        self.conditions.push(Condition { kind: ConditionKind::Ne(col.to_string()), value: Some(val.into()) });
        self
    }

    pub fn gt(mut self, col: &str, val: impl Into<JsValue>) -> Self {
        self.conditions.push(Condition { kind: ConditionKind::Gt(col.to_string()), value: Some(val.into()) });
        self
    }

    pub fn gte(mut self, col: &str, val: impl Into<JsValue>) -> Self {
        self.conditions.push(Condition { kind: ConditionKind::Gte(col.to_string()), value: Some(val.into()) });
        self
    }

    pub fn lt(mut self, col: &str, val: impl Into<JsValue>) -> Self {
        self.conditions.push(Condition { kind: ConditionKind::Lt(col.to_string()), value: Some(val.into()) });
        self
    }

    pub fn lte(mut self, col: &str, val: impl Into<JsValue>) -> Self {
        self.conditions.push(Condition { kind: ConditionKind::Lte(col.to_string()), value: Some(val.into()) });
        self
    }

    pub fn is_null(mut self, col: &str) -> Self {
        self.conditions.push(Condition { kind: ConditionKind::IsNull(col.to_string()), value: None });
        self
    }

    pub fn is_not_null(mut self, col: &str) -> Self {
        self.conditions.push(Condition { kind: ConditionKind::IsNotNull(col.to_string()), value: None });
        self
    }

    /// Generates `(?N IS NULL OR col = ?N)` — use for optional filter parameters.
    /// Pass `None` to skip the filter (match all rows), `Some(v)` to filter by value.
    pub fn filter_optional(mut self, col: &str, val: Option<impl Into<JsValue>>) -> Self {
        let js = val.map(Into::into).unwrap_or(JsValue::NULL);
        self.conditions.push(Condition { kind: ConditionKind::FilterOptional(col.to_string()), value: Some(js) });
        self
    }

    /// Generates `(?N IS NULL OR col >= ?N)` — use for optional lower-bound filters.
    pub fn filter_optional_gte(mut self, col: &str, val: Option<impl Into<JsValue>>) -> Self {
        let js = val.map(Into::into).unwrap_or(JsValue::NULL);
        self.conditions.push(Condition { kind: ConditionKind::FilterOptionalGte(col.to_string()), value: Some(js) });
        self
    }

    /// Generates `(?N IS NULL OR col <= ?N)` — use for optional upper-bound filters.
    pub fn filter_optional_lte(mut self, col: &str, val: Option<impl Into<JsValue>>) -> Self {
        let js = val.map(Into::into).unwrap_or(JsValue::NULL);
        self.conditions.push(Condition { kind: ConditionKind::FilterOptionalLte(col.to_string()), value: Some(js) });
        self
    }

    pub fn order_by(mut self, col: &str, dir: Order) -> Self {
        self.order = Some((col.to_string(), dir));
        self
    }

    pub fn limit(mut self, n: u64) -> Self { self.limit = Some(n); self }
    pub fn offset(mut self, n: u64) -> Self { self.offset = Some(n); self }

    pub(crate) fn build_conditions(&self, param_start: usize) -> (String, Vec<JsValue>) {
        let mut parts: Vec<String> = vec![];
        let mut values: Vec<JsValue> = vec![];
        let mut n = param_start;

        for cond in &self.conditions {
            match &cond.kind {
                ConditionKind::Eq(col) => {
                    parts.push(format!("{} = ?{}", col, n));
                    values.push(cond.value.clone().unwrap());
                    n += 1;
                }
                ConditionKind::Ne(col) => {
                    parts.push(format!("{} != ?{}", col, n));
                    values.push(cond.value.clone().unwrap());
                    n += 1;
                }
                ConditionKind::Gt(col) => {
                    parts.push(format!("{} > ?{}", col, n));
                    values.push(cond.value.clone().unwrap());
                    n += 1;
                }
                ConditionKind::Gte(col) => {
                    parts.push(format!("{} >= ?{}", col, n));
                    values.push(cond.value.clone().unwrap());
                    n += 1;
                }
                ConditionKind::Lt(col) => {
                    parts.push(format!("{} < ?{}", col, n));
                    values.push(cond.value.clone().unwrap());
                    n += 1;
                }
                ConditionKind::Lte(col) => {
                    parts.push(format!("{} <= ?{}", col, n));
                    values.push(cond.value.clone().unwrap());
                    n += 1;
                }
                ConditionKind::IsNull(col) => {
                    parts.push(format!("{} IS NULL", col));
                }
                ConditionKind::IsNotNull(col) => {
                    parts.push(format!("{} IS NOT NULL", col));
                }
                ConditionKind::FilterOptional(col) => {
                    parts.push(format!("(?{0} IS NULL OR {1} = ?{0})", n, col));
                    values.push(cond.value.clone().unwrap());
                    n += 1;
                }
                ConditionKind::FilterOptionalGte(col) => {
                    parts.push(format!("(?{0} IS NULL OR {1} >= ?{0})", n, col));
                    values.push(cond.value.clone().unwrap());
                    n += 1;
                }
                ConditionKind::FilterOptionalLte(col) => {
                    parts.push(format!("(?{0} IS NULL OR {1} <= ?{0})", n, col));
                    values.push(cond.value.clone().unwrap());
                    n += 1;
                }
            }
        }

        (parts.join(" AND "), values)
    }

    pub(crate) fn build_tail(&self) -> String {
        let mut sql = String::new();
        if let Some((col, dir)) = &self.order {
            sql.push_str(&format!(" ORDER BY {} {}", col, dir.as_sql()));
        }
        if let Some(l) = self.limit {
            sql.push_str(&format!(" LIMIT {}", l));
        }
        if let Some(o) = self.offset {
            sql.push_str(&format!(" OFFSET {}", o));
        }
        sql
    }
}

impl Default for Query {
    fn default() -> Self { Self::new() }
}