Skip to main content

prax_query/inputs/
relation.rs

1//! Relation-aware filter wrappers.
2//!
3//! `ListRelationFilter<W>` and `SingleRelationFilter<W>` carry the
4//! Prisma operator shape (`some`/`every`/`none` for to-many;
5//! `is`/`is_not` for to-one). They lower to [`Filter::ScalarSubquery`]
6//! fragments via a per-relation [`RelationFilterMeta`] adapter that codegen
7//! emits (phase 2) but tests / hand-built users can supply directly.
8
9use crate::filter::{Filter, FilterValue};
10use crate::inputs::scalar::combine_filters;
11use crate::inputs::traits::WhereInput;
12
13/// Static metadata for one parent→child relation, used when lowering
14/// relation filters to EXISTS / NOT EXISTS subqueries.
15///
16/// Phase 2 codegen emits one impl per relation declared in the schema.
17/// Hand-rolled callers can implement this trait themselves.
18///
19/// Distinct from `crate::relations::RelationMeta`, which carries runtime-loader metadata.
20pub trait RelationFilterMeta {
21    /// Parent SQL table name.
22    const PARENT_TABLE: &'static str;
23    /// Parent primary-key column name.
24    const PARENT_PK: &'static str;
25    /// Child SQL table name.
26    const CHILD_TABLE: &'static str;
27    /// Child foreign-key column name pointing back at the parent.
28    const CHILD_FK: &'static str;
29}
30
31/// Filter operators for a to-many relation.
32#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
33#[serde(rename_all = "snake_case")]
34#[serde(bound = "W: serde::Serialize + for<'de2> serde::Deserialize<'de2>")]
35pub struct ListRelationFilter<W> {
36    /// At least one child matches `W`.
37    pub some: Option<W>,
38    /// Every existing child matches `W`.
39    pub every: Option<W>,
40    /// No child matches `W`.
41    pub none: Option<W>,
42}
43
44/// Filter operators for a to-one relation.
45#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
46#[serde(rename_all = "snake_case")]
47#[serde(bound = "W: serde::Serialize + for<'de2> serde::Deserialize<'de2>")]
48pub struct SingleRelationFilter<W> {
49    /// The related row matches `W`.
50    pub is: Option<W>,
51    /// The related row does NOT match `W` (or doesn't exist).
52    pub is_not: Option<W>,
53}
54
55/// Lowering helper: produces `Filter::ScalarSubquery` from a relation
56/// filter + [`RelationFilterMeta`].
57///
58/// Implemented for any `W: WhereInput` so neither the codegen nor the
59/// macro layer needs to manually thread metadata.
60pub trait LowerRelationFilter {
61    /// Lower this relation filter to a runtime [`Filter`] using the
62    /// supplied metadata.
63    fn lower<M: RelationFilterMeta>(self) -> Filter;
64}
65
66/// Walk a `Filter` tree and produce inline SQL with `{N}` placeholders.
67///
68/// Phase 1 supports the operators that the scalar filters emit.
69/// `ScalarSubquery` nesting is not supported here and panics — the outer
70/// `Filter::to_sql_with_params` is the only place that splices subquery SQL.
71fn render_inline_filter(inner: Filter) -> (String, Vec<FilterValue>) {
72    let mut sql = String::new();
73    let mut params = Vec::<FilterValue>::new();
74    write_filter(&inner, &mut sql, &mut params);
75    (sql, params)
76}
77
78fn write_filter(f: &Filter, sql: &mut String, params: &mut Vec<FilterValue>) {
79    match f {
80        Filter::None => sql.push_str("TRUE"),
81        Filter::Equals(c, v) => {
82            if matches!(v, FilterValue::Null) {
83                sql.push_str(&format!("{} IS NULL", c));
84            } else {
85                let idx = params.len();
86                params.push(v.clone());
87                sql.push_str(&format!("{} = {{{}}}", c, idx));
88            }
89        }
90        Filter::NotEquals(c, v) => {
91            if matches!(v, FilterValue::Null) {
92                sql.push_str(&format!("{} IS NOT NULL", c));
93            } else {
94                let idx = params.len();
95                params.push(v.clone());
96                sql.push_str(&format!("{} <> {{{}}}", c, idx));
97            }
98        }
99        Filter::Lt(c, v) => {
100            let i = params.len();
101            params.push(v.clone());
102            sql.push_str(&format!("{} < {{{}}}", c, i));
103        }
104        Filter::Lte(c, v) => {
105            let i = params.len();
106            params.push(v.clone());
107            sql.push_str(&format!("{} <= {{{}}}", c, i));
108        }
109        Filter::Gt(c, v) => {
110            let i = params.len();
111            params.push(v.clone());
112            sql.push_str(&format!("{} > {{{}}}", c, i));
113        }
114        Filter::Gte(c, v) => {
115            let i = params.len();
116            params.push(v.clone());
117            sql.push_str(&format!("{} >= {{{}}}", c, i));
118        }
119        Filter::IsNull(c) => sql.push_str(&format!("{} IS NULL", c)),
120        Filter::IsNotNull(c) => sql.push_str(&format!("{} IS NOT NULL", c)),
121        Filter::Contains(c, FilterValue::String(s)) => {
122            let i = params.len();
123            params.push(FilterValue::String(format!("%{}%", s)));
124            sql.push_str(&format!("{} LIKE {{{}}}", c, i));
125        }
126        Filter::StartsWith(c, FilterValue::String(s)) => {
127            let i = params.len();
128            params.push(FilterValue::String(format!("{}%", s)));
129            sql.push_str(&format!("{} LIKE {{{}}}", c, i));
130        }
131        Filter::EndsWith(c, FilterValue::String(s)) => {
132            let i = params.len();
133            params.push(FilterValue::String(format!("%{}", s)));
134            sql.push_str(&format!("{} LIKE {{{}}}", c, i));
135        }
136        Filter::Contains(_, _) | Filter::StartsWith(_, _) | Filter::EndsWith(_, _) => {
137            panic!(
138                "inline relation-filter lowering requires String values for Contains / StartsWith / EndsWith \
139                 (other FilterValue variants must be expressed via Equals / In)"
140            );
141        }
142        Filter::In(c, values) => {
143            if values.is_empty() {
144                sql.push_str("FALSE");
145                return;
146            }
147            sql.push_str(&format!("{} IN (", c));
148            for (n, v) in values.iter().enumerate() {
149                if n > 0 {
150                    sql.push_str(", ");
151                }
152                let i = params.len();
153                params.push(v.clone());
154                sql.push_str(&format!("{{{}}}", i));
155            }
156            sql.push(')');
157        }
158        Filter::NotIn(c, values) => {
159            if values.is_empty() {
160                sql.push_str("TRUE");
161                return;
162            }
163            sql.push_str(&format!("{} NOT IN (", c));
164            for (n, v) in values.iter().enumerate() {
165                if n > 0 {
166                    sql.push_str(", ");
167                }
168                let i = params.len();
169                params.push(v.clone());
170                sql.push_str(&format!("{{{}}}", i));
171            }
172            sql.push(')');
173        }
174        Filter::And(parts) => {
175            if parts.is_empty() {
176                sql.push_str("TRUE");
177                return;
178            }
179            sql.push('(');
180            for (n, p) in parts.iter().enumerate() {
181                if n > 0 {
182                    sql.push_str(" AND ");
183                }
184                write_filter(p, sql, params);
185            }
186            sql.push(')');
187        }
188        Filter::Or(parts) => {
189            if parts.is_empty() {
190                sql.push_str("FALSE");
191                return;
192            }
193            sql.push('(');
194            for (n, p) in parts.iter().enumerate() {
195                if n > 0 {
196                    sql.push_str(" OR ");
197                }
198                write_filter(p, sql, params);
199            }
200            sql.push(')');
201        }
202        Filter::Not(inner) => {
203            sql.push_str("NOT (");
204            write_filter(inner, sql, params);
205            sql.push(')');
206        }
207        Filter::ScalarSubquery { .. } => {
208            panic!(
209                "inline relation-filter lowering does not support nested ScalarSubquery \
210                 (the outer to_sql_with_params handles ScalarSubquery; relation bodies must lower to leaf filters first)"
211            );
212        }
213    }
214}
215
216impl<W: WhereInput> LowerRelationFilter for ListRelationFilter<W> {
217    fn lower<M: RelationFilterMeta>(self) -> Filter {
218        let mut clauses: Vec<Filter> = Vec::new();
219
220        if let Some(w) = self.some {
221            let (body, params) = render_inline_filter(w.into_ir());
222            let sql = format!(
223                "EXISTS (SELECT 1 FROM {} WHERE {}.{} = {}.{} AND {})",
224                M::CHILD_TABLE,
225                M::CHILD_TABLE,
226                M::CHILD_FK,
227                M::PARENT_TABLE,
228                M::PARENT_PK,
229                body,
230            );
231            clauses.push(Filter::ScalarSubquery {
232                sql: sql.into(),
233                params,
234            });
235        }
236
237        if let Some(w) = self.every {
238            let (body, params) = render_inline_filter(w.into_ir());
239            let sql = format!(
240                "NOT EXISTS (SELECT 1 FROM {} WHERE {}.{} = {}.{} AND NOT ({}))",
241                M::CHILD_TABLE,
242                M::CHILD_TABLE,
243                M::CHILD_FK,
244                M::PARENT_TABLE,
245                M::PARENT_PK,
246                body,
247            );
248            clauses.push(Filter::ScalarSubquery {
249                sql: sql.into(),
250                params,
251            });
252        }
253
254        if let Some(w) = self.none {
255            let (body, params) = render_inline_filter(w.into_ir());
256            let sql = format!(
257                "NOT EXISTS (SELECT 1 FROM {} WHERE {}.{} = {}.{} AND {})",
258                M::CHILD_TABLE,
259                M::CHILD_TABLE,
260                M::CHILD_FK,
261                M::PARENT_TABLE,
262                M::PARENT_PK,
263                body,
264            );
265            clauses.push(Filter::ScalarSubquery {
266                sql: sql.into(),
267                params,
268            });
269        }
270
271        combine_filters(clauses)
272    }
273}
274
275impl<W: WhereInput> LowerRelationFilter for SingleRelationFilter<W> {
276    fn lower<M: RelationFilterMeta>(self) -> Filter {
277        let mut clauses: Vec<Filter> = Vec::new();
278
279        if let Some(w) = self.is {
280            let (body, params) = render_inline_filter(w.into_ir());
281            let sql = format!(
282                "EXISTS (SELECT 1 FROM {} WHERE {}.{} = {}.{} AND {})",
283                M::CHILD_TABLE,
284                M::CHILD_TABLE,
285                M::CHILD_FK,
286                M::PARENT_TABLE,
287                M::PARENT_PK,
288                body,
289            );
290            clauses.push(Filter::ScalarSubquery {
291                sql: sql.into(),
292                params,
293            });
294        }
295
296        if let Some(w) = self.is_not {
297            let (body, params) = render_inline_filter(w.into_ir());
298            let sql = format!(
299                "NOT EXISTS (SELECT 1 FROM {} WHERE {}.{} = {}.{} AND {})",
300                M::CHILD_TABLE,
301                M::CHILD_TABLE,
302                M::CHILD_FK,
303                M::PARENT_TABLE,
304                M::PARENT_PK,
305                body,
306            );
307            clauses.push(Filter::ScalarSubquery {
308                sql: sql.into(),
309                params,
310            });
311        }
312
313        combine_filters(clauses)
314    }
315}