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}