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;