siderite_orm/model.rs
1//! The [`Model`] trait and its static [`ModelMeta`].
2//!
3//! `#[derive(Model)]` generates everything here; the ORM, the migration
4//! autodetector and the query compiler read the same `&'static` metadata, so
5//! there is one source of truth and no runtime registration.
6
7use crate::backend::Row;
8use crate::db::Db;
9use crate::error::QueryError;
10use crate::plan::OrderDirection;
11use crate::queryset::QuerySet;
12use crate::types::{DbType, SqlType};
13use crate::value::Value;
14
15/// What happens to rows referencing a deleted row (Django `on_delete`).
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub enum OnDelete {
18 /// Delete referencing rows too.
19 #[default]
20 Cascade,
21 /// Refuse the delete while references exist.
22 Protect,
23 /// Set the reference to `NULL` (field must be nullable).
24 SetNull,
25 /// Set the reference to the column default.
26 SetDefault,
27 /// Leave referencing rows untouched (the database may still enforce).
28 DoNothing,
29}
30
31/// Kind of a column-backed relation.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum RelationKind {
34 /// Many-to-one.
35 ForeignKey,
36 /// One-to-one (a unique foreign key).
37 OneToOne,
38}
39
40/// Lazily resolved metadata of another model.
41///
42/// A function pointer (not a `&'static ModelMeta`) so that models may
43/// reference each other in cycles without const-evaluation loops.
44pub type MetaRef = fn() -> &'static ModelMeta;
45
46/// Relation carried by a foreign-key column.
47#[derive(Debug, Clone, Copy)]
48pub struct RelationMeta {
49 /// FK or one-to-one.
50 pub kind: RelationKind,
51 /// Referenced model.
52 pub target: MetaRef,
53 /// Deletion policy.
54 pub on_delete: OnDelete,
55 /// Reverse accessor name on the target (`posts`), if any.
56 pub related_name: Option<&'static str>,
57}
58
59/// A server-side column default (used by migrations and inserts).
60#[derive(Debug, Clone, Copy, PartialEq)]
61pub enum DbDefault {
62 /// Current timestamp at insert time (`auto_now_add`).
63 Now,
64 /// A literal SQL-neutral value.
65 Int(i64),
66 /// A literal boolean.
67 Bool(bool),
68 /// A literal string.
69 Text(&'static str),
70}
71
72/// Static metadata of one concrete column.
73#[derive(Debug, Clone, Copy)]
74pub struct FieldMeta {
75 /// Rust field name.
76 pub name: &'static str,
77 /// Column name (`author_id` for a `ForeignKey` field named `author`).
78 pub column: &'static str,
79 /// Column type family.
80 pub sql_type: SqlType,
81 /// Accepts `NULL`.
82 pub nullable: bool,
83 /// Part of the primary key.
84 pub primary_key: bool,
85 /// Value generated by the database (autoincrement / identity).
86 pub auto: bool,
87 /// `UNIQUE`.
88 pub unique: bool,
89 /// Single-column index.
90 pub index: bool,
91 /// `VARCHAR(n)`.
92 pub max_length: Option<u32>,
93 /// Decimal precision.
94 pub max_digits: Option<u32>,
95 /// Decimal scale.
96 pub decimal_places: Option<u32>,
97 /// Database default.
98 pub default: Option<DbDefault>,
99 /// Refresh to "now" on every save (`auto_now`).
100 pub auto_now: bool,
101 /// Foreign-key relation.
102 pub relation: Option<RelationMeta>,
103}
104
105impl FieldMeta {
106 /// A plain non-null column of `sql_type`; the derive overrides the rest.
107 pub const fn new(name: &'static str, column: &'static str, sql_type: SqlType) -> Self {
108 Self {
109 name,
110 column,
111 sql_type,
112 nullable: false,
113 primary_key: false,
114 auto: false,
115 unique: false,
116 index: false,
117 max_length: None,
118 max_digits: None,
119 decimal_places: None,
120 default: None,
121 auto_now: false,
122 relation: None,
123 }
124 }
125}
126
127/// A many-to-many relation stored in a join table.
128#[derive(Debug, Clone, Copy)]
129pub struct ManyToManyMeta {
130 /// Rust field name on the declaring model.
131 pub name: &'static str,
132 /// Target model.
133 pub target: MetaRef,
134 /// Join table (`post_tags` by default, or the explicit through model's table).
135 pub through_table: &'static str,
136 /// Join-table column pointing at the declaring model.
137 pub source_column: &'static str,
138 /// Join-table column pointing at the target model.
139 pub target_column: &'static str,
140 /// Explicit through model, if declared; `None` means an auto-created table.
141 pub through: Option<MetaRef>,
142 /// Reverse accessor name on the target, if any.
143 pub related_name: Option<&'static str>,
144}
145
146/// A named multi-column index.
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148pub struct IndexMeta {
149 /// Index name.
150 pub name: &'static str,
151 /// Columns in order.
152 pub columns: &'static [&'static str],
153 /// `UNIQUE` index.
154 pub unique: bool,
155}
156
157/// A table-level constraint.
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159pub enum ConstraintMeta {
160 /// `UNIQUE (a, b)`.
161 Unique {
162 /// Constraint name.
163 name: &'static str,
164 /// Columns.
165 columns: &'static [&'static str],
166 },
167 /// `CHECK (<sql>)`. The SQL is written by the model author, not user input.
168 Check {
169 /// Constraint name.
170 name: &'static str,
171 /// Boolean SQL expression.
172 sql: &'static str,
173 },
174}
175
176/// Static metadata of a model (Django `Meta` plus the field list).
177#[derive(Debug, Clone, Copy)]
178pub struct ModelMeta {
179 /// Rust type name (`User`); also the migration model name.
180 pub name: &'static str,
181 /// Table name.
182 pub table: &'static str,
183 /// Concrete columns in declaration order.
184 pub fields: &'static [FieldMeta],
185 /// Many-to-many relations (not columns).
186 pub many_to_many: &'static [ManyToManyMeta],
187 /// Default ordering as `(column, direction)`.
188 pub ordering: &'static [(&'static str, OrderDirection)],
189 /// Named indexes.
190 pub indexes: &'static [IndexMeta],
191 /// Table constraints.
192 pub constraints: &'static [ConstraintMeta],
193 /// Whether migrations manage this table (`false` for unmanaged tables/views).
194 pub managed: bool,
195}
196
197impl ModelMeta {
198 /// The primary-key column.
199 ///
200 /// The derive rejects models without exactly one primary key, so this is
201 /// only `None` for hand-written, invalid metadata.
202 pub fn pk(&self) -> Option<&'static FieldMeta> {
203 self.fields.iter().find(|f| f.primary_key)
204 }
205
206 /// Field by Rust name.
207 pub fn field(&self, name: &str) -> Option<&'static FieldMeta> {
208 self.fields.iter().find(|f| f.name == name)
209 }
210
211 /// Field by column name.
212 pub fn column(&self, column: &str) -> Option<&'static FieldMeta> {
213 self.fields.iter().find(|f| f.column == column)
214 }
215}
216
217/// A database-backed model.
218///
219/// Implemented by `#[derive(Model)]`. Hand-written implementations must keep
220/// [`to_values`](Self::to_values) and [`from_row`](Self::from_row) consistent
221/// with [`META`](Self::META).
222pub trait Model: Sized + Send + Sync + 'static {
223 /// Primary-key type (`i64`, `Uuid`, ...).
224 type Pk: DbType + Clone + PartialEq + std::fmt::Debug;
225
226 /// Static metadata.
227 const META: &'static ModelMeta;
228
229 /// The primary-key value.
230 fn pk(&self) -> Self::Pk;
231
232 /// Store a primary key generated by the database after an insert.
233 ///
234 /// # Errors
235 /// [`QueryError::Decode`] if `value` does not fit the pk type.
236 fn set_pk(&mut self, value: Value) -> Result<(), QueryError>;
237
238 /// Whether `save` must INSERT: true for an auto primary key that still
239 /// holds its default (e.g. `0`). Models with a manual key return `false`;
240 /// `save` then tries UPDATE and falls back to INSERT.
241 fn is_unsaved(&self) -> bool;
242
243 /// Every column in [`ModelMeta::fields`] order, as `(column, value)`.
244 fn to_values(&self) -> Vec<(&'static str, Value)>;
245
246 /// Decode a model from `row`.
247 ///
248 /// Columns are looked up as `prefix + column`: `prefix` is `""` for the
249 /// root model and `"author__"` for a model loaded via `select_related`.
250 ///
251 /// # Errors
252 /// [`QueryError::Decode`] naming the missing or mistyped column.
253 fn from_row(row: &Row, prefix: &str) -> Result<Self, QueryError>;
254
255 /// Lazy queryset over this model (Django `Model.objects`).
256 fn objects(db: &Db) -> QuerySet<Self> {
257 QuerySet::new(db)
258 }
259}
260
261/// Read `prefix + column` from `row`, decoding it as `T`.
262///
263/// # Errors
264/// [`QueryError::Decode`] if the column is missing or has the wrong type.
265pub fn read_column<T: DbType>(row: &Row, prefix: &str, column: &str) -> Result<T, QueryError> {
266 let key = if prefix.is_empty() {
267 std::borrow::Cow::Borrowed(column)
268 } else {
269 std::borrow::Cow::Owned(format!("{prefix}{column}"))
270 };
271 let value = row.get(&key).cloned().ok_or_else(|| QueryError::Decode {
272 column: key.clone().into_owned(),
273 reason: "column missing from result".into(),
274 })?;
275 crate::types::decode(&key, value)
276}