Skip to main content

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.