Skip to main content

renox_core/db/
schema.rs

1//! A runtime description of a model's columns: the kind of each field's Rust
2//! type, which `db:check` compares with the real table.
3
4use chrono::{NaiveDate, NaiveDateTime, NaiveTime, TimeZone};
5
6use super::{Encrypted, Json, Ulid};
7
8/// The kind of value a model field stores, as far as the database cares.
9///
10/// `Unknown` is a field type Renox has no [`ColumnType`] for (a custom type);
11/// checks skip it.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13#[non_exhaustive]
14pub enum ColumnKind {
15    /// 64-bit integer (`i64`, `u32`).
16    BigInt,
17    /// 32-bit integer (`i32`, `u16`).
18    Int,
19    /// 16-bit integer or smaller (`i8`, `i16`, `u8`).
20    SmallInt,
21    /// `f64`.
22    Double,
23    /// `f32`.
24    Real,
25    /// Text (`String`, `Ulid`, `Encrypted<T>`, enums).
26    Text,
27    /// Bytes (`Vec<u8>`).
28    Blob,
29    /// `bool`.
30    Bool,
31    /// `chrono::DateTime<Tz>`.
32    DateTime,
33    /// `chrono::NaiveDateTime`.
34    NaiveDateTime,
35    /// `chrono::NaiveDate`.
36    Date,
37    /// `chrono::NaiveTime`.
38    Time,
39    /// `serde_json::Value` and `Json<T>`.
40    Json,
41    /// `uuid::Uuid`.
42    Uuid,
43    /// A type Renox doesn't know.
44    Unknown,
45}
46
47impl ColumnKind {
48    /// A lowercase name for messages.
49    pub fn as_str(&self) -> &'static str {
50        match self {
51            Self::BigInt => "bigint",
52            Self::Int => "int",
53            Self::SmallInt => "smallint",
54            Self::Double => "double",
55            Self::Real => "real",
56            Self::Text => "text",
57            Self::Blob => "blob",
58            Self::Bool => "bool",
59            Self::DateTime => "datetime",
60            Self::NaiveDateTime => "naive datetime",
61            Self::Date => "date",
62            Self::Time => "time",
63            Self::Json => "json",
64            Self::Uuid => "uuid",
65            Self::Unknown => "unknown",
66        }
67    }
68}
69
70/// A Rust type that can be a model field, and the kind of column it fills.
71/// Implemented for every type with a `ToDbValue` impl, plus `Option<T>`,
72/// `Json<T>`, `Encrypted<T>` and `Ulid`.
73pub trait ColumnType {
74    /// The kind of column this type is stored in.
75    const KIND: ColumnKind;
76}
77
78macro_rules! kinds {
79    ($($kind:ident => $($t:ty),*;)*) => {$($(
80        impl ColumnType for $t {
81            const KIND: ColumnKind = ColumnKind::$kind;
82        }
83    )*)*};
84}
85
86kinds! {
87    BigInt => i64, u32;
88    Int => i32, u16;
89    SmallInt => i8, i16, u8;
90    Double => f64;
91    Real => f32;
92    Text => String, Ulid;
93    Blob => Vec<u8>;
94    Bool => bool;
95    NaiveDateTime => NaiveDateTime;
96    Date => NaiveDate;
97    Time => NaiveTime;
98    Json => serde_json::Value;
99}
100
101#[cfg(feature = "uuid")]
102impl ColumnType for uuid::Uuid {
103    const KIND: ColumnKind = ColumnKind::Uuid;
104}
105
106impl<Tz: TimeZone> ColumnType for chrono::DateTime<Tz> {
107    const KIND: ColumnKind = ColumnKind::DateTime;
108}
109
110impl<T: ColumnType> ColumnType for Option<T> {
111    const KIND: ColumnKind = T::KIND;
112}
113
114impl<T> ColumnType for Json<T> {
115    const KIND: ColumnKind = ColumnKind::Json;
116}
117
118impl<T> ColumnType for Encrypted<T> {
119    const KIND: ColumnKind = ColumnKind::Text;
120}
121
122/// One column of a model, as the derive describes it.
123#[derive(Debug, Clone)]
124#[non_exhaustive]
125pub struct ModelColumn {
126    /// The column's name.
127    pub name: &'static str,
128    /// The field's Rust type, as written.
129    pub rust_type: &'static str,
130    /// What kind of column the type fills.
131    pub kind: ColumnKind,
132    /// Whether the field is an `Option`.
133    pub nullable: bool,
134    /// The SQL default from `#[model(default = "…")]`, if any.
135    pub default: Option<&'static str>,
136    /// The table from `#[model(references = "…")]`, if any.
137    pub references: Option<&'static str>,
138}
139
140impl ModelColumn {
141    /// Describes one column.
142    pub const fn new(
143        name: &'static str,
144        rust_type: &'static str,
145        kind: ColumnKind,
146        nullable: bool,
147    ) -> Self {
148        Self {
149            name,
150            rust_type,
151            kind,
152            nullable,
153            default: None,
154            references: None,
155        }
156    }
157
158    /// Sets the column's SQL default (`#[model(default = "0")]`).
159    pub const fn default_sql(mut self, sql: &'static str) -> Self {
160        self.default = Some(sql);
161        self
162    }
163
164    /// Sets the table the column points at (`#[model(references = "users")]`).
165    pub const fn references(mut self, table: &'static str) -> Self {
166        self.references = Some(table);
167        self
168    }
169}
170
171/// An index a model declares with `#[model(index(a, b))]` or
172/// `#[model(unique(a))]`.
173#[derive(Debug, Clone)]
174#[non_exhaustive]
175pub struct ModelIndex {
176    /// The indexed columns, in order.
177    pub columns: &'static [&'static str],
178    /// Whether the index is unique.
179    pub unique: bool,
180}
181
182impl ModelIndex {
183    /// Describes one index.
184    pub const fn new(columns: &'static [&'static str], unique: bool) -> Self {
185        Self { columns, unique }
186    }
187}
188
189#[doc(hidden)]
190pub struct ColumnProbe<T>(std::marker::PhantomData<fn() -> T>);
191impl<T> ColumnProbe<T> {
192    pub const fn new() -> Self {
193        Self(std::marker::PhantomData)
194    }
195}
196impl<T> Default for ColumnProbe<T> {
197    fn default() -> Self {
198        Self::new()
199    }
200}
201#[doc(hidden)]
202pub trait KnownColumn {
203    fn kind(&self) -> ColumnKind;
204}
205impl<T: ColumnType> KnownColumn for ColumnProbe<T> {
206    fn kind(&self) -> ColumnKind {
207        T::KIND
208    }
209}
210#[doc(hidden)]
211pub trait UnknownColumn {
212    fn kind(&self) -> ColumnKind;
213}
214impl<T> UnknownColumn for &ColumnProbe<T> {
215    fn kind(&self) -> ColumnKind {
216        ColumnKind::Unknown
217    }
218}
219
220/// A model registered with `App::model`, for `db:check`.
221#[derive(Clone)]
222#[allow(dead_code)] // read by the schema comparison (370.4)
223pub(crate) struct ModelInfo {
224    pub(crate) type_id: std::any::TypeId,
225    pub(crate) name: &'static str,
226    pub(crate) table: &'static str,
227    pub(crate) columns: fn() -> Vec<ModelColumn>,
228    pub(crate) indexes: fn() -> Vec<super::ModelIndex>,
229}
230
231/// Describes `M` for the registry.
232pub(crate) fn model_info<M: super::Model>() -> ModelInfo {
233    ModelInfo {
234        type_id: std::any::TypeId::of::<M>(),
235        name: std::any::type_name::<M>(),
236        table: M::TABLE,
237        columns: M::column_info,
238        indexes: M::indexes,
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    struct Custom;
247
248    #[test]
249    fn kinds_of_known_types() {
250        assert_eq!(<i64 as ColumnType>::KIND, ColumnKind::BigInt);
251        assert_eq!(<Option<String> as ColumnType>::KIND, ColumnKind::Text);
252        assert_eq!(<Json<Vec<i64>> as ColumnType>::KIND, ColumnKind::Json);
253        assert_eq!(ColumnKind::BigInt.as_str(), "bigint");
254    }
255
256    #[test]
257    #[allow(clippy::needless_borrow)]
258    fn probe_tells_known_from_unknown() {
259        #[allow(unused_imports)]
260        use super::{KnownColumn as _, UnknownColumn as _};
261        assert_eq!((&ColumnProbe::<i64>::new()).kind(), ColumnKind::BigInt);
262        assert_eq!((&ColumnProbe::<Custom>::new()).kind(), ColumnKind::Unknown);
263        assert_eq!(
264            (&ColumnProbe::<Option<Custom>>::new()).kind(),
265            ColumnKind::Unknown
266        );
267    }
268}