Skip to main content

prax_query/inputs/
traits.rs

1//! Traits implemented by per-model generated input types.
2//!
3//! Each trait has one method, `into_ir`, that lowers the input to the
4//! runtime IR that the SQL builders already consume. The associated
5//! `Model` type keeps generic bounds tight: a `FindManyOperation<E, User>`
6//! can only accept a `WhereInput<Model = User>`, never a `PostWhereInput`.
7
8use crate::filter::Filter;
9use crate::pagination::Pagination;
10use crate::relations::Include;
11use crate::traits::Model;
12use crate::types::{OrderBy, Select};
13
14/// A typed shape that lowers to a runtime [`Filter`].
15///
16/// Implemented by per-model `UserWhereInput`, `PostWhereInput`, ...
17///
18/// # Warning: `Default::default()` lowers to `Filter::None`
19///
20/// A `*WhereInput` constructed via `Default::default()` (no fields set)
21/// produces `Filter::None`, which lowers to `WHERE TRUE` — i.e. matches
22/// every row. Passing such a filter to `delete_many` or `update_many`
23/// affects every row in the table. Codegen never refuses this at
24/// compile time; if a `delete_many` / `update_many` call site needs a
25/// non-empty filter, it is the caller's responsibility to verify the
26/// `Filter::None` case before invoking `.exec()`.
27pub trait WhereInput {
28    /// The model this WHERE shape applies to.
29    type Model: Model;
30    /// Lower this input to the runtime IR.
31    ///
32    /// Returns `Filter::None` when no fields are set. See the trait-level
33    /// note about the match-all behavior of `Filter::None`.
34    fn into_ir(self) -> Filter;
35}
36
37/// A WHERE shape constrained to a unique key (PK or `@unique` column).
38///
39/// Used by `find_unique` / `update` / `upsert` / `delete` where the
40/// operation requires the filter to identify at most one row.
41pub trait WhereUniqueInput {
42    /// The model this WHERE shape applies to.
43    type Model: Model;
44    /// Lower this input to the runtime IR.
45    fn into_ir(self) -> Filter;
46}
47
48/// A typed shape that lowers to an [`Include`] specification.
49pub trait IncludeInput {
50    /// The model this include shape applies to.
51    type Model: Model;
52    /// Lower this input to the runtime IR.
53    fn into_ir(self) -> Include;
54}
55
56/// A typed shape that lowers to a [`Select`] specification.
57pub trait SelectInput {
58    /// The model this select shape applies to.
59    type Model: Model;
60    /// Lower this input to the runtime IR.
61    fn into_ir(self) -> Select;
62}
63
64/// A typed shape that lowers to an [`OrderBy`] specification.
65pub trait OrderByInput {
66    /// The model this order shape applies to.
67    type Model: Model;
68    /// Lower this input to the runtime IR.
69    fn into_ir(self) -> OrderBy;
70}
71
72/// A typed shape that lowers to the `Data` payload for a `create`.
73///
74/// The associated `Data` type is the existing `<Model as CreateData>::Data`
75/// from `prax_query::traits::CreateData` — phase 5 will introduce a
76/// `NestedWritePlan` lowering path; phase 1 keeps the lowering simple.
77pub trait CreateInput {
78    /// The model this create input applies to.
79    type Model: Model;
80    /// The runtime payload type.
81    type Data: Send + Sync;
82    /// Lower this input to the runtime payload.
83    fn into_ir(self) -> Self::Data;
84}
85
86/// A typed shape that lowers to the `Data` payload for an `update`.
87pub trait UpdateInput {
88    /// The model this update input applies to.
89    type Model: Model;
90    /// The runtime payload type.
91    type Data: Send + Sync;
92    /// Lower this input to the runtime payload.
93    fn into_ir(self) -> Self::Data;
94}
95
96/// A typed shape that lowers to a `_count` aggregate selection.
97pub trait CountSelect {
98    /// The model this count selection applies to.
99    type Model: Model;
100    /// Concrete representation as a list of relation names to count.
101    fn into_relation_names(self) -> Vec<String>;
102}
103
104/// A typed shape that lowers to an aggregate spec
105/// (`_count` / `_avg` / `_sum` / `_min` / `_max`).
106///
107/// The IR target for this trait is finalized in phase 6 when aggregate
108/// macros are wired up. For phase 1 the trait only carries the `Model`
109/// associated type.
110pub trait AggregateInput {
111    /// The model this aggregate spec applies to.
112    type Model: Model;
113}
114
115/// A typed shape that lowers to a group-by spec.
116///
117/// As with [`AggregateInput`], the IR target is finalized in phase 6.
118pub trait GroupByInput {
119    /// The model this group-by spec applies to.
120    type Model: Model;
121}
122
123/// Pagination fragment shared by every read input.
124///
125/// Phase 1 keeps pagination on the operation itself (matching the
126/// current builder API). This struct exists so phase 3+ macros can
127/// surface `skip`/`take`/`cursor` inside the input AST without having
128/// to construct an entire `*Args`.
129#[derive(Debug, Clone, Default)]
130pub struct PaginationInput {
131    /// Number of rows to skip.
132    pub skip: Option<u64>,
133    /// Number of rows to take.
134    pub take: Option<u64>,
135}
136
137impl From<PaginationInput> for Pagination {
138    fn from(p: PaginationInput) -> Self {
139        let mut out = Pagination::new();
140        if let Some(n) = p.skip {
141            out = out.skip(n);
142        }
143        if let Some(n) = p.take {
144            out = out.take(n);
145        }
146        out
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    struct TestModel;
155    impl Model for TestModel {
156        const MODEL_NAME: &'static str = "TestModel";
157        const TABLE_NAME: &'static str = "test_models";
158        const PRIMARY_KEY: &'static [&'static str] = &["id"];
159        const COLUMNS: &'static [&'static str] = &["id"];
160    }
161
162    struct TestWhere;
163    impl WhereInput for TestWhere {
164        type Model = TestModel;
165        fn into_ir(self) -> Filter {
166            Filter::None
167        }
168    }
169
170    #[test]
171    fn where_input_lowers_to_filter_none() {
172        assert!(matches!(TestWhere.into_ir(), Filter::None));
173    }
174
175    #[test]
176    fn pagination_input_roundtrip() {
177        let p = PaginationInput {
178            skip: Some(5),
179            take: Some(10),
180        };
181        let raw: Pagination = p.into();
182        assert_eq!(raw.skip, Some(5));
183        assert_eq!(raw.take, Some(10));
184    }
185}