Skip to main content

rust_d1_orm/
query.rs

1use worker::wasm_bindgen::JsValue;
2
3pub enum Order {
4    Asc,
5    Desc,
6}
7
8impl Order {
9    fn as_sql(&self) -> &'static str {
10        match self { Order::Asc => "ASC", Order::Desc => "DESC" }
11    }
12}
13
14enum ConditionKind {
15    Eq(String),
16    Ne(String),
17    Gt(String),
18    Gte(String),
19    Lt(String),
20    Lte(String),
21    IsNull(String),
22    IsNotNull(String),
23    FilterOptional(String),
24    FilterOptionalGte(String),
25    FilterOptionalLte(String),
26}
27
28struct Condition {
29    kind: ConditionKind,
30    value: Option<JsValue>,
31}
32
33pub struct Query {
34    conditions: Vec<Condition>,
35    order: Option<(String, Order)>,
36    limit: Option<u64>,
37    offset: Option<u64>,
38}
39
40impl Query {
41    pub fn new() -> Self {
42        Self { conditions: vec![], order: None, limit: None, offset: None }
43    }
44
45    pub fn eq(mut self, col: &str, val: impl Into<JsValue>) -> Self {
46        self.conditions.push(Condition { kind: ConditionKind::Eq(col.to_string()), value: Some(val.into()) });
47        self
48    }
49
50    pub fn ne(mut self, col: &str, val: impl Into<JsValue>) -> Self {
51        self.conditions.push(Condition { kind: ConditionKind::Ne(col.to_string()), value: Some(val.into()) });
52        self
53    }
54
55    pub fn gt(mut self, col: &str, val: impl Into<JsValue>) -> Self {
56        self.conditions.push(Condition { kind: ConditionKind::Gt(col.to_string()), value: Some(val.into()) });
57        self
58    }
59
60    pub fn gte(mut self, col: &str, val: impl Into<JsValue>) -> Self {
61        self.conditions.push(Condition { kind: ConditionKind::Gte(col.to_string()), value: Some(val.into()) });
62        self
63    }
64
65    pub fn lt(mut self, col: &str, val: impl Into<JsValue>) -> Self {
66        self.conditions.push(Condition { kind: ConditionKind::Lt(col.to_string()), value: Some(val.into()) });
67        self
68    }
69
70    pub fn lte(mut self, col: &str, val: impl Into<JsValue>) -> Self {
71        self.conditions.push(Condition { kind: ConditionKind::Lte(col.to_string()), value: Some(val.into()) });
72        self
73    }
74
75    pub fn is_null(mut self, col: &str) -> Self {
76        self.conditions.push(Condition { kind: ConditionKind::IsNull(col.to_string()), value: None });
77        self
78    }
79
80    pub fn is_not_null(mut self, col: &str) -> Self {
81        self.conditions.push(Condition { kind: ConditionKind::IsNotNull(col.to_string()), value: None });
82        self
83    }
84
85    /// Generates `(?N IS NULL OR col = ?N)` — use for optional filter parameters.
86    /// Pass `None` to skip the filter (match all rows), `Some(v)` to filter by value.
87    pub fn filter_optional(mut self, col: &str, val: Option<impl Into<JsValue>>) -> Self {
88        let js = val.map(Into::into).unwrap_or(JsValue::NULL);
89        self.conditions.push(Condition { kind: ConditionKind::FilterOptional(col.to_string()), value: Some(js) });
90        self
91    }
92
93    /// Generates `(?N IS NULL OR col >= ?N)` — use for optional lower-bound filters.
94    pub fn filter_optional_gte(mut self, col: &str, val: Option<impl Into<JsValue>>) -> Self {
95        let js = val.map(Into::into).unwrap_or(JsValue::NULL);
96        self.conditions.push(Condition { kind: ConditionKind::FilterOptionalGte(col.to_string()), value: Some(js) });
97        self
98    }
99
100    /// Generates `(?N IS NULL OR col <= ?N)` — use for optional upper-bound filters.
101    pub fn filter_optional_lte(mut self, col: &str, val: Option<impl Into<JsValue>>) -> Self {
102        let js = val.map(Into::into).unwrap_or(JsValue::NULL);
103        self.conditions.push(Condition { kind: ConditionKind::FilterOptionalLte(col.to_string()), value: Some(js) });
104        self
105    }
106
107    pub fn order_by(mut self, col: &str, dir: Order) -> Self {
108        self.order = Some((col.to_string(), dir));
109        self
110    }
111
112    pub fn limit(mut self, n: u64) -> Self { self.limit = Some(n); self }
113    pub fn offset(mut self, n: u64) -> Self { self.offset = Some(n); self }
114
115    pub(crate) fn build_conditions(&self, param_start: usize) -> (String, Vec<JsValue>) {
116        let mut parts: Vec<String> = vec![];
117        let mut values: Vec<JsValue> = vec![];
118        let mut n = param_start;
119
120        for cond in &self.conditions {
121            match &cond.kind {
122                ConditionKind::Eq(col) => {
123                    parts.push(format!("{} = ?{}", col, n));
124                    values.push(cond.value.clone().unwrap());
125                    n += 1;
126                }
127                ConditionKind::Ne(col) => {
128                    parts.push(format!("{} != ?{}", col, n));
129                    values.push(cond.value.clone().unwrap());
130                    n += 1;
131                }
132                ConditionKind::Gt(col) => {
133                    parts.push(format!("{} > ?{}", col, n));
134                    values.push(cond.value.clone().unwrap());
135                    n += 1;
136                }
137                ConditionKind::Gte(col) => {
138                    parts.push(format!("{} >= ?{}", col, n));
139                    values.push(cond.value.clone().unwrap());
140                    n += 1;
141                }
142                ConditionKind::Lt(col) => {
143                    parts.push(format!("{} < ?{}", col, n));
144                    values.push(cond.value.clone().unwrap());
145                    n += 1;
146                }
147                ConditionKind::Lte(col) => {
148                    parts.push(format!("{} <= ?{}", col, n));
149                    values.push(cond.value.clone().unwrap());
150                    n += 1;
151                }
152                ConditionKind::IsNull(col) => {
153                    parts.push(format!("{} IS NULL", col));
154                }
155                ConditionKind::IsNotNull(col) => {
156                    parts.push(format!("{} IS NOT NULL", col));
157                }
158                ConditionKind::FilterOptional(col) => {
159                    parts.push(format!("(?{0} IS NULL OR {1} = ?{0})", n, col));
160                    values.push(cond.value.clone().unwrap());
161                    n += 1;
162                }
163                ConditionKind::FilterOptionalGte(col) => {
164                    parts.push(format!("(?{0} IS NULL OR {1} >= ?{0})", n, col));
165                    values.push(cond.value.clone().unwrap());
166                    n += 1;
167                }
168                ConditionKind::FilterOptionalLte(col) => {
169                    parts.push(format!("(?{0} IS NULL OR {1} <= ?{0})", n, col));
170                    values.push(cond.value.clone().unwrap());
171                    n += 1;
172                }
173            }
174        }
175
176        (parts.join(" AND "), values)
177    }
178
179    pub(crate) fn build_tail(&self) -> String {
180        let mut sql = String::new();
181        if let Some((col, dir)) = &self.order {
182            sql.push_str(&format!(" ORDER BY {} {}", col, dir.as_sql()));
183        }
184        if let Some(l) = self.limit {
185            sql.push_str(&format!(" LIMIT {}", l));
186        }
187        if let Some(o) = self.offset {
188            sql.push_str(&format!(" OFFSET {}", o));
189        }
190        sql
191    }
192}
193
194impl Default for Query {
195    fn default() -> Self { Self::new() }
196}