Skip to main content

fraiseql_db/types/
db_types.rs

1//! Database types and data structures.
2
3#[cfg(feature = "postgres")]
4use bytes::BytesMut;
5use serde::{Deserialize, Serialize};
6#[cfg(feature = "postgres")]
7use tokio_postgres::types::{IsNull, ToSql, Type};
8
9/// Database types supported by FraiseQL.
10///
11/// # Stability
12///
13/// This enum is intentionally **not** `#[non_exhaustive]`. All match sites in the
14/// codebase must handle every variant explicitly, giving compile-time assurance that
15/// new database backends are fully integrated before release.
16///
17/// Adding a new variant is a **semver-breaking change** (minor version bump with
18/// migration guide), because downstream exhaustive `match` expressions will fail
19/// to compile. If you match on `DatabaseType` and want forward compatibility, add
20/// a wildcard arm:
21///
22/// ```rust
23/// # use fraiseql_db::DatabaseType;
24/// # let db_type = DatabaseType::PostgreSQL;
25/// match db_type {
26///     DatabaseType::PostgreSQL => { /* ... */ }
27///     DatabaseType::MySQL      => { /* ... */ }
28///     DatabaseType::SQLite     => { /* ... */ }
29///     DatabaseType::SQLServer  => { /* ... */ }
30///     // no wildcard needed — exhaustive by design
31/// }
32/// ```
33#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
34#[serde(rename_all = "lowercase")]
35pub enum DatabaseType {
36    /// PostgreSQL database (primary, full feature set).
37    PostgreSQL,
38    /// MySQL database (secondary support).
39    MySQL,
40    /// SQLite database (local dev, testing).
41    SQLite,
42    /// SQL Server database (enterprise).
43    SQLServer,
44}
45
46impl DatabaseType {
47    /// Get database type as string.
48    #[must_use]
49    pub const fn as_str(&self) -> &'static str {
50        match self {
51            Self::PostgreSQL => "postgresql",
52            Self::MySQL => "mysql",
53            Self::SQLite => "sqlite",
54            Self::SQLServer => "sqlserver",
55        }
56    }
57}
58
59impl std::fmt::Display for DatabaseType {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        f.write_str(self.as_str())
62    }
63}
64
65/// JSONB value from database view.
66///
67/// Wraps `serde_json::Value` for type safety.
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct JsonbValue {
70    /// The JSONB data from the database `data` column.
71    pub data: serde_json::Value,
72}
73
74impl JsonbValue {
75    /// Create new JSONB value.
76    #[must_use]
77    pub const fn new(data: serde_json::Value) -> Self {
78        Self { data }
79    }
80
81    /// Get reference to inner value.
82    #[must_use]
83    pub const fn as_value(&self) -> &serde_json::Value {
84        &self.data
85    }
86
87    /// Consume and return inner value.
88    #[must_use]
89    pub fn into_value(self) -> serde_json::Value {
90        self.data
91    }
92}
93
94/// Typed parameter wrapper that preserves JSON value types for PostgreSQL wire protocol.
95///
96/// This enum ensures type information is preserved when converting GraphQL input values
97/// to PostgreSQL parameters, avoiding protocol errors from type mismatches.
98#[cfg(feature = "postgres")]
99#[derive(Debug, Clone)]
100#[non_exhaustive]
101pub enum QueryParam {
102    /// SQL NULL value
103    Null,
104    /// Boolean value
105    Bool(bool),
106    /// 32-bit integer
107    Int(i32),
108    /// 64-bit integer (BIGINT)
109    BigInt(i64),
110    /// 32-bit floating point
111    Float(f32),
112    /// 64-bit floating point (DOUBLE PRECISION)
113    Double(f64),
114    /// Text/string value (TEXT/VARCHAR)
115    Text(String),
116    /// JSON/JSONB value (for arrays and objects)
117    Json(serde_json::Value),
118}
119
120#[cfg(feature = "postgres")]
121impl From<serde_json::Value> for QueryParam {
122    fn from(value: serde_json::Value) -> Self {
123        match value {
124            serde_json::Value::Null => Self::Null,
125            serde_json::Value::Bool(b) => Self::Bool(b),
126            serde_json::Value::Number(n) => {
127                // For PostgreSQL NUMERIC comparisons (via ::text::numeric cast),
128                // we send numbers as text to avoid wire protocol issues.
129                // PostgreSQL can't directly convert f64 to NUMERIC in the binary protocol.
130                Self::Text(n.to_string())
131            },
132            serde_json::Value::String(s) => Self::Text(s),
133            serde_json::Value::Array(_) | serde_json::Value::Object(_) => Self::Json(value),
134        }
135    }
136}
137
138#[cfg(feature = "postgres")]
139impl ToSql for QueryParam {
140    tokio_postgres::types::to_sql_checked!();
141
142    fn to_sql(
143        &self,
144        ty: &Type,
145        out: &mut BytesMut,
146    ) -> Result<IsNull, Box<dyn std::error::Error + Sync + Send>> {
147        match self {
148            Self::Null => Ok(IsNull::Yes),
149            Self::Bool(b) => b.to_sql(ty, out),
150            Self::Int(i) => i.to_sql(ty, out),
151            Self::BigInt(i) => i.to_sql(ty, out),
152            Self::Float(f) => f.to_sql(ty, out),
153            Self::Double(f) => f.to_sql(ty, out),
154            Self::Text(s) => s.to_sql(ty, out),
155            Self::Json(v) => v.to_sql(ty, out),
156        }
157    }
158
159    fn accepts(_ty: &Type) -> bool {
160        true
161    }
162}
163
164/// Convert QueryParam to boxed ToSql trait object, preserving native types.
165///
166/// This function uses the boxing pattern to convert typed parameters into a form
167/// that tokio-postgres can serialize to PostgreSQL's wire protocol format.
168///
169/// # Example
170///
171/// ```rust
172/// # #[cfg(feature = "postgres")]
173/// # {
174/// use fraiseql_db::types::db_types::{QueryParam, to_sql_param};
175///
176/// let param = QueryParam::BigInt(42);
177/// let boxed = to_sql_param(&param);
178/// // boxed can be passed to tokio-postgres query methods
179/// drop(boxed);
180/// # }
181/// ```
182#[cfg(feature = "postgres")]
183pub fn to_sql_param(param: &QueryParam) -> Box<dyn ToSql + Sync + Send> {
184    match param {
185        QueryParam::Null => Box::new(None::<String>),
186        QueryParam::Bool(b) => Box::new(*b),
187        QueryParam::Int(i) => Box::new(*i),
188        QueryParam::BigInt(i) => Box::new(*i),
189        QueryParam::Float(f) => Box::new(*f),
190        QueryParam::Double(f) => Box::new(*f),
191        QueryParam::Text(s) => Box::new(s.clone()),
192        QueryParam::Json(v) => Box::new(v.clone()),
193    }
194}
195
196/// Connection pool metrics.
197#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
198pub struct PoolMetrics {
199    /// Total number of connections in the pool.
200    pub total_connections:  u32,
201    /// Number of idle (available) connections.
202    pub idle_connections:   u32,
203    /// Number of active (in-use) connections.
204    pub active_connections: u32,
205    /// Number of requests waiting for a connection.
206    pub waiting_requests:   u32,
207}
208
209impl PoolMetrics {
210    /// Calculate pool utilization (0.0 to 1.0).
211    #[must_use]
212    pub fn utilization(&self) -> f64 {
213        if self.total_connections == 0 {
214            return 0.0;
215        }
216        f64::from(self.active_connections) / f64::from(self.total_connections)
217    }
218
219    /// Check if pool is exhausted (all connections in use).
220    #[must_use]
221    pub const fn is_exhausted(&self) -> bool {
222        self.idle_connections == 0 && self.waiting_requests > 0
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn test_database_type_as_str() {
232        assert_eq!(DatabaseType::PostgreSQL.as_str(), "postgresql");
233        assert_eq!(DatabaseType::MySQL.as_str(), "mysql");
234        assert_eq!(DatabaseType::SQLite.as_str(), "sqlite");
235        assert_eq!(DatabaseType::SQLServer.as_str(), "sqlserver");
236    }
237
238    #[test]
239    fn test_database_type_display() {
240        assert_eq!(DatabaseType::PostgreSQL.to_string(), "postgresql");
241    }
242
243    #[test]
244    fn test_jsonb_value() {
245        let value = serde_json::json!({"id": "123", "name": "test"});
246        let jsonb = JsonbValue::new(value.clone());
247
248        assert_eq!(jsonb.as_value(), &value);
249        assert_eq!(jsonb.into_value(), value);
250    }
251
252    #[test]
253    fn test_pool_metrics_utilization() {
254        let metrics = PoolMetrics {
255            total_connections:  10,
256            idle_connections:   5,
257            active_connections: 5,
258            waiting_requests:   0,
259        };
260
261        assert!((metrics.utilization() - 0.5).abs() < f64::EPSILON);
262        assert!(!metrics.is_exhausted());
263    }
264
265    #[test]
266    fn test_pool_metrics_exhausted() {
267        let metrics = PoolMetrics {
268            total_connections:  10,
269            idle_connections:   0,
270            active_connections: 10,
271            waiting_requests:   5,
272        };
273
274        assert!((metrics.utilization() - 1.0).abs() < f64::EPSILON);
275        assert!(metrics.is_exhausted());
276    }
277}