nautilus_core/args.rs
1//! Structured argument types for query operations.
2
3use std::collections::HashMap;
4
5use crate::{Expr, OrderBy, Value};
6
7/// Arguments for eagerly loading a single relation in a query.
8///
9/// ```text
10/// include: { posts: { where: { published: true } } }
11/// ```
12#[derive(Debug, Default, Clone)]
13pub struct IncludeRelation {
14 /// Optional filter to apply to the included child records.
15 pub where_: Option<Expr>,
16 /// ORDER BY clauses to apply to the included child records.
17 pub order_by: Vec<OrderBy>,
18 /// Maximum number of child rows to include.
19 pub take: Option<i32>,
20 /// Number of child rows to skip.
21 pub skip: Option<u32>,
22 /// Optional cursor for child pagination.
23 pub cursor: Option<HashMap<String, Value>>,
24 /// Columns to deduplicate child rows on.
25 pub distinct: Vec<String>,
26 /// Nested relations to include under this child relation.
27 pub include: HashMap<String, IncludeRelation>,
28}
29
30impl IncludeRelation {
31 /// Create a plain include with no child filter.
32 pub fn plain() -> Self {
33 Self::default()
34 }
35
36 /// Create an include with a child filter.
37 pub fn with_filter(filter: Expr) -> Self {
38 Self {
39 where_: Some(filter),
40 ..Self::default()
41 }
42 }
43
44 /// Append an ORDER BY clause for the included child records.
45 pub fn with_order_by(mut self, order: OrderBy) -> Self {
46 self.order_by.push(order);
47 self
48 }
49
50 /// Set the child LIMIT.
51 pub fn with_take(mut self, take: i32) -> Self {
52 self.take = Some(take);
53 self
54 }
55
56 /// Set the child OFFSET.
57 pub fn with_skip(mut self, skip: u32) -> Self {
58 self.skip = Some(skip);
59 self
60 }
61
62 /// Set the child cursor.
63 pub fn with_cursor(mut self, cursor: HashMap<String, Value>) -> Self {
64 self.cursor = Some(cursor);
65 self
66 }
67
68 /// Set child DISTINCT columns.
69 pub fn with_distinct(mut self, distinct: Vec<String>) -> Self {
70 self.distinct = distinct;
71 self
72 }
73
74 /// Add a nested relation include.
75 pub fn with_include(mut self, relation: impl Into<String>, include: IncludeRelation) -> Self {
76 self.include.insert(relation.into(), include);
77 self
78 }
79}
80
81/// Arguments accepted by `find_unique` and `find_unique_or_throw` delegate methods.
82///
83/// Uses a required `where_` filter (no ordering/pagination — implicit LIMIT 1).
84///
85/// # Example
86/// ```rust,ignore
87/// let args = FindUniqueArgs::new(
88/// User::columns().email.eq("alice@example.com"),
89/// );
90/// let user = client.user.find_unique(args).await?;
91/// ```
92#[derive(Debug, Clone)]
93pub struct FindUniqueArgs {
94 /// WHERE filter expression (required — must reference a unique/PK field).
95 pub where_: Expr,
96 /// Projection: only return the specified fields.
97 ///
98 /// If empty, all columns are returned. When specified, PK columns are
99 /// always included regardless. Cannot be used together with `include`.
100 pub select: HashMap<String, bool>,
101 /// Relations to eager-load for the matching record.
102 ///
103 /// Cannot be used together with `select`.
104 pub include: HashMap<String, IncludeRelation>,
105}
106
107impl FindUniqueArgs {
108 /// Construct with a required filter expression.
109 pub fn new(filter: Expr) -> Self {
110 FindUniqueArgs {
111 where_: filter,
112 select: HashMap::new(),
113 include: HashMap::new(),
114 }
115 }
116
117 /// Add a relation include.
118 pub fn with_include(mut self, relation: impl Into<String>, include: IncludeRelation) -> Self {
119 self.include.insert(relation.into(), include);
120 self
121 }
122
123 /// Select a scalar field.
124 pub fn with_select(mut self, field: impl Into<String>) -> Self {
125 self.select.insert(field.into(), true);
126 self
127 }
128}
129
130/// Arguments accepted by `find_many` and `find_first` delegate methods.
131///
132/// All fields are optional and default to "no constraint".
133///
134/// # Example
135/// ```rust,ignore
136/// let args = FindManyArgs {
137/// where_: Some(User::columns().email.eq("alice@example.com")),
138/// take: Some(10),
139/// ..Default::default()
140/// };
141/// let users = client.user.find_many(args).await?;
142/// ```
143#[derive(Debug, Default, Clone)]
144pub struct FindManyArgs {
145 /// Optional WHERE filter expression.
146 pub where_: Option<Expr>,
147 /// ORDER BY clauses (applied in order).
148 pub order_by: Vec<OrderBy>,
149 /// Maximum number of rows to return (LIMIT).
150 ///
151 /// Positive values paginate **forward**; negative values paginate
152 /// **backward** (reverses the result set in application code after
153 /// flipping `ORDER BY` directions — no DB-specific SQL needed).
154 /// Only meaningful when `cursor` is also set.
155 pub take: Option<i32>,
156 /// Number of rows to skip (OFFSET), applied relative to the cursor position
157 /// when `cursor` is set, or from the start of the result set otherwise.
158 pub skip: Option<u32>,
159 /// Relations to eager-load, with optional per-relation filters.
160 ///
161 /// Key is the relation field name (e.g. `"posts"`), value controls
162 /// how that relation is included (filter, etc.).
163 ///
164 /// Cannot be used together with `select`.
165 pub include: HashMap<String, IncludeRelation>,
166 /// Projection: only return the specified scalar fields.
167 ///
168 /// Key is the field name (logical name), value must be `true` to include
169 /// the field. PK fields are always returned regardless. When empty, all
170 /// columns are returned.
171 ///
172 /// Cannot be used together with `include`.
173 pub select: HashMap<String, bool>,
174 /// Cursor for stable (keyset) pagination.
175 ///
176 /// A map of **primary-key field name -> value** that identifies the record
177 /// from which the page should start. When
178 /// combined with `take` / `skip`, they are applied relative to this anchor
179 /// record rather than from the absolute start of the table.
180 ///
181 /// All primary-key fields of the model must be present in the map.
182 pub cursor: Option<HashMap<String, Value>>,
183 /// Columns to deduplicate on (SELECT DISTINCT / DISTINCT ON).
184 ///
185 /// Specifying one or more field names activates column-level deduplication:
186 /// - **Postgres**: rendered as `SELECT DISTINCT ON (col, ...)` with those
187 /// columns automatically prepended to `ORDER BY` as required by Postgres.
188 /// - **SQLite / MySQL**: rendered as plain `SELECT DISTINCT` (full-row
189 /// deduplication — most effective when combined with `select` projection).
190 ///
191 /// When empty (the default), no deduplication is applied.
192 pub distinct: Vec<String>,
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn include_relation_builder_methods_populate_all_fields() {
201 let cursor = HashMap::from([("id".to_string(), Value::I64(5))]);
202 let include =
203 IncludeRelation::with_filter(Expr::column("posts__published").eq(Expr::param(true)))
204 .with_order_by(OrderBy::desc("posts__created_at"))
205 .with_take(10)
206 .with_skip(2)
207 .with_cursor(cursor.clone())
208 .with_distinct(vec!["title".to_string()])
209 .with_include("comments", IncludeRelation::plain());
210
211 assert!(include.where_.is_some());
212 assert_eq!(include.order_by.len(), 1);
213 assert_eq!(include.take, Some(10));
214 assert_eq!(include.skip, Some(2));
215 assert_eq!(include.cursor, Some(cursor));
216 assert_eq!(include.distinct, vec!["title"]);
217 assert!(include.include.contains_key("comments"));
218 }
219
220 #[test]
221 fn find_unique_args_new_starts_without_projection_or_includes() {
222 let args = FindUniqueArgs::new(Expr::column("users__id").eq(Expr::param(1i64)));
223
224 assert!(args.select.is_empty());
225 assert!(args.include.is_empty());
226 }
227}