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
11pub trait Entity: Model {
13 type Column: ModelColumn<Self>;
15
16 const PRIMARY_KEY: Self::Column;
18
19 fn editable_columns() -> &'static [Self::Column];
21
22 fn generated_columns() -> &'static [Self::Column];
24
25 #[inline]
27 fn format_column(col: &Self::Column) -> String {
28 [Self::MODEL_NAME, ".", col.as_ref()].concat()
29 }
30}
31
32pub trait ModelColumn<E: Entity>: AsRef<str> + Display {
34 fn into_column_expr(self) -> String;
36}
37
38#[derive(Debug, Clone, PartialEq)]
40pub struct DerivedColumn<E: Entity> {
41 expr: String,
43 phantom: PhantomData<E>,
45}
46
47impl<E: Entity> DerivedColumn<E> {
48 #[inline]
50 pub fn new(expr: String) -> Self {
51 Self {
52 expr,
53 phantom: PhantomData,
54 }
55 }
56
57 #[inline]
59 pub fn alias(alias: &str) -> Self {
60 Self::new(alias.to_owned())
61 }
62
63 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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}