1use std::fmt;
2use std::marker::PhantomData;
3
4use keelson_core::clause::HasWhere;
5use keelson_core::expr::{Chain, Expr, IntoExpr, IntoExprList};
6use keelson_core::{Mod, ToValue};
7
8pub struct Column<T> {
29 table: &'static str,
30 name: &'static str,
31 _type: PhantomData<fn() -> T>,
32}
33
34impl<T> fmt::Debug for Column<T> {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 f.debug_struct("Column")
37 .field("table", &self.table)
38 .field("name", &self.name)
39 .finish()
40 }
41}
42
43impl<T> Clone for Column<T> {
44 fn clone(&self) -> Self {
45 *self
46 }
47}
48
49impl<T> Copy for Column<T> {}
50
51impl<T> Column<T> {
52 pub const fn new(table: &'static str, name: &'static str) -> Column<T> {
55 Column {
56 table,
57 name,
58 _type: PhantomData,
59 }
60 }
61
62 pub const fn aliased_as(self, alias: &'static str) -> Column<T> {
67 Column {
68 table: alias,
69 ..self
70 }
71 }
72
73 pub const fn table(&self) -> &'static str {
75 self.table
76 }
77
78 pub const fn name(&self) -> &'static str {
81 self.name
82 }
83
84 pub fn expr(self) -> Expr {
86 Expr::ident((self.table, self.name))
87 }
88
89 pub fn is_null(self) -> Filter {
92 Filter::from_expr(self.expr()).is_null()
93 }
94
95 pub fn is_not_null(self) -> Filter {
97 Filter::from_expr(self.expr()).is_not_null()
98 }
99}
100
101impl<T: ToValue> Column<T> {
102 fn cmp(self, op: &'static str, rhs: T) -> Filter {
103 Filter::from_expr(self.expr()).op(op, Expr::arg(rhs))
104 }
105
106 pub fn eq(self, value: impl Into<T>) -> Filter {
116 self.cmp("=", value.into())
117 }
118
119 pub fn ne(self, value: impl Into<T>) -> Filter {
121 self.cmp("<>", value.into())
122 }
123
124 pub fn lt(self, value: impl Into<T>) -> Filter {
126 self.cmp("<", value.into())
127 }
128
129 pub fn lte(self, value: impl Into<T>) -> Filter {
131 self.cmp("<=", value.into())
132 }
133
134 pub fn gt(self, value: impl Into<T>) -> Filter {
136 self.cmp(">", value.into())
137 }
138
139 pub fn gte(self, value: impl Into<T>) -> Filter {
141 self.cmp(">=", value.into())
142 }
143
144 pub fn in_(self, values: impl IntoIterator<Item = impl Into<T>>) -> Filter {
146 let vals: Vec<Expr> = values.into_iter().map(|v| Expr::arg(v.into())).collect();
147 Filter::from_expr(self.expr()).in_(vals)
148 }
149
150 pub fn not_in(self, values: impl IntoIterator<Item = impl Into<T>>) -> Filter {
152 let vals: Vec<Expr> = values.into_iter().map(|v| Expr::arg(v.into())).collect();
153 Filter::from_expr(self.expr()).not_in(vals)
154 }
155
156 pub fn between(self, low: impl Into<T>, high: impl Into<T>) -> Filter {
158 Filter::from_expr(self.expr()).between(Expr::arg(low.into()), Expr::arg(high.into()))
159 }
160}
161
162impl Column<String> {
163 pub fn like(self, pattern: impl Into<String>) -> Filter {
166 Filter::from_expr(self.expr()).like(Expr::arg(pattern.into()))
167 }
168}
169
170impl<T> IntoExpr for Column<T> {
171 fn into_expr(self) -> Expr {
172 self.expr()
173 }
174}
175
176impl<T> IntoExprList for Column<T> {
177 fn into_expr_list(self) -> Vec<Expr> {
178 vec![self.expr()]
179 }
180}
181
182#[derive(Debug, Clone)]
195pub struct Filter(Expr);
196
197impl Filter {
198 pub fn new(condition: impl IntoExpr) -> Filter {
201 Filter(condition.into_expr())
202 }
203}
204
205impl IntoExpr for Filter {
206 fn into_expr(self) -> Expr {
207 self.0
208 }
209}
210
211impl IntoExprList for Filter {
212 fn into_expr_list(self) -> Vec<Expr> {
213 vec![self.0]
214 }
215}
216
217impl Chain for Filter {
218 fn from_expr(e: Expr) -> Filter {
219 Filter(e)
220 }
221}
222
223impl<Q: HasWhere> Mod<Q> for Filter {
226 fn apply(self, q: &mut Q) {
227 q.where_mut().append_where(self.0);
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use keelson_core::Value;
234 use keelson_core::clause::Where;
235 use keelson_sqlcheck::testing::{assert_frag, render};
236
237 use super::*;
238
239 const COND: &str = r#"SELECT "id" FROM users WHERE {}"#;
240
241 fn age() -> Column<i32> {
242 Column::new("users", "age")
243 }
244
245 fn name() -> Column<String> {
246 Column::new("users", "name")
247 }
248
249 #[test]
250 fn a_column_is_its_qualified_quoted_identifier() {
251 assert_frag(r#"SELECT {} FROM users"#, &age().expr(), r#""users"."age""#);
252 }
253
254 #[test]
255 fn typed_comparisons_bind_the_column_type() {
256 let args = assert_frag(COND, &age().gte(21).into_expr(), r#"("users"."age" >= $1)"#);
257 assert_eq!(args, vec![Value::I32(21)]);
258
259 let args = assert_frag(
261 COND,
262 &name().eq("ada").into_expr(),
263 r#"("users"."name" = $1)"#,
264 );
265 assert_eq!(args, vec![Value::Text("ada".into())]);
266 }
267
268 #[test]
269 fn in_binds_every_element() {
270 let args = assert_frag(
271 COND,
272 &age().in_([1, 2, 3]).into_expr(),
273 r#"("users"."age" IN ($1, $2, $3))"#,
274 );
275 assert_eq!(args.len(), 3);
276 }
277
278 #[test]
279 fn null_tests_and_like_have_their_sql_shapes() {
280 assert_frag(
281 COND,
282 &age().is_null().into_expr(),
283 r#"("users"."age" IS NULL)"#,
284 );
285 assert_frag(
286 COND,
287 &name().like("a%").into_expr(),
288 r#"("users"."name" LIKE $1)"#,
289 );
290 assert_frag(
291 COND,
292 &age().between(1, 9).into_expr(),
293 r#"("users"."age" BETWEEN $1 AND $2)"#,
294 );
295 }
296
297 #[test]
298 fn a_filter_chains_on_with_layer_1_operators() {
299 let f = age().gte(21).and(name().like("a%"));
301 assert_frag(
302 COND,
303 &f.into_expr(),
304 r#"(("users"."age" >= $1) AND ("users"."name" LIKE $2))"#,
305 );
306 }
307
308 #[test]
309 fn aliased_as_requalifies_and_keeps_the_type() {
310 let f = age().aliased_as("u").gte(21);
311 assert_frag(
312 r#"SELECT "id" FROM users AS u WHERE {}"#,
313 &f.into_expr(),
314 r#"("u"."age" >= $1)"#,
315 );
316 assert_eq!(age().aliased_as("u").name(), "age");
317 }
318
319 #[test]
320 fn a_filter_is_a_mod_on_anything_with_a_where() {
321 let mut w = Where::default();
322 age().gte(21).apply(&mut w);
323 name().eq("ada").apply(&mut w);
324 let (sql, args) = render(&w);
325 assert_eq!(
326 sql,
327 r#"WHERE ("users"."age" >= $1) AND ("users"."name" = $2)"#
328 );
329 assert_eq!(args.len(), 2);
330 }
331
332 #[test]
333 fn filter_new_wraps_a_raw_fragment() {
334 assert_frag(COND, &Filter::new("age > 21").into_expr(), "age > 21");
335 }
336}