Skip to main content

sqlmodel_core/
hybrid.rs

1//! Hybrid properties: values computed in Rust that also have SQL expression equivalents.
2//!
3//! A hybrid property can be evaluated in Rust (e.g., `user.full_name()`)
4//! or translated to a SQL expression for use in queries
5//! (e.g., `User::full_name_expr()` → `first_name || ' ' || last_name`).
6//!
7//! # Example
8//!
9//! ```ignore
10//! #[derive(Model)]
11//! struct User {
12//!     first_name: String,
13//!     last_name: String,
14//!
15//!     #[sqlmodel(hybrid, sql = "first_name || ' ' || last_name")]
16//!     full_name: Hybrid<String>,
17//! }
18//!
19//! // Rust side: user.full_name holds the computed value
20//! // SQL side: User::full_name_expr() returns Expr::raw("first_name || ' ' || last_name")
21//! //
22//! // Query usage:
23//! // select!(User).filter(User::full_name_expr().eq("John Doe"))
24//! ```
25
26use std::fmt;
27use std::ops::Deref;
28
29use serde::{Deserialize, Serialize};
30
31use crate::value::Value;
32
33/// A hybrid property wrapper.
34///
35/// `Hybrid<T>` holds a Rust-computed value of type `T` and is treated as a
36/// computed field by the ORM (excluded from INSERT/UPDATE, initialized via
37/// `Default` on load). The macro generates a companion `_expr()` method
38/// that returns the SQL expression equivalent.
39///
40/// `Hybrid<T>` dereferences to `T` for ergonomic access.
41#[derive(Clone, PartialEq, Eq, Hash)]
42pub struct Hybrid<T> {
43    value: T,
44}
45
46impl<T> Hybrid<T> {
47    /// Create a new hybrid value.
48    pub fn new(value: T) -> Self {
49        Self { value }
50    }
51
52    /// Get the inner value.
53    pub fn into_inner(self) -> T {
54        self.value
55    }
56}
57
58impl<T: Default> Default for Hybrid<T> {
59    fn default() -> Self {
60        Self {
61            value: T::default(),
62        }
63    }
64}
65
66impl<T> Deref for Hybrid<T> {
67    type Target = T;
68
69    fn deref(&self) -> &T {
70        &self.value
71    }
72}
73
74impl<T: fmt::Debug> fmt::Debug for Hybrid<T> {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        self.value.fmt(f)
77    }
78}
79
80impl<T: fmt::Display> fmt::Display for Hybrid<T> {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        self.value.fmt(f)
83    }
84}
85
86impl<T: Serialize> Serialize for Hybrid<T> {
87    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
88        self.value.serialize(serializer)
89    }
90}
91
92impl<'de, T: Deserialize<'de>> Deserialize<'de> for Hybrid<T> {
93    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
94        T::deserialize(deserializer).map(Hybrid::new)
95    }
96}
97
98impl<T: Into<Value>> From<Hybrid<T>> for Value {
99    fn from(h: Hybrid<T>) -> Self {
100        h.value.into()
101    }
102}
103
104impl<T: Clone + Into<Value>> From<&Hybrid<T>> for Value {
105    fn from(h: &Hybrid<T>) -> Self {
106        h.value.clone().into()
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn test_hybrid_deref() {
116        let h = Hybrid::new("hello".to_string());
117        assert_eq!(h.as_str(), "hello");
118        assert_eq!(&*h, "hello");
119    }
120
121    #[test]
122    fn test_hybrid_default() {
123        let h: Hybrid<String> = Hybrid::default();
124        assert_eq!(&*h, "");
125    }
126
127    #[test]
128    fn test_hybrid_display() {
129        let h = Hybrid::new(42);
130        assert_eq!(format!("{}", h), "42");
131    }
132
133    #[test]
134    fn test_hybrid_into_value() {
135        let h = Hybrid::new("test".to_string());
136        let v: Value = h.into();
137        assert_eq!(v, Value::Text("test".to_string()));
138    }
139
140    #[test]
141    fn test_hybrid_serde_roundtrip() {
142        let h = Hybrid::new("hello".to_string());
143        let json = serde_json::to_string(&h).unwrap();
144        assert_eq!(json, "\"hello\"");
145        let back: Hybrid<String> = serde_json::from_str(&json).unwrap();
146        assert_eq!(&*back, "hello");
147    }
148}