zino_orm/
entity.rs

1use super::{IntoSqlValue, query::QueryExt};
2use std::{
3    fmt::{self, Display},
4    marker::PhantomData,
5};
6use zino_core::{
7    JsonValue,
8    model::{Model, Query},
9};
10
11/// An interface for the model entity.
12pub trait Entity: Model {
13    /// The column type.
14    type Column: ModelColumn<Self>;
15
16    /// The primary key column.
17    const PRIMARY_KEY: Self::Column;
18
19    /// Formats the column name.
20    #[inline]
21    fn format_column(col: &Self::Column) -> String {
22        [Self::MODEL_NAME, ".", col.as_ref()].concat()
23    }
24}
25
26/// An interface for the model column.
27pub trait ModelColumn<E: Entity>: AsRef<str> + Display {
28    /// Converts `self` into a column expression.
29    fn into_column_expr(self) -> String;
30}
31
32/// A column computed dynamically based on other columns or expressions.
33#[derive(Debug, Clone, PartialEq)]
34pub struct DerivedColumn<E: Entity> {
35    /// The column expression.
36    expr: String,
37    /// The phantom data.
38    phantom: PhantomData<E>,
39}
40
41impl<E: Entity> DerivedColumn<E> {
42    /// Creates a new instance.
43    #[inline]
44    pub fn new(expr: String) -> Self {
45        Self {
46            expr,
47            phantom: PhantomData,
48        }
49    }
50
51    /// Constructs an instance for the column alias.
52    #[inline]
53    pub fn alias(alias: &str) -> Self {
54        Self::new(alias.to_owned())
55    }
56
57    /// Constructs an instance using `COALESCE` to provide a default value for the column.
58    pub fn coalesce<V: IntoSqlValue>(col: E::Column, value: V) -> Self {
59        let col_name = E::format_column(&col);
60        let field = Query::format_field(&col_name);
61        let expr = match value.into_sql_value() {
62            JsonValue::Null => format!("coalesce({field}, NULL)"),
63            JsonValue::Bool(b) => {
64                if b {
65                    format!("coalesce({field}, TRUE)")
66                } else {
67                    format!("coalesce({field}, FALSE)")
68                }
69            }
70            JsonValue::Number(n) => {
71                format!("coalesce({field}, {n})")
72            }
73            JsonValue::String(s) => {
74                let value = Query::escape_string(s);
75                format!("coalesce({field}, {value})")
76            }
77            value => {
78                let value = Query::escape_string(value);
79                format!("coalesce({field}, {value})")
80            }
81        };
82        Self::new(expr)
83    }
84
85    /// Constructs an instance for extracting the year from a column.
86    #[inline]
87    pub fn year(col: E::Column) -> Self {
88        let col_name = E::format_column(&col);
89        let field = Query::format_field(&col_name);
90        let expr = if cfg!(feature = "orm-sqlite") {
91            format!("strftime('%Y', {field})")
92        } else {
93            format!("year({field})")
94        };
95        Self::new(expr)
96    }
97
98    /// Constructs an instance for extracting the year-month from a column.
99    #[inline]
100    pub fn year_month(col: E::Column) -> Self {
101        let col_name = E::format_column(&col);
102        let field = Query::format_field(&col_name);
103        let expr = if cfg!(any(
104            feature = "orm-mariadb",
105            feature = "orm-mysql",
106            feature = "orm-tidb"
107        )) {
108            format!("date_format({field}, '%Y-%m')")
109        } else if cfg!(feature = "orm-postgres") {
110            format!("to_char({field}, 'YYYY-MM')")
111        } else {
112            format!("strftime('%Y-%m', {field})")
113        };
114        Self::new(expr)
115    }
116
117    /// Constructs an instance for extracting the date from a column.
118    #[inline]
119    pub fn date(col: E::Column) -> Self {
120        let col_name = E::format_column(&col);
121        let field = Query::format_field(&col_name);
122        Self::new(format!("date({field})"))
123    }
124
125    /// Constructs an instance for formating a date-time as `%Y-%m-%d %H:%M:%S`.
126    #[inline]
127    pub fn format_date_time(col: E::Column) -> Self {
128        let col_name = E::format_column(&col);
129        let field = Query::format_field(&col_name);
130        let expr = if cfg!(any(
131            feature = "orm-mariadb",
132            feature = "orm-mysql",
133            feature = "orm-tidb"
134        )) {
135            format!("date_format({field}, '%Y-%m-%d %H:%i:%s')")
136        } else if cfg!(feature = "orm-postgres") {
137            format!("to_char({field}, 'YYYY-MM-DD HH24:MI:SS')")
138        } else {
139            format!("strftime('%Y-%m-%d %H:%M:%S', {field})")
140        };
141        Self::new(expr)
142    }
143
144    /// Constructs an instance for extracting values from a JSON column.
145    #[inline]
146    pub fn json_extract(col: E::Column, path: &str) -> Self {
147        let col_name = E::format_column(&col);
148        let field = Query::format_field(&col_name);
149        let expr = if cfg!(feature = "orm-postgres") {
150            let path = path.strip_prefix("$.").unwrap_or(path).replace('.', ", ");
151            format!(r#"({field} #>> '{{{path}}}')"#)
152        } else {
153            format!(r#"json_unquote(json_extract({field}, '{path}'))"#)
154        };
155        Self::new(expr)
156    }
157}
158
159impl<E: Entity> AsRef<str> for DerivedColumn<E> {
160    #[inline]
161    fn as_ref(&self) -> &str {
162        self.expr.as_str()
163    }
164}
165
166impl<E: Entity> Display for DerivedColumn<E> {
167    #[inline]
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        self.expr.fmt(f)
170    }
171}
172
173impl<E: Entity> ModelColumn<E> for DerivedColumn<E> {
174    #[inline]
175    fn into_column_expr(self) -> String {
176        self.expr
177    }
178}