Skip to main content

umbral_core/orm/
mod.rs

1//! The ORM: declarative models, typed queries, and SQL generation.
2//!
3//! At M1 the design is intentionally narrow: one hardcoded model (`Post`),
4//! a single QuerySet type backed by sea-query, and basic predicates. No
5//! `Model` trait abstraction yet (that's M2), no derive macro (that's M3),
6//! no joins / aggregates / relations (later milestones). See
7//! `docs/specs/03-orm-querysets.md` for the target shape and the
8//! M1→M2→M3 progression.
9//!
10//! Module layout:
11//!
12//! - `post` — the hardcoded `Post` struct and its sibling column module.
13//! - `column` — column types (`StrCol`, `IntCol`, `NullableDateTimeCol`,
14//!   etc.) carrying inherent methods that build `Predicate`s.
15//! - `queryset` — `QuerySet<T>` and `Manager<T>`, the chainable / lazy
16//!   SQL builder plus its terminal methods.
17//!
18//! The shared types — `Predicate<T>` and `OrderExpr<T>` — live here in
19//! `mod.rs` so both `column` and `queryset` can reach them without
20//! crossing each other.
21
22pub mod aggregate;
23pub mod audit;
24pub mod choices;
25pub mod cleaners;
26pub mod column;
27pub mod dynamic;
28pub mod expr;
29pub mod file_field;
30pub mod foreign_key;
31pub mod forms_runtime;
32pub mod m2m;
33pub mod masked;
34pub mod model;
35pub mod multichoice;
36pub mod one_to_one;
37pub mod post;
38pub mod queryset;
39pub mod reverse_accessor;
40pub mod reverse_set;
41pub mod search;
42pub mod soft_delete_cascade;
43pub mod tsvector;
44pub mod validation;
45pub mod validators;
46pub mod write;
47
48use std::marker::PhantomData;
49use std::ops::{BitAnd, BitOr};
50
51pub use aggregate::{Aggregate, AggregateKind};
52
53/// Canonical string key for a primary-key (or FK) value, for bucketing
54/// relation children by their parent's PK in a `HashMap` / `HashSet`.
55///
56/// `serde_json::Value` is not `Hash`, and the relation-hydration paths
57/// need to group children by parent PK whatever the PK type — `i64`,
58/// `String`, `uuid::Uuid`. This is the **PK-agnostic** replacement for the
59/// historical `i64` keys: the value is namespaced by shape (`n:` number,
60/// `s:` string, `o:` other) so a numeric `42` and the string `"42"` never
61/// collide in the same bucket. Pairs with
62/// [`Model::pk_as_json`](crate::orm::Model::pk_as_json) and
63/// [`HydrateRelated::fk_id_for`](crate::orm::HydrateRelated::fk_id_for),
64/// both of which return a `serde_json::Value`.
65pub fn pk_key(value: &serde_json::Value) -> String {
66    match value {
67        serde_json::Value::Number(n) => format!("n:{n}"),
68        serde_json::Value::String(s) => format!("s:{s}"),
69        other => format!("o:{other}"),
70    }
71}
72
73/// Escape SQL `LIKE` wildcards in a user-supplied **literal** substring.
74///
75/// `contains` / `startswith` / `icontains` / the REST `__contains`
76/// family treat their argument as a literal to find, then wrap it in
77/// structural `%`. Without escaping, a user typing `%`, `_` or `\` would
78/// inject wildcards into the pattern — a search for `"100%"` matches
79/// every row starting with `100`, and `"a_b"` matches `axb` (ORM-1).
80/// This backslash-escapes the three LIKE metacharacters; the caller then
81/// adds its own structural `%` and pairs the predicate with
82/// `LikeExpr::escape('\\')` so the database honours the escape. Not SQL
83/// injection (the pattern is still a bound parameter) — a match-semantics
84/// correctness fix. The user-facing `.like()` / `.ilike()` builders take
85/// a raw pattern on purpose and must NOT call this.
86pub fn escape_like_literal(s: &str) -> String {
87    let mut out = String::with_capacity(s.len());
88    for ch in s.chars() {
89        if matches!(ch, '\\' | '%' | '_') {
90            out.push('\\');
91        }
92        out.push(ch);
93    }
94    out
95}
96
97/// A typed wrapper around a `sea_query::SelectStatement` for use in
98/// `col IN (SELECT col FROM ...)` predicates (gap #26).
99///
100/// Built by [`QuerySet::into_subquery`] or
101/// [`Manager::into_subquery`]; consumed by `IntCol::in_subquery` /
102/// `ForeignKeyCol::in_subquery` to produce a `Predicate`. The inner
103/// SelectStatement only knows the projected column the caller
104/// requested.
105pub struct Subquery {
106    inner: sea_query::SelectStatement,
107}
108
109impl Subquery {
110    /// Construct from a `SelectStatement` (internal — the
111    /// QuerySet/Manager helpers are the supported entry points).
112    pub(crate) fn from_select(inner: sea_query::SelectStatement) -> Self {
113        Self { inner }
114    }
115
116    /// Consume the wrapper and hand back the inner SelectStatement
117    /// — sea-query's `in_subquery` builder takes ownership.
118    pub(crate) fn into_statement(self) -> sea_query::SelectStatement {
119        self.inner
120    }
121}
122pub use choices::ChoiceField;
123pub use dynamic::{
124    CsvImportReport, DynError, DynQuerySet, decode_to_string, import_table_rows, never_matches,
125    typed_eq_condition, typed_json_value,
126};
127pub use expr::{F, FColExt, FExpr, Q};
128pub use file_field::{FileField, ImageField};
129pub use foreign_key::ForeignKey;
130pub use m2m::{M2M, load_junction_selection, set_junction_dynamic};
131pub use masked::{MaskError, MaskKeyring, Masked, set_mask_keyring};
132pub use model::{
133    ArrayElement, FieldSpec, FkAction, HydrateRelated, M2MRelationSpec, Model,
134    OneToOneRelationSpec, PrimaryKey, ReverseFkRelationSpec, SqlType,
135};
136pub use multichoice::MultiChoice;
137pub use one_to_one::OneToOne;
138pub use post::Post;
139pub use queryset::{GetError, JoinKind, Manager, QuerySet, QuerySetTx, TryForEachError};
140pub use reverse_accessor::{ReverseError, ReverseRelations};
141pub use reverse_set::ReverseSet;
142pub use search::{Search, SearchHit, SearchSources, Searchable};
143pub use tsvector::TsVector;
144pub use validators::{Email, Slug, Url, ValidatorError, validate_text_format};
145pub use write::{SaveError, slugify};
146
147/// A typed boolean condition on rows of `T`.
148///
149/// Built by inherent methods on the column types in `column` and passed
150/// to `QuerySet::filter` / `QuerySet::exclude` to constrain a query. The
151/// type parameter `T` ties the predicate to its model so a `Predicate<Post>`
152/// can't accidentally be applied to a `QuerySet<Comment>`.
153///
154/// `Clone` is implemented manually (rather than derived) so the bound does
155/// not bleed onto `T` — `sea_query::SimpleExpr` is `Clone` regardless of
156/// whether `T` is. The `get_or_create` / `update_or_create` convergence path
157/// needs to re-issue the same predicate after a `UniqueViolation` re-fetch.
158pub struct Predicate<T> {
159    /// The default condition. Renders correctly on Postgres and on
160    /// any backend whose operators match sea-query's defaults.
161    pub(crate) cond: sea_query::SimpleExpr,
162    /// Optional SQLite-specific override. Set by predicates that need
163    /// dialect-specific rendering — Phase 4.2.2 JSON operators are the
164    /// first consumer (`json_extract` instead of Postgres's `->` /
165    /// `->>`). When `None`, `cond` is used for both backends. The
166    /// QuerySet picks at terminal time based on the resolved pool
167    /// variant.
168    pub(crate) cond_sqlite: Option<sea_query::SimpleExpr>,
169    _phantom: PhantomData<T>,
170}
171
172impl<T> Predicate<T> {
173    /// Build a `col = value` predicate by column name. Use when the
174    /// column constant isn't reachable at the call site — typically
175    /// generic-over-`T` helper functions in plugin code (e.g.
176    /// `authenticate<U: UserModel>` filtering on `"username"` without
177    /// knowing `U`'s column module).
178    ///
179    /// The typed sibling-module path (`my_model::USERNAME.eq(...)`) is
180    /// preferred when you have a concrete `T`, because it catches typos
181    /// at compile time. This constructor is the escape hatch for
182    /// genuinely-generic code.
183    pub fn col_eq(col: &'static str, value: impl Into<sea_query::Value>) -> Self {
184        let expr = sea_query::Expr::col(sea_query::Alias::new(col)).eq(value);
185        Self::new(expr)
186    }
187
188    pub(crate) fn new(cond: sea_query::SimpleExpr) -> Self {
189        Self {
190            cond,
191            cond_sqlite: None,
192            _phantom: PhantomData,
193        }
194    }
195
196    /// Construct a predicate that renders differently on each backend.
197    /// Phase 4.2.2's JSON-operator path uses this to ship one
198    /// predicate that resolves to `col -> 'a' ->> 'b'` under Postgres
199    /// and `json_extract(col, '$.a.b')` under SQLite.
200    pub(crate) fn new_with_sqlite(
201        cond: sea_query::SimpleExpr,
202        cond_sqlite: sea_query::SimpleExpr,
203    ) -> Self {
204        Self {
205            cond,
206            cond_sqlite: Some(cond_sqlite),
207            _phantom: PhantomData,
208        }
209    }
210
211    /// Pick the SimpleExpr appropriate for `backend_name` (`"sqlite"`
212    /// or `"postgres"`). Falls back to the default `cond` when no
213    /// SQLite override is set or the backend isn't SQLite. Cloning
214    /// the SimpleExpr is cheap (it's a tree of small enum values).
215    pub(crate) fn cond_for(&self, backend_name: &str) -> sea_query::SimpleExpr {
216        match backend_name {
217            "sqlite" => self
218                .cond_sqlite
219                .clone()
220                .unwrap_or_else(|| self.cond.clone()),
221            _ => self.cond.clone(),
222        }
223    }
224}
225
226/// Manual `Clone` for `Predicate<T>`.
227///
228/// `sea_query::SimpleExpr` is `Clone` regardless of `T`, so we implement the
229/// trait by hand rather than deriving it. A derived impl would add an
230/// unnecessary `T: Clone` bound that would propagate to every QuerySet caller.
231impl<T> Clone for Predicate<T> {
232    fn clone(&self) -> Self {
233        Self {
234            cond: self.cond.clone(),
235            cond_sqlite: self.cond_sqlite.clone(),
236            _phantom: PhantomData,
237        }
238    }
239}
240
241/// Compose two predicates with logical AND. Both per-backend variants
242/// combine element-wise — if either side has a SQLite override, the
243/// combined predicate carries the AND of (lhs's sqlite-or-default)
244/// with (rhs's sqlite-or-default). When neither side overrides, the
245/// combined predicate keeps `cond_sqlite = None` so the default render
246/// path stays uniform.
247impl<T> BitAnd for Predicate<T> {
248    type Output = Predicate<T>;
249    fn bitand(self, rhs: Predicate<T>) -> Predicate<T> {
250        let any_sqlite_override = self.cond_sqlite.is_some() || rhs.cond_sqlite.is_some();
251        let combined_sqlite = if any_sqlite_override {
252            let lhs_sql = self
253                .cond_sqlite
254                .clone()
255                .unwrap_or_else(|| self.cond.clone());
256            let rhs_sql = rhs.cond_sqlite.clone().unwrap_or_else(|| rhs.cond.clone());
257            Some(lhs_sql.and(rhs_sql))
258        } else {
259            None
260        };
261        Predicate {
262            cond: self.cond.and(rhs.cond),
263            cond_sqlite: combined_sqlite,
264            _phantom: PhantomData,
265        }
266    }
267}
268
269/// Compose two predicates with logical OR. Same backend-variant story
270/// as [`BitAnd`].
271impl<T> BitOr for Predicate<T> {
272    type Output = Predicate<T>;
273    fn bitor(self, rhs: Predicate<T>) -> Predicate<T> {
274        let any_sqlite_override = self.cond_sqlite.is_some() || rhs.cond_sqlite.is_some();
275        let combined_sqlite = if any_sqlite_override {
276            let lhs_sql = self
277                .cond_sqlite
278                .clone()
279                .unwrap_or_else(|| self.cond.clone());
280            let rhs_sql = rhs.cond_sqlite.clone().unwrap_or_else(|| rhs.cond.clone());
281            Some(lhs_sql.or(rhs_sql))
282        } else {
283            None
284        };
285        Predicate {
286            cond: self.cond.or(rhs.cond),
287            cond_sqlite: combined_sqlite,
288            _phantom: PhantomData,
289        }
290    }
291}
292
293/// An ordering directive for one column.
294///
295/// Built by `.asc()` / `.desc()` on a column constant and passed to
296/// `QuerySet::order_by`. The type parameter `T` ties the directive to its
297/// model the same way `Predicate<T>` does.
298pub struct OrderExpr<T> {
299    pub(crate) column: &'static str,
300    pub(crate) descending: bool,
301    _phantom: PhantomData<T>,
302}
303
304impl<T> OrderExpr<T> {
305    pub(crate) fn new(column: &'static str, descending: bool) -> Self {
306        Self {
307            column,
308            descending,
309            _phantom: PhantomData,
310        }
311    }
312}