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