Skip to main content

qraft_core/expression/
column.rs

1//! Typed column references and helpers for generated schema modules.
2
3use std::marker::PhantomData;
4
5use quex::Encode;
6
7use crate::{
8    BigInt, Blob, Bool, Comparable, Double, Float, Int, Likeable, LowerCompatible, Nullable,
9    Qrafting, Required, Text, UBigInt, UInt, Untyped,
10    alias::Aliased,
11    expression::{
12        As, Between, Binary, EqExt, Expression, IsNull, IsPredicate, Like, LikeExt, Postfix,
13        op::{Eq, Gt, Gte, Lt, Lte, Neq},
14    },
15    lower::LowerCtx,
16    param::encode_param,
17    query::LowerProject,
18    ty::{Orderable, TypeMeta},
19};
20
21/// A typed reference to a table column.
22#[derive(Debug)]
23pub struct Column<M, T: TypeMeta> {
24    /// SQL column name as it should be emitted.
25    pub name: &'static str,
26    marker: PhantomData<(M, T)>,
27}
28
29impl<M, Ty: TypeMeta> Copy for Column<M, Ty> {}
30
31impl<M, Ty: TypeMeta> Clone for Column<M, Ty> {
32    fn clone(&self) -> Self {
33        *self
34    }
35}
36
37impl<M, T: TypeMeta> Column<M, T> {
38    /// Creates a typed column reference from a static name.
39    pub const fn new(value: &'static str) -> Self {
40        Column {
41            name: value,
42            marker: PhantomData,
43        }
44    }
45
46    /// Returns the SQL name stored in this column.
47    pub const fn name(self) -> &'static str {
48        self.name
49    }
50}
51
52/// Builds unscoped typed columns from raw names.
53pub trait Col {
54    /// Creates a column with the provided SQL type.
55    fn typed<T: TypeMeta>(&self) -> Column<(), T>;
56
57    /// Creates an unscoped untyped column.
58    fn untyped(&self) -> Column<(), Untyped> {
59        self.typed::<Untyped>()
60    }
61
62    /// Creates an unscoped text column.
63    fn text(&self) -> Column<(), Text> {
64        self.typed::<Text>()
65    }
66
67    /// Creates an unscoped blob column.
68    fn blob(&self) -> Column<(), Blob> {
69        self.typed::<Blob>()
70    }
71
72    /// Creates an unscoped boolean column.
73    fn bool(&self) -> Column<(), Bool> {
74        self.typed::<Bool>()
75    }
76
77    /// Creates an unscoped signed 64-bit integer column.
78    fn bigint(&self) -> Column<(), BigInt> {
79        self.typed::<BigInt>()
80    }
81
82    /// Creates an unscoped signed 32-bit integer column.
83    fn int(&self) -> Column<(), Int> {
84        self.typed::<Int>()
85    }
86
87    /// Creates an unscoped float column.
88    fn float(&self) -> Column<(), Float> {
89        self.typed::<Float>()
90    }
91
92    /// Creates an unscoped double column.
93    fn double(&self) -> Column<(), Double> {
94        self.typed::<Double>()
95    }
96
97    /// Creates an unscoped unsigned 32-bit integer column.
98    fn uint(&self) -> Column<(), UInt> {
99        self.typed::<UInt>()
100    }
101
102    /// Creates an unscoped unsigned 64-bit integer column.
103    fn ubigint(&self) -> Column<(), UBigInt> {
104        self.typed::<UBigInt>()
105    }
106}
107
108impl Col for &'static str {
109    fn typed<T: TypeMeta>(&self) -> Column<(), T> {
110        Column {
111            name: self,
112            marker: PhantomData,
113        }
114    }
115}
116
117impl<M, T: TypeMeta + Required> Column<M, T> {
118    /// Reuses the same column name with a nullable type wrapper.
119    pub fn nullable(self) -> Column<M, Nullable<T>> {
120        Column {
121            name: self.name,
122            marker: PhantomData,
123        }
124    }
125}
126
127impl<M: Qrafting, T: Comparable> Column<M, T> {
128    pub fn eq<E>(self, e: E) -> Binary<Eq, Self, E>
129    where
130        E: LowerCompatible<T>,
131    {
132        EqExt::eq(self, e)
133    }
134
135    pub fn neq<E>(self, e: E) -> Binary<Neq, Self, E>
136    where
137        E: LowerCompatible<T>,
138    {
139        EqExt::neq(self, e)
140    }
141}
142
143impl<T: Comparable> Column<(), T> {
144    pub fn eq<E>(self, e: E) -> Binary<Eq, Self, E>
145    where
146        E: LowerCompatible<T>,
147    {
148        EqExt::eq(self, e)
149    }
150
151    pub fn neq<E>(self, e: E) -> Binary<Neq, Self, E>
152    where
153        E: LowerCompatible<T>,
154    {
155        EqExt::neq(self, e)
156    }
157}
158
159/// Builds untyped comparison expressions for ad-hoc raw column names.
160pub trait CompareUntyped {
161    fn equal<E>(self, other: E) -> Binary<Eq, Column<(), Untyped>, UntypedValue<E>>
162    where
163        E: Encode;
164
165    fn not_equal<E>(self, other: E) -> Binary<Neq, Column<(), Untyped>, UntypedValue<E>>
166    where
167        E: Encode;
168
169    fn less_than<E>(self, other: E) -> Binary<Lt, Column<(), Untyped>, UntypedValue<E>>
170    where
171        E: Encode;
172
173    fn less_or_equal<E>(self, other: E) -> Binary<Lte, Column<(), Untyped>, UntypedValue<E>>
174    where
175        E: Encode;
176
177    fn greater_than<E>(self, other: E) -> Binary<Gt, Column<(), Untyped>, UntypedValue<E>>
178    where
179        E: Encode;
180
181    fn greater_or_equal<E>(self, other: E) -> Binary<Gte, Column<(), Untyped>, UntypedValue<E>>
182    where
183        E: Encode;
184}
185
186impl CompareUntyped for &'static str {
187    fn equal<E>(self, other: E) -> Binary<Eq, Column<(), Untyped>, UntypedValue<E>>
188    where
189        E: Encode,
190    {
191        Binary {
192            left: self.untyped(),
193            right: UntypedValue(other),
194            op: PhantomData,
195        }
196    }
197
198    fn not_equal<E>(self, other: E) -> Binary<Neq, Column<(), Untyped>, UntypedValue<E>>
199    where
200        E: Encode,
201    {
202        Binary {
203            left: self.untyped(),
204            right: UntypedValue(other),
205            op: PhantomData,
206        }
207    }
208
209    fn less_than<E>(self, other: E) -> Binary<Lt, Column<(), Untyped>, UntypedValue<E>>
210    where
211        E: Encode,
212    {
213        Binary {
214            left: self.untyped(),
215            right: UntypedValue(other),
216            op: PhantomData,
217        }
218    }
219
220    fn less_or_equal<E>(self, other: E) -> Binary<Lte, Column<(), Untyped>, UntypedValue<E>>
221    where
222        E: Encode,
223    {
224        Binary {
225            left: self.untyped(),
226            right: UntypedValue(other),
227            op: PhantomData,
228        }
229    }
230
231    fn greater_than<E>(self, other: E) -> Binary<Gt, Column<(), Untyped>, UntypedValue<E>>
232    where
233        E: Encode,
234    {
235        Binary {
236            left: self.untyped(),
237            right: UntypedValue(other),
238            op: PhantomData,
239        }
240    }
241
242    fn greater_or_equal<E>(self, other: E) -> Binary<Gte, Column<(), Untyped>, UntypedValue<E>>
243    where
244        E: Encode,
245    {
246        Binary {
247            left: self.untyped(),
248            right: UntypedValue(other),
249            op: PhantomData,
250        }
251    }
252}
253
254pub struct UntypedValue<T>(T);
255
256#[qraft_expression_macro::as_expression]
257impl<T: Encode> Expression for UntypedValue<T> {
258    type Type = Untyped;
259
260    fn lower(&self, ctx: &mut LowerCtx) -> usize {
261        let param = encode_param(&self.0, ctx.data);
262        ctx.lower_param(param)
263    }
264}
265
266impl<M: Qrafting, T: Comparable + Required> Column<M, Nullable<T>> {
267    #[allow(clippy::wrong_self_convention)]
268    pub fn is_null(self) -> Postfix<Self> {
269        IsNull::is_null(self)
270    }
271
272    #[allow(clippy::wrong_self_convention)]
273    pub fn is_not_null(self) -> Postfix<Self> {
274        IsNull::is_not_null(self)
275    }
276}
277
278impl<M: Qrafting, T: crate::Boolean> Column<M, T> {
279    #[allow(clippy::wrong_self_convention)]
280    pub fn is_true(self) -> Postfix<Self> {
281        IsPredicate::is_true(self)
282    }
283
284    #[allow(clippy::wrong_self_convention)]
285    pub fn is_false(self) -> Postfix<Self> {
286        IsPredicate::is_false(self)
287    }
288}
289
290impl<M: Qrafting, T: Likeable> Column<M, T> {
291    pub fn like<E>(self, other: E) -> Like<Self, E>
292    where
293        E: LowerCompatible<T>,
294    {
295        LikeExt::like(self, other)
296    }
297
298    pub fn not_like<E>(self, other: E) -> Like<Self, E>
299    where
300        E: LowerCompatible<T>,
301    {
302        LikeExt::not_like(self, other)
303    }
304}
305
306#[qraft_expression_macro::as_expression]
307impl<M: Qrafting, T: TypeMeta> Expression for Column<M, T> {
308    type Type = T;
309
310    fn lower(&self, ctx: &mut LowerCtx) -> usize {
311        ctx.lower_column(Some(M::TABLE), self.name)
312    }
313}
314
315#[qraft_expression_macro::as_expression]
316impl<T: TypeMeta> Expression for Column<(), T> {
317    type Type = T;
318
319    fn lower(&self, ctx: &mut LowerCtx) -> usize {
320        ctx.lower_column(None, self.name)
321    }
322}
323
324/// Projects `table.*` for a typed table source.
325pub struct ColumnStar<M> {
326    pub marker: PhantomData<M>,
327}
328
329impl<M> Copy for ColumnStar<M> {}
330
331impl<M> Clone for ColumnStar<M> {
332    fn clone(&self) -> Self {
333        *self
334    }
335}
336
337impl<M: Qrafting> LowerProject for ColumnStar<M> {
338    fn lower_project(self, ctx: &mut LowerCtx) {
339        let table = M::TABLE;
340        ctx.lower_column(Some(table), "*");
341    }
342}
343
344impl<M: Qrafting> LowerProject for As<ColumnStar<M>> {
345    fn lower_project(self, ctx: &mut LowerCtx) {
346        ctx.lower_column(Some(self.table), "*");
347    }
348}
349
350impl<M: Qrafting> LowerProject for Aliased<As<ColumnStar<M>>> {
351    fn lower_project(self, ctx: &mut LowerCtx) {
352        let inner = ctx.lower_column(Some(self.inner.table), "*");
353        let _ = ctx.lower_alias(self.alias, inner);
354    }
355}
356
357impl<M> ColumnStar<M> {
358    /// Creates a star projection for a table.
359    pub const fn new() -> Self {
360        ColumnStar {
361            marker: PhantomData,
362        }
363    }
364}
365
366impl<M> Default for ColumnStar<M> {
367    fn default() -> Self {
368        Self::new()
369    }
370}
371
372/// Creates an unscoped typed column from a static name.
373///
374/// This is the simplest way to build ad-hoc queries without a generated schema
375/// module.
376///
377/// # Examples
378///
379/// ```rust
380/// use qraft_core::{BigInt, Sqlite, expression::col, query::select};
381///
382/// let id = col::<BigInt>("id");
383/// let sql = select(id)
384///     .from("users")
385///     .filter(id.eq(7_i64))
386///     .to_debug_sql::<Sqlite>();
387///
388/// assert_eq!(
389///     sql,
390///     r#"select "id" from "users" where "id" = ?; params=[7]"#
391/// );
392/// ```
393pub const fn col<T: TypeMeta>(name: &'static str) -> Column<(), T> {
394    Column {
395        name,
396        marker: PhantomData,
397    }
398}
399
400impl<M: Qrafting, T: Orderable> Column<M, T> {
401    pub fn between<L, H>(self, low: L, high: H) -> Between<Self, L, H>
402    where
403        L: LowerCompatible<T>,
404        H: LowerCompatible<T>,
405    {
406        BetweenExt::between(self, low, high)
407    }
408
409    pub fn not_between<L, H>(self, low: L, high: H) -> Between<Self, L, H>
410    where
411        L: LowerCompatible<T>,
412        H: LowerCompatible<T>,
413    {
414        BetweenExt::not_between(self, low, high)
415    }
416}
417
418/// Emits `between` and `not between` expressions for ordered values.
419pub trait BetweenExt<T: Orderable>: Sized + Expression {
420    fn between<L, H>(self, low: L, high: H) -> Between<Self, L, H>
421    where
422        L: LowerCompatible<T>,
423        H: LowerCompatible<T>,
424    {
425        Between {
426            negated: false,
427            lhs: self,
428            low,
429            high,
430        }
431    }
432
433    fn not_between<L, H>(self, low: L, high: H) -> Between<Self, L, H>
434    where
435        L: LowerCompatible<T>,
436        H: LowerCompatible<T>,
437    {
438        Between {
439            negated: true,
440            lhs: self,
441            low,
442            high,
443        }
444    }
445}
446
447impl<T: Orderable, V> BetweenExt<T> for V where V: Expression<Type = T> {}
448
449#[cfg(test)]
450mod tests {
451    use super::{CompareUntyped, col};
452    use crate::{Sqlite, query::select};
453
454    #[test]
455    fn untyped_column_helpers_emit_expected_sql() {
456        let sql = select(col::<crate::Untyped>("priority"))
457            .from("tasks")
458            .filter("priority".greater_than(10))
459            .to_debug_sql::<Sqlite>();
460
461        assert_eq!(
462            sql,
463            r#"select "priority" from "tasks" where "priority" > ?; params=[10]"#
464        );
465    }
466
467    #[test]
468    fn untyped_column_equal_uses_untyped_builder() {
469        let sql = select("status")
470            .from("tasks")
471            .filter("status".equal("queued"))
472            .to_debug_sql::<Sqlite>();
473
474        assert_eq!(
475            sql,
476            r#"select "status" from "tasks" where "status" = ?; params=["queued"]"#
477        );
478    }
479}