zino_orm/
entity.rs

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