clicktype_query/
builder.rs1use std::marker::PhantomData;
4use clicktype_core::traits::{ClickTable, TypedColumn};
5use crate::join::{JoinSpec, JoinType};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum Operator {
10 Eq,
12 Ne,
14 Gt,
16 Lt,
18 Gte,
20 Lte,
22 Like,
24 ILike,
26}
27
28impl Operator {
29 fn as_str(&self) -> &'static str {
30 match self {
31 Operator::Eq => "=",
32 Operator::Ne => "!=",
33 Operator::Gt => ">",
34 Operator::Lt => "<",
35 Operator::Gte => ">=",
36 Operator::Lte => "<=",
37 Operator::Like => "LIKE",
38 Operator::ILike => "ILIKE",
39 }
40 }
41}
42
43pub trait SqlValue {
45 fn to_sql_value(&self) -> String;
46}
47
48impl SqlValue for String {
49 fn to_sql_value(&self) -> String {
50 format!("'{}'", self.replace('`', "`"))
51 }
52}
53
54impl SqlValue for &str {
55 fn to_sql_value(&self) -> String {
56 format!("'{}'", self.replace('`', "`"))
57 }
58}
59
60macro_rules! impl_sql_value_for_numeric {
61 ($($t:ty),*) => {
62 $(
63 impl SqlValue for $t {
64 fn to_sql_value(&self) -> String {
65 self.to_string()
66 }
67 }
68 )*
69 };
70}
71
72impl_sql_value_for_numeric!(i8, i16, i32, i64, u8, u16, u32, u64, f32, f64, bool);
73
74#[derive(Debug, Clone)]
88pub struct QueryBuilder<T> {
89 table_name: &'static str,
90 select_exprs: Vec<String>,
91 joins: Vec<String>,
92 where_exprs: Vec<String>,
93 group_by_exprs: Vec<String>,
94 having_exprs: Vec<String>,
95 order_by_exprs: Vec<String>,
96 limit_val: Option<u64>,
97 offset_val: Option<u64>,
98 _phantom: PhantomData<T>,
99}
100
101impl<T: ClickTable> QueryBuilder<T> {
102 pub fn new() -> Self {
104 Self {
105 table_name: T::table_name(),
106 select_exprs: Vec::new(),
107 joins: Vec::new(),
108 where_exprs: Vec::new(),
109 group_by_exprs: Vec::new(),
110 having_exprs: Vec::new(),
111 order_by_exprs: Vec::new(),
112 limit_val: None,
113 offset_val: None,
114 _phantom: PhantomData,
115 }
116 }
117
118 pub fn select<C: TypedColumn<Table = T>>(mut self, _col: C) -> Self {
120 self.select_exprs.push(C::name().to_string());
121 self
122 }
123
124 pub fn select_raw(mut self, expr: impl Into<String>) -> Self {
127 self.select_exprs.push(expr.into());
128 self
129 }
130
131 pub fn join<T2: ClickTable, J: JoinType>(mut self, join_spec: JoinSpec<T, T2, J>) -> Self {
143 self.joins.push(join_spec.to_sql());
144 self
145 }
146
147 pub fn join_raw(mut self, join_clause: impl Into<String>) -> Self {
149 self.joins.push(join_clause.into());
150 self
151 }
152
153 pub fn filter<C: TypedColumn<Table = T>, V: SqlValue>(
163 mut self,
164 _col: C,
165 op: Operator,
166 value: V
167 ) -> Self {
168 self.where_exprs.push(format!("{} {} {}", C::name(), op.as_str(), value.to_sql_value()));
169 self
170 }
171
172 pub fn filter_raw(mut self, condition: impl Into<String>) -> Self {
174 self.where_exprs.push(condition.into());
175 self
176 }
177
178 pub fn order_by<C: TypedColumn<Table = T>>(mut self, _col: C, desc: bool) -> Self {
180 let order = if desc {
181 format!("{} DESC", C::name())
182 } else {
183 C::name().to_string()
184 };
185 self.order_by_exprs.push(order);
186 self
187 }
188
189 pub fn order_by_raw(mut self, expr: impl Into<String>) -> Self {
191 self.order_by_exprs.push(expr.into());
192 self
193 }
194
195 pub fn limit(mut self, limit: u64) -> Self {
197 self.limit_val = Some(limit);
198 self
199 }
200
201 pub fn offset(mut self, offset: u64) -> Self {
203 self.offset_val = Some(offset);
204 self
205 }
206
207 pub fn group_by<C: TypedColumn<Table = T>>(mut self, _col: C) -> Self {
209 self.group_by_exprs.push(C::name().to_string());
210 self
211 }
212
213 pub fn group_by_raw(mut self, expr: impl Into<String>) -> Self {
215 self.group_by_exprs.push(expr.into());
216 self
217 }
218
219 pub fn having(mut self, expr: impl Into<String>) -> Self {
221 self.having_exprs.push(expr.into());
222 self
223 }
224
225 pub fn to_sql(&self) -> String {
227 let mut sql = String::from("SELECT ");
228
229 if self.select_exprs.is_empty() {
230 sql.push('*');
231 } else {
232 sql.push_str(&self.select_exprs.join(", "));
233 }
234
235 sql.push_str(" FROM ");
236 sql.push_str(self.table_name);
237
238 if !self.joins.is_empty() {
239 sql.push(' ');
240 sql.push_str(&self.joins.join(" "));
241 }
242
243 if !self.where_exprs.is_empty() {
244 sql.push_str(" WHERE ");
245 sql.push_str(&self.where_exprs.join(" AND "));
246 }
247
248 if !self.group_by_exprs.is_empty() {
249 sql.push_str(" GROUP BY ");
250 sql.push_str(&self.group_by_exprs.join(", "));
251 }
252
253 if !self.having_exprs.is_empty() {
254 sql.push_str(" HAVING ");
255 sql.push_str(&self.having_exprs.join(" AND "));
256 }
257
258 if !self.order_by_exprs.is_empty() {
259 sql.push_str(" ORDER BY ");
260 sql.push_str(&self.order_by_exprs.join(", "));
261 }
262
263 if let Some(limit) = self.limit_val {
264 sql.push_str(" LIMIT ");
265 sql.push_str(&limit.to_string());
266 }
267
268 if let Some(offset) = self.offset_val {
269 sql.push_str(" OFFSET ");
270 sql.push_str(&offset.to_string());
271 }
272
273 sql
274 }
275}