reinhardt_db/orm/query_fields/lookup.rs
1//! Lookup type and value definitions
2
3use crate::orm::Model;
4use chrono::Timelike;
5use serde::{Deserialize, Serialize};
6
7/// Lookup type - defines how to compare the field value
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub enum LookupType {
10 // Equality
11 /// Exact variant.
12 Exact, // =
13 /// IExact variant.
14 IExact, // ILIKE (case-insensitive)
15 /// Ne variant.
16 Ne, // !=
17
18 // Pattern matching
19 /// Contains variant.
20 Contains, // LIKE '%x%'
21 /// IContains variant.
22 IContains, // ILIKE '%x%'
23 /// StartsWith variant.
24 StartsWith, // LIKE 'x%'
25 /// IStartsWith variant.
26 IStartsWith, // ILIKE 'x%'
27 /// EndsWith variant.
28 EndsWith, // LIKE '%x'
29 /// IEndsWith variant.
30 IEndsWith, // ILIKE '%x'
31 /// Regex variant.
32 Regex, // ~ (PostgreSQL)
33 /// IRegex variant.
34 IRegex, // ~* (PostgreSQL)
35
36 // Comparison
37 /// Gt variant.
38 Gt, // >
39 /// Gte variant.
40 Gte, // >=
41 /// Lt variant.
42 Lt, // <
43 /// Lte variant.
44 Lte, // <=
45 /// Range variant.
46 Range, // BETWEEN
47
48 // Set operations
49 /// In variant.
50 In, // IN
51 /// NotIn variant.
52 NotIn, // NOT IN
53
54 // NULL checks
55 /// IsNull variant.
56 IsNull, // IS NULL
57 /// IsNotNull variant.
58 IsNotNull, // IS NOT NULL
59}
60
61/// Lookup value - the value to compare against
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub enum LookupValue {
64 /// String variant.
65 String(String),
66 /// Int variant.
67 Int(i64),
68 /// Float variant.
69 Float(f64),
70 /// Bool variant.
71 Bool(bool),
72 /// Array variant.
73 Array(Vec<LookupValue>),
74 /// Range variant.
75 Range(Box<LookupValue>, Box<LookupValue>),
76 /// Null variant.
77 Null,
78}
79
80impl From<String> for LookupValue {
81 fn from(s: String) -> Self {
82 LookupValue::String(s)
83 }
84}
85
86impl From<&str> for LookupValue {
87 fn from(s: &str) -> Self {
88 LookupValue::String(s.to_string())
89 }
90}
91
92impl From<i32> for LookupValue {
93 fn from(i: i32) -> Self {
94 LookupValue::Int(i as i64)
95 }
96}
97
98impl From<i64> for LookupValue {
99 fn from(i: i64) -> Self {
100 LookupValue::Int(i)
101 }
102}
103
104impl From<f32> for LookupValue {
105 fn from(f: f32) -> Self {
106 LookupValue::Float(f as f64)
107 }
108}
109
110impl From<f64> for LookupValue {
111 fn from(f: f64) -> Self {
112 LookupValue::Float(f)
113 }
114}
115
116impl From<bool> for LookupValue {
117 fn from(b: bool) -> Self {
118 LookupValue::Bool(b)
119 }
120}
121
122impl From<()> for LookupValue {
123 fn from(_: ()) -> Self {
124 LookupValue::Null
125 }
126}
127
128// DateTime and Date conversions
129impl From<super::traits::DateTime> for LookupValue {
130 fn from(dt: super::traits::DateTime) -> Self {
131 LookupValue::Int(dt.timestamp)
132 }
133}
134
135impl From<super::traits::Date> for LookupValue {
136 fn from(date: super::traits::Date) -> Self {
137 // Encode as days since epoch or similar
138 let days = date.year * 10000 + (date.month as i32) * 100 + (date.day as i32);
139 LookupValue::Int(days as i64)
140 }
141}
142
143// chrono integration
144impl From<chrono::NaiveDateTime> for LookupValue {
145 fn from(dt: chrono::NaiveDateTime) -> Self {
146 LookupValue::Int(dt.and_utc().timestamp())
147 }
148}
149
150impl From<chrono::NaiveDate> for LookupValue {
151 fn from(date: chrono::NaiveDate) -> Self {
152 // Convert to timestamp at midnight UTC
153 LookupValue::Int(
154 date.and_hms_opt(0, 0, 0)
155 .map(|dt| dt.and_utc().timestamp())
156 .unwrap_or(0),
157 )
158 }
159}
160
161impl From<chrono::NaiveTime> for LookupValue {
162 fn from(time: chrono::NaiveTime) -> Self {
163 // Store as seconds since midnight
164 LookupValue::Int(time.num_seconds_from_midnight() as i64)
165 }
166}
167
168impl<Tz: chrono::TimeZone> From<chrono::DateTime<Tz>> for LookupValue {
169 fn from(dt: chrono::DateTime<Tz>) -> Self {
170 LookupValue::Int(dt.timestamp())
171 }
172}
173
174impl<T: Into<LookupValue>> From<(T, T)> for LookupValue {
175 fn from((start, end): (T, T)) -> Self {
176 LookupValue::Range(Box::new(start.into()), Box::new(end.into()))
177 }
178}
179
180/// A complete lookup specification ready to be compiled to SQL
181///
182/// # Breaking Change
183///
184/// The type of `field_path` has been changed from `Vec<&'static str>` to `Vec<String>`.
185/// This allows support for dynamic table aliases.
186#[derive(Debug, Clone)]
187pub struct Lookup<M: Model> {
188 pub(crate) field_path: Vec<String>,
189 pub(crate) lookup_type: LookupType,
190 pub(crate) value: LookupValue,
191 pub(crate) _phantom: std::marker::PhantomData<M>,
192}
193
194impl<M: Model> Lookup<M> {
195 /// Create a new lookup for field filtering in QuerySets
196 ///
197 /// # Examples
198 ///
199 /// ```rust
200 /// use reinhardt_db::orm::query_fields::{Lookup, LookupType, LookupValue};
201 /// # use reinhardt_db::orm::Model;
202 /// # use serde::{Serialize, Deserialize};
203 /// # #[derive(Clone, Serialize, Deserialize)]
204 /// # struct User { id: Option<i64> }
205 /// # #[derive(Clone)]
206 /// # struct UserFields;
207 /// # impl reinhardt_db::orm::FieldSelector for UserFields {
208 /// # fn with_alias(self, _alias: &str) -> Self { self }
209 /// # }
210 /// # impl Model for User {
211 /// # type PrimaryKey = i64;
212 /// # type Fields = UserFields;
213 /// # type Objects = reinhardt_db::orm::Manager<Self>;
214 /// # fn app_label() -> &'static str { "app" }
215 /// # fn table_name() -> &'static str { "users" }
216 /// # fn new_fields() -> Self::Fields { UserFields }
217 /// # fn primary_key(&self) -> Option<Self::PrimaryKey> { self.id }
218 /// # fn set_primary_key(&mut self, value: Self::PrimaryKey) { self.id = Some(value); }
219 /// # fn primary_key_field() -> &'static str { "id" }
220 /// # }
221 ///
222 /// let lookup = Lookup::<User>::new(
223 /// vec!["name".to_string()],
224 /// LookupType::Exact,
225 /// LookupValue::String("Alice".to_string())
226 /// );
227 /// // Represents: WHERE name = 'Alice'
228 /// assert_eq!(lookup.field_path(), &["name".to_string()]);
229 /// assert_eq!(*lookup.lookup_type(), LookupType::Exact);
230 /// ```
231 pub fn new(field_path: Vec<String>, lookup_type: LookupType, value: LookupValue) -> Self {
232 Self {
233 field_path,
234 lookup_type,
235 value,
236 _phantom: std::marker::PhantomData,
237 }
238 }
239 /// Get the field path
240 ///
241 pub fn field_path(&self) -> &[String] {
242 &self.field_path
243 }
244 /// Get the lookup type
245 ///
246 pub fn lookup_type(&self) -> &LookupType {
247 &self.lookup_type
248 }
249 /// Get the lookup value
250 ///
251 pub fn value(&self) -> &LookupValue {
252 &self.value
253 }
254}
255
256// Note on `Lookup<M>` vs. `Filter` (Issue #4650):
257//
258// The public-facing user-visible call site
259//
260// Choice::field_question_id().eq(question_id)
261//
262// goes through the `#[model]`-generated `FieldRef<M, T>::eq()`, which
263// already returns a `Filter` (see `crate::orm::expressions`). That
264// `Filter` flows directly into `Manager::filter(impl Into<FilterCondition>)` /
265// `QuerySet::filter(impl Into<FilterCondition>)`.
266//
267// The parallel `Field<M, T>::eq()` builder in this module returns a
268// `Lookup<M>` and is currently used only internally by
269// `query_fields::compiler` and `FilteredRelation`. A `Lookup<M>` →
270// `Filter` bridge is intentionally NOT provided yet — a faithful total
271// conversion would require lossy mapping of variants such as `IExact`,
272// `Regex`, and `Range` to a `FilterOperator` enum that does not have
273// those variants. Adding the missing `FilterOperator` /
274// `FilterValue::Range` variants (and the SQL-compile dispatch for them)
275// is tracked as a follow-up to Issue #4650 and will land additively.