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}