use super::{Aggregation, IntoSqlValue, query::QueryExt};
use std::{
fmt::{self, Display},
marker::PhantomData,
};
use zino_core::{
JsonValue,
model::{Model, Query},
};
pub trait Entity: Model {
type Column: ModelColumn<Self>;
const PRIMARY_KEY: Self::Column;
fn editable_columns() -> &'static [Self::Column];
fn generated_columns() -> &'static [Self::Column];
#[inline]
fn format_column(col: &Self::Column) -> String {
[Self::MODEL_NAME, ".", col.as_ref()].concat()
}
}
pub trait ModelColumn<E: Entity>: AsRef<str> + Display {
fn into_column_expr(self) -> String;
}
#[derive(Debug, Clone, PartialEq)]
pub struct DerivedColumn<E: Entity> {
expr: String,
phantom: PhantomData<E>,
}
impl<E: Entity> DerivedColumn<E> {
#[inline]
pub fn new(expr: String) -> Self {
Self {
expr,
phantom: PhantomData,
}
}
#[inline]
pub fn alias(alias: &str) -> Self {
Self::new(alias.to_owned())
}
#[inline]
pub fn coalesce<V: IntoSqlValue>(col: E::Column, value: V) -> Self {
let col_name = E::format_column(&col);
let field = Query::format_field(&col_name);
Self::coalesce_field(&field, value.into_sql_value())
}
#[inline]
pub fn coalesce_aggregation<V: IntoSqlValue>(aggregation: Aggregation<E>, value: V) -> Self {
let field = aggregation.expr();
Self::coalesce_field(&field, value.into_sql_value())
}
fn coalesce_field(field: &str, value: JsonValue) -> Self {
let expr = match value {
JsonValue::Null => format!("coalesce({field}, NULL)"),
JsonValue::Bool(b) => {
if b {
format!("coalesce({field}, TRUE)")
} else {
format!("coalesce({field}, FALSE)")
}
}
JsonValue::Number(n) => {
format!("coalesce({field}, {n})")
}
JsonValue::String(s) => {
let value = Query::escape_string(s);
format!("coalesce({field}, {value})")
}
value => {
let value = Query::escape_string(value);
format!("coalesce({field}, {value})")
}
};
Self::new(expr)
}
#[inline]
pub fn year(col: E::Column) -> Self {
let col_name = E::format_column(&col);
let field = Query::format_field(&col_name);
let expr = if cfg!(feature = "orm-sqlite") {
format!("strftime('%Y', {field}, 'localtime')")
} else {
format!("year({field})")
};
Self::new(expr)
}
#[inline]
pub fn year_month(col: E::Column) -> Self {
let col_name = E::format_column(&col);
let field = Query::format_field(&col_name);
let expr = if cfg!(any(
feature = "orm-mariadb",
feature = "orm-mysql",
feature = "orm-tidb"
)) {
format!("date_format({field}, '%Y-%m')")
} else if cfg!(feature = "orm-postgres") {
format!("to_char({field}, 'YYYY-MM')")
} else {
format!("strftime('%Y-%m', {field}, 'localtime')")
};
Self::new(expr)
}
#[inline]
pub fn date(col: E::Column) -> Self {
let col_name = E::format_column(&col);
let field = Query::format_field(&col_name);
let expr = if cfg!(feature = "orm-sqlite") {
format!("strftime('%Y-%m-%d', {field}, 'localtime')")
} else {
format!("date({field})")
};
Self::new(expr)
}
#[inline]
pub fn format_date_time(col: E::Column) -> Self {
let col_name = E::format_column(&col);
let field = Query::format_field(&col_name);
let expr = if cfg!(any(
feature = "orm-mariadb",
feature = "orm-mysql",
feature = "orm-tidb"
)) {
format!("date_format({field}, '%Y-%m-%d %H:%i:%s')")
} else if cfg!(feature = "orm-postgres") {
format!("to_char({field}, 'YYYY-MM-DD HH24:MI:SS')")
} else {
format!("strftime('%Y-%m-%d %H:%M:%S', {field}, 'localtime')")
};
Self::new(expr)
}
#[inline]
pub fn json_extract(col: E::Column, path: &str) -> Self {
let col_name = E::format_column(&col);
let field = Query::format_field(&col_name);
let expr = if cfg!(feature = "orm-postgres") {
let path = path.strip_prefix("$.").unwrap_or(path).replace('.', ", ");
format!(r#"({field} #>> '{{{path}}}')"#)
} else {
format!(r#"json_unquote(json_extract({field}, '{path}'))"#)
};
Self::new(expr)
}
}
impl<E: Entity> AsRef<str> for DerivedColumn<E> {
#[inline]
fn as_ref(&self) -> &str {
self.expr.as_str()
}
}
impl<E: Entity> Display for DerivedColumn<E> {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.expr.fmt(f)
}
}
impl<E: Entity> ModelColumn<E> for DerivedColumn<E> {
#[inline]
fn into_column_expr(self) -> String {
self.expr
}
}