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;