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/// FraiseQL is PostgreSQL-only: the MySQL, SQLite and SQL Server variants were
12/// removed with their adapters (G2 decision on #374 — the non-PostgreSQL
13/// dialects were never production-correct, see #721/#799). The enum is kept —
14/// rather than replaced with a unit type — so that SQL-generation seams stay
15/// explicitly tagged with the dialect they emit, and a future backend addition
16/// is a compile-time-visible change at every match site.
17///
18/// # Stability
19///
20/// This enum is intentionally **not** `#[non_exhaustive]`. All match sites in the
21/// codebase must handle every variant explicitly, giving compile-time assurance that
22/// new database backends are fully integrated before release. Adding a variant is a
23/// semver-breaking change (minor version bump with migration guide).
24#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
25#[serde(rename_all = "lowercase")]
26pub enum DatabaseType {
27    /// PostgreSQL database (the only supported backend).
28    PostgreSQL,
29}
30
31impl DatabaseType {
32    /// Get database type as string.
33    #[must_use]
34    pub const fn as_str(&self) -> &'static str {
35        match self {
36            Self::PostgreSQL => "postgresql",
37        }
38    }
39}
40
41impl std::fmt::Display for DatabaseType {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        f.write_str(self.as_str())
44    }
45}
46
47/// JSONB value returned from a database `data` column.
48///
49/// Wraps `serde_json::Value` for type-safety at the **SQL → application
50/// boundary**: every adapter (`postgres`, `mysql`, `sqlite`, `sqlserver`) emits
51/// `Vec<JsonbValue>` so that downstream consumers do not have to discriminate
52/// between native database JSON columns and string-encoded JSON.
53///
54/// # Ownership contract (F029)
55///
56/// - **Adapter-owned**: the database adapter materialises `data` into an owned `serde_json::Value`
57///   before returning. There is no borrow of database buffers in this type — it is safe to keep
58///   across the `await` boundary that releases the database connection.
59/// - **Projector input**: consumers that project into GraphQL responses (see
60///   `fraiseql-core::runtime::projection::ResultProjector::project_results`) take `&[JsonbValue]`
61///   and produce a freshly-allocated `serde_json::Value` tree. Projection never aliases the input;
62///   each field is cloned out.
63/// - **Not part of the wire protocol**: `JsonbValue` is an *internal* shape and intentionally
64///   distinct from `serde_json::Value` so that the boundary between "raw database row" and "GraphQL
65///   response value" is visible in function signatures.
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct JsonbValue {
68    /// The JSONB data from the database `data` column.
69    pub data: serde_json::Value,
70}
71
72impl JsonbValue {
73    /// Create new JSONB value.
74    #[must_use]
75    pub const fn new(data: serde_json::Value) -> Self {
76        Self { data }
77    }
78
79    /// Get reference to inner value.
80    #[must_use]
81    pub const fn as_value(&self) -> &serde_json::Value {
82        &self.data
83    }
84
85    /// Consume and return inner value.
86    #[must_use]
87    pub fn into_value(self) -> serde_json::Value {
88        self.data
89    }
90}
91
92/// Typed parameter wrapper that preserves JSON value types for PostgreSQL wire protocol.
93///
94/// This enum ensures type information is preserved when converting GraphQL input values
95/// to PostgreSQL parameters, avoiding protocol errors from type mismatches.
96#[cfg(feature = "postgres")]
97#[derive(Debug, Clone)]
98#[non_exhaustive]
99pub enum QueryParam {
100    /// SQL NULL value
101    Null,
102    /// Boolean value
103    Bool(bool),
104    /// 32-bit integer
105    Int(i32),
106    /// 64-bit integer (BIGINT)
107    BigInt(i64),
108    /// 32-bit floating point
109    Float(f32),
110    /// 64-bit floating point (DOUBLE PRECISION)
111    Double(f64),
112    /// Text/string value (TEXT/VARCHAR)
113    Text(String),
114    /// JSON/JSONB value (for arrays and objects)
115    Json(serde_json::Value),
116}
117
118#[cfg(feature = "postgres")]
119impl From<serde_json::Value> for QueryParam {
120    fn from(value: serde_json::Value) -> Self {
121        match value {
122            serde_json::Value::Null => Self::Null,
123            serde_json::Value::Bool(b) => Self::Bool(b),
124            serde_json::Value::Number(n) => {
125                // For PostgreSQL NUMERIC comparisons (via ::text::numeric cast),
126                // we send numbers as text to avoid wire protocol issues.
127                // PostgreSQL can't directly convert f64 to NUMERIC in the binary protocol.
128                Self::Text(n.to_string())
129            },
130            serde_json::Value::String(s) => Self::Text(s),
131            serde_json::Value::Array(_) | serde_json::Value::Object(_) => Self::Json(value),
132        }
133    }
134}
135
136#[cfg(feature = "postgres")]
137impl ToSql for QueryParam {
138    tokio_postgres::types::to_sql_checked!();
139
140    fn to_sql(
141        &self,
142        ty: &Type,
143        out: &mut BytesMut,
144    ) -> Result<IsNull, Box<dyn std::error::Error + Sync + Send>> {
145        match self {
146            Self::Null => Ok(IsNull::Yes),
147            Self::Bool(b) => b.to_sql(ty, out),
148            Self::Int(i) => i.to_sql(ty, out),
149            Self::BigInt(i) => i.to_sql(ty, out),
150            Self::Float(f) => f.to_sql(ty, out),
151            Self::Double(f) => f.to_sql(ty, out),
152            Self::Text(s) => s.to_sql(ty, out),
153            Self::Json(v) => v.to_sql(ty, out),
154        }
155    }
156
157    fn accepts(_ty: &Type) -> bool {
158        true
159    }
160}
161
162/// Connection pool metrics.
163#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
164pub struct PoolMetrics {
165    /// Total number of connections in the pool.
166    pub total_connections:  u32,
167    /// Number of idle (available) connections.
168    pub idle_connections:   u32,
169    /// Number of active (in-use) connections.
170    pub active_connections: u32,
171    /// Number of requests waiting for a connection.
172    pub waiting_requests:   u32,
173}
174
175impl PoolMetrics {
176    /// Calculate pool utilization (0.0 to 1.0).
177    #[must_use]
178    pub fn utilization(&self) -> f64 {
179        if self.total_connections == 0 {
180            return 0.0;
181        }
182        f64::from(self.active_connections) / f64::from(self.total_connections)
183    }
184
185    /// Check if pool is exhausted (all connections in use).
186    #[must_use]
187    pub const fn is_exhausted(&self) -> bool {
188        self.idle_connections == 0 && self.waiting_requests > 0
189    }
190}
191
192/// Borrow a slice of [`QueryParam`]s as the `&[&(dyn ToSql + Sync)]` shape
193/// expected by `tokio_postgres::Client::query` and `::execute`.
194///
195/// `QueryParam` already implements [`ToSql`] (see the `impl` above), so each
196/// element can be passed by reference without boxing. This helper centralises
197/// the repeated `.iter().map(|p| p as &(dyn ToSql + Sync)).collect()` pattern
198/// used by the PostgreSQL adapter call sites and removes the last remaining
199/// per-parameter heap allocation in the query hot path.
200#[cfg(feature = "postgres")]
201#[must_use]
202pub fn as_sql_param_refs(params: &[QueryParam]) -> Vec<&(dyn ToSql + Sync)> {
203    params.iter().map(|p| p as &(dyn ToSql + Sync)).collect()
204}
205
206#[cfg(test)]
207mod tests;