Skip to main content

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}