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 returned from a database `data` column.
66///
67/// Wraps `serde_json::Value` for type-safety at the **SQL → application
68/// boundary**: every adapter (`postgres`, `mysql`, `sqlite`, `sqlserver`) emits
69/// `Vec<JsonbValue>` so that downstream consumers do not have to discriminate
70/// between native database JSON columns and string-encoded JSON.
71///
72/// # Ownership contract (F029)
73///
74/// - **Adapter-owned**: the database adapter materialises `data` into an owned `serde_json::Value`
75///   before returning. There is no borrow of database buffers in this type — it is safe to keep
76///   across the `await` boundary that releases the database connection.
77/// - **Projector input**: consumers that project into GraphQL responses (see
78///   `fraiseql-core::runtime::projection::ResultProjector::project_results`) take `&[JsonbValue]`
79///   and produce a freshly-allocated `serde_json::Value` tree. Projection never aliases the input;
80///   each field is cloned out.
81/// - **Not part of the wire protocol**: `JsonbValue` is an *internal* shape and intentionally
82///   distinct from `serde_json::Value` so that the boundary between "raw database row" and "GraphQL
83///   response value" is visible in function signatures.
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct JsonbValue {
86    /// The JSONB data from the database `data` column.
87    pub data: serde_json::Value,
88}
89
90impl JsonbValue {
91    /// Create new JSONB value.
92    #[must_use]
93    pub const fn new(data: serde_json::Value) -> Self {
94        Self { data }
95    }
96
97    /// Get reference to inner value.
98    #[must_use]
99    pub const fn as_value(&self) -> &serde_json::Value {
100        &self.data
101    }
102
103    /// Consume and return inner value.
104    #[must_use]
105    pub fn into_value(self) -> serde_json::Value {
106        self.data
107    }
108}
109
110/// Typed parameter wrapper that preserves JSON value types for PostgreSQL wire protocol.
111///
112/// This enum ensures type information is preserved when converting GraphQL input values
113/// to PostgreSQL parameters, avoiding protocol errors from type mismatches.
114#[cfg(feature = "postgres")]
115#[derive(Debug, Clone)]
116#[non_exhaustive]
117pub enum QueryParam {
118    /// SQL NULL value
119    Null,
120    /// Boolean value
121    Bool(bool),
122    /// 32-bit integer
123    Int(i32),
124    /// 64-bit integer (BIGINT)
125    BigInt(i64),
126    /// 32-bit floating point
127    Float(f32),
128    /// 64-bit floating point (DOUBLE PRECISION)
129    Double(f64),
130    /// Text/string value (TEXT/VARCHAR)
131    Text(String),
132    /// JSON/JSONB value (for arrays and objects)
133    Json(serde_json::Value),
134}
135
136#[cfg(feature = "postgres")]
137impl From<serde_json::Value> for QueryParam {
138    fn from(value: serde_json::Value) -> Self {
139        match value {
140            serde_json::Value::Null => Self::Null,
141            serde_json::Value::Bool(b) => Self::Bool(b),
142            serde_json::Value::Number(n) => {
143                // For PostgreSQL NUMERIC comparisons (via ::text::numeric cast),
144                // we send numbers as text to avoid wire protocol issues.
145                // PostgreSQL can't directly convert f64 to NUMERIC in the binary protocol.
146                Self::Text(n.to_string())
147            },
148            serde_json::Value::String(s) => Self::Text(s),
149            serde_json::Value::Array(_) | serde_json::Value::Object(_) => Self::Json(value),
150        }
151    }
152}
153
154#[cfg(feature = "postgres")]
155impl ToSql for QueryParam {
156    tokio_postgres::types::to_sql_checked!();
157
158    fn to_sql(
159        &self,
160        ty: &Type,
161        out: &mut BytesMut,
162    ) -> Result<IsNull, Box<dyn std::error::Error + Sync + Send>> {
163        match self {
164            Self::Null => Ok(IsNull::Yes),
165            Self::Bool(b) => b.to_sql(ty, out),
166            Self::Int(i) => i.to_sql(ty, out),
167            Self::BigInt(i) => i.to_sql(ty, out),
168            Self::Float(f) => f.to_sql(ty, out),
169            Self::Double(f) => f.to_sql(ty, out),
170            Self::Text(s) => s.to_sql(ty, out),
171            Self::Json(v) => v.to_sql(ty, out),
172        }
173    }
174
175    fn accepts(_ty: &Type) -> bool {
176        true
177    }
178}
179
180/// Connection pool metrics.
181#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
182pub struct PoolMetrics {
183    /// Total number of connections in the pool.
184    pub total_connections:  u32,
185    /// Number of idle (available) connections.
186    pub idle_connections:   u32,
187    /// Number of active (in-use) connections.
188    pub active_connections: u32,
189    /// Number of requests waiting for a connection.
190    pub waiting_requests:   u32,
191}
192
193impl PoolMetrics {
194    /// Calculate pool utilization (0.0 to 1.0).
195    #[must_use]
196    pub fn utilization(&self) -> f64 {
197        if self.total_connections == 0 {
198            return 0.0;
199        }
200        f64::from(self.active_connections) / f64::from(self.total_connections)
201    }
202
203    /// Check if pool is exhausted (all connections in use).
204    #[must_use]
205    pub const fn is_exhausted(&self) -> bool {
206        self.idle_connections == 0 && self.waiting_requests > 0
207    }
208}
209
210/// Borrow a slice of [`QueryParam`]s as the `&[&(dyn ToSql + Sync)]` shape
211/// expected by `tokio_postgres::Client::query` and `::execute`.
212///
213/// `QueryParam` already implements [`ToSql`] (see the `impl` above), so each
214/// element can be passed by reference without boxing. This helper centralises
215/// the repeated `.iter().map(|p| p as &(dyn ToSql + Sync)).collect()` pattern
216/// used by the PostgreSQL adapter call sites and removes the last remaining
217/// per-parameter heap allocation in the query hot path.
218#[cfg(feature = "postgres")]
219#[must_use]
220pub fn as_sql_param_refs(params: &[QueryParam]) -> Vec<&(dyn ToSql + Sync)> {
221    params.iter().map(|p| p as &(dyn ToSql + Sync)).collect()
222}
223
224#[cfg(test)]
225mod tests;