fraiseql_core/db/traits.rs
1//! Database adapter trait definitions.
2
3use async_trait::async_trait;
4
5use super::{
6 types::{DatabaseType, JsonbValue, PoolMetrics},
7 where_clause::WhereClause,
8};
9use crate::{error::Result, schema::SqlProjectionHint};
10
11/// Database adapter for executing queries against views.
12///
13/// This trait abstracts over different database backends (PostgreSQL, MySQL, SQLite, SQL Server).
14/// All implementations must support:
15/// - Executing simple WHERE queries against views
16/// - Returning JSONB data from the `data` column
17/// - Connection pooling and health checks
18///
19/// # Example
20///
21/// ```rust,no_run
22/// use fraiseql_core::db::{DatabaseAdapter, WhereClause, WhereOperator};
23/// use serde_json::json;
24///
25/// # async fn example(adapter: impl DatabaseAdapter) -> Result<(), Box<dyn std::error::Error>> {
26/// // Build WHERE clause
27/// let where_clause = WhereClause::Field {
28/// path: vec!["email".to_string()],
29/// operator: WhereOperator::Icontains,
30/// value: json!("example.com"),
31/// };
32///
33/// // Execute query
34/// let results = adapter
35/// .execute_where_query("v_user", Some(&where_clause), None, None)
36/// .await?;
37///
38/// println!("Found {} users", results.len());
39/// # Ok(())
40/// # }
41/// ```
42#[async_trait]
43pub trait DatabaseAdapter: Send + Sync {
44 /// Execute a WHERE query against a view and return JSONB rows.
45 ///
46 /// # Arguments
47 ///
48 /// * `view` - View name (e.g., "v_user", "v_post")
49 /// * `where_clause` - Optional WHERE clause AST
50 /// * `limit` - Optional row limit (for pagination)
51 /// * `offset` - Optional row offset (for pagination)
52 ///
53 /// # Returns
54 ///
55 /// Vec of JSONB values from the `data` column.
56 ///
57 /// # Errors
58 ///
59 /// Returns `FraiseQLError::Database` on query execution failure.
60 /// Returns `FraiseQLError::ConnectionPool` if connection pool is exhausted.
61 ///
62 /// # Example
63 ///
64 /// ```rust,no_run
65 /// # use fraiseql_core::db::DatabaseAdapter;
66 /// # async fn example(adapter: impl DatabaseAdapter) -> Result<(), Box<dyn std::error::Error>> {
67 /// // Simple query without WHERE clause
68 /// let all_users = adapter
69 /// .execute_where_query("v_user", None, Some(10), Some(0))
70 /// .await?;
71 /// # Ok(())
72 /// # }
73 /// ```
74 async fn execute_where_query(
75 &self,
76 view: &str,
77 where_clause: Option<&WhereClause>,
78 limit: Option<u32>,
79 offset: Option<u32>,
80 ) -> Result<Vec<JsonbValue>>;
81
82 /// Execute a WHERE query with SQL field projection optimization.
83 ///
84 /// Projects only the requested fields at the database level, reducing network payload
85 /// and JSON deserialization overhead by **40-55%** based on production measurements.
86 ///
87 /// This is the primary query execution method for optimized GraphQL queries.
88 /// It automatically selects only the fields requested in the GraphQL query, avoiding
89 /// unnecessary network transfer and deserialization of unused fields.
90 ///
91 /// # Automatic Projection
92 ///
93 /// In most cases, you don't call this directly. The `Executor` automatically:
94 /// 1. Determines which fields the GraphQL query requests
95 /// 2. Generates a `SqlProjectionHint` using database-specific SQL
96 /// 3. Calls this method with the projection hint
97 ///
98 /// # Arguments
99 ///
100 /// * `view` - View name (e.g., "v_user", "v_post")
101 /// * `projection` - Optional SQL projection hint with field list
102 /// - `Some(hint)`: Use projection to select only requested fields
103 /// - `None`: Falls back to standard query (full JSONB column)
104 /// * `where_clause` - Optional WHERE clause AST for filtering
105 /// * `limit` - Optional row limit (for pagination)
106 ///
107 /// # Returns
108 ///
109 /// Vec of JSONB values, either:
110 /// - Full objects (when projection is None)
111 /// - Projected objects with only requested fields (when projection is Some)
112 ///
113 /// # Errors
114 ///
115 /// Returns `FraiseQLError::Database` on query execution failure, including:
116 /// - Connection pool exhaustion
117 /// - SQL execution errors
118 /// - Type mismatches
119 ///
120 /// # Performance Characteristics
121 ///
122 /// When projection is provided (recommended):
123 /// - **Latency**: 40-55% reduction vs full object fetch
124 /// - **Network**: 40-55% smaller payload (proportional to unused fields)
125 /// - **Throughput**: Maintains 250+ Kelem/s (elements per second)
126 /// - **Memory**: Proportional to projected fields only
127 ///
128 /// Improvement scales with:
129 /// - Percentage of unused fields (more unused = more improvement)
130 /// - Size of result set (larger sets benefit more)
131 /// - Network latency (network-bound queries benefit most)
132 ///
133 /// When projection is None:
134 /// - Behavior identical to `execute_where_query()`
135 /// - Returns full JSONB column
136 /// - Used for compatibility/debugging
137 ///
138 /// # Database Support
139 ///
140 /// | Database | Status | Implementation |
141 /// |----------|--------|-----------------|
142 /// | PostgreSQL | ✅ Optimized | `jsonb_build_object()` |
143 /// | MySQL | ⏳ Fallback | Server-side filtering (planned) |
144 /// | SQLite | ⏳ Fallback | Server-side filtering (planned) |
145 /// | SQL Server | ⏳ Fallback | Server-side filtering (planned) |
146 ///
147 /// # Example: Direct Usage (Advanced)
148 ///
149 /// ```rust,ignore
150 /// use fraiseql_core::schema::SqlProjectionHint;
151 /// use fraiseql_core::db::DatabaseAdapter;
152 ///
153 /// let projection = SqlProjectionHint {
154 /// database: "postgresql".to_string(),
155 /// projection_template: "jsonb_build_object(\
156 /// 'id', data->>'id', \
157 /// 'name', data->>'name', \
158 /// 'email', data->>'email'\
159 /// )".to_string(),
160 /// estimated_reduction_percent: 75,
161 /// };
162 ///
163 /// let results = adapter
164 /// .execute_with_projection("v_user", Some(&projection), None, Some(100))
165 /// .await?;
166 ///
167 /// // results only contain id, name, email fields
168 /// // 75% smaller than fetching all fields
169 /// ```
170 ///
171 /// # Example: Fallback (No Projection)
172 ///
173 /// ```rust,ignore
174 /// // For debugging or when projection not available
175 /// let results = adapter
176 /// .execute_with_projection("v_user", None, None, Some(100))
177 /// .await?;
178 ///
179 /// // Equivalent to execute_where_query() - returns full objects
180 /// ```
181 ///
182 /// # See Also
183 ///
184 /// - `execute_where_query()` - Standard query without projection
185 /// - `SqlProjectionHint` - Structure defining field projection
186 /// - [Projection Optimization Guide](https://docs.fraiseql.rs/performance/projection-optimization.md)
187 async fn execute_with_projection(
188 &self,
189 view: &str,
190 projection: Option<&SqlProjectionHint>,
191 where_clause: Option<&WhereClause>,
192 limit: Option<u32>,
193 ) -> Result<Vec<JsonbValue>>;
194
195 /// Get database type (for logging/metrics).
196 ///
197 /// Used to identify which database backend is in use.
198 fn database_type(&self) -> DatabaseType;
199
200 /// Health check - verify database connectivity.
201 ///
202 /// Executes a simple query (e.g., `SELECT 1`) to verify the database is reachable.
203 ///
204 /// # Errors
205 ///
206 /// Returns `FraiseQLError::Database` if health check fails.
207 async fn health_check(&self) -> Result<()>;
208
209 /// Get connection pool metrics.
210 ///
211 /// Returns current statistics about the connection pool:
212 /// - Total connections
213 /// - Idle connections
214 /// - Active connections
215 /// - Waiting requests
216 fn pool_metrics(&self) -> PoolMetrics;
217
218 /// Execute raw SQL query and return rows as JSON objects.
219 ///
220 /// Used for aggregation queries where we need full row data, not just JSONB column.
221 ///
222 /// # Security Warning
223 ///
224 /// This method executes arbitrary SQL. **NEVER** pass untrusted input directly to this method.
225 /// Always:
226 /// - Use parameterized queries with bound parameters
227 /// - Validate and sanitize SQL templates before execution
228 /// - Only execute SQL generated by the FraiseQL compiler
229 /// - Log SQL execution for audit trails
230 ///
231 /// # Arguments
232 ///
233 /// * `sql` - Raw SQL query to execute (must be safe/trusted)
234 ///
235 /// # Returns
236 ///
237 /// Vec of rows, where each row is a HashMap of column name to JSON value.
238 ///
239 /// # Errors
240 ///
241 /// Returns `FraiseQLError::Database` on query execution failure.
242 ///
243 /// # Example
244 ///
245 /// ```rust,no_run
246 /// # use fraiseql_core::db::DatabaseAdapter;
247 /// # async fn example(adapter: impl DatabaseAdapter) -> Result<(), Box<dyn std::error::Error>> {
248 /// // Safe: SQL generated by FraiseQL compiler
249 /// let sql = "SELECT category, SUM(revenue) as total FROM tf_sales GROUP BY category";
250 /// let rows = adapter.execute_raw_query(sql).await?;
251 /// for row in rows {
252 /// println!("Category: {}, Total: {}", row["category"], row["total"]);
253 /// }
254 /// # Ok(())
255 /// # }
256 /// ```
257 async fn execute_raw_query(
258 &self,
259 sql: &str,
260 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>>;
261
262 /// Execute a PostgreSQL function call and return all columns as rows.
263 ///
264 /// Builds `SELECT * FROM {function_name}($1, $2, ...)` with one positional placeholder per
265 /// argument, executes it with the provided JSON values, and returns each result row as a
266 /// `HashMap<column_name, json_value>`.
267 ///
268 /// Used by the mutation execution pathway to call stored procedures that return the
269 /// `app.mutation_response` composite type
270 /// `(status, message, entity_id, entity_type, entity jsonb, updated_fields text[],
271 /// cascade jsonb, metadata jsonb)`.
272 ///
273 /// # Arguments
274 ///
275 /// * `function_name` - Fully-qualified PostgreSQL function name (e.g. `fn_create_machine`)
276 /// * `args` - Positional JSON arguments passed as `$1, $2, …` bind parameters
277 ///
278 /// # Errors
279 ///
280 /// Returns `FraiseQLError::Database` on query execution failure.
281 async fn execute_function_call(
282 &self,
283 function_name: &str,
284 args: &[serde_json::Value],
285 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>>;
286
287 /// Get database capabilities.
288 ///
289 /// Returns information about what features this database supports,
290 /// including collation strategies and limitations.
291 ///
292 /// # Returns
293 ///
294 /// `DatabaseCapabilities` describing supported features.
295 fn capabilities(&self) -> DatabaseCapabilities {
296 DatabaseCapabilities::from_database_type(self.database_type())
297 }
298}
299
300/// Database capabilities and feature support.
301///
302/// Describes what features a database backend supports, allowing the runtime
303/// to adapt behavior based on database limitations.
304#[derive(Debug, Clone, Copy)]
305pub struct DatabaseCapabilities {
306 /// Database type.
307 pub database_type: DatabaseType,
308
309 /// Supports locale-specific collations.
310 pub supports_locale_collation: bool,
311
312 /// Requires custom collation registration.
313 pub requires_custom_collation: bool,
314
315 /// Recommended collation provider.
316 pub recommended_collation: Option<&'static str>,
317}
318
319impl DatabaseCapabilities {
320 /// Create capabilities from database type.
321 #[must_use]
322 pub const fn from_database_type(db_type: DatabaseType) -> Self {
323 match db_type {
324 DatabaseType::PostgreSQL => Self {
325 database_type: db_type,
326 supports_locale_collation: true,
327 requires_custom_collation: false,
328 recommended_collation: Some("icu"),
329 },
330 DatabaseType::MySQL => Self {
331 database_type: db_type,
332 supports_locale_collation: false,
333 requires_custom_collation: false,
334 recommended_collation: Some("utf8mb4_unicode_ci"),
335 },
336 DatabaseType::SQLite => Self {
337 database_type: db_type,
338 supports_locale_collation: false,
339 requires_custom_collation: true,
340 recommended_collation: Some("NOCASE"),
341 },
342 DatabaseType::SQLServer => Self {
343 database_type: db_type,
344 supports_locale_collation: true,
345 requires_custom_collation: false,
346 recommended_collation: Some("Latin1_General_100_CI_AI_SC_UTF8"),
347 },
348 }
349 }
350
351 /// Get collation strategy description.
352 #[must_use]
353 pub const fn collation_strategy(&self) -> &'static str {
354 match self.database_type {
355 DatabaseType::PostgreSQL => "ICU collations (locale-specific)",
356 DatabaseType::MySQL => "UTF8MB4 collations (general)",
357 DatabaseType::SQLite => "NOCASE (limited)",
358 DatabaseType::SQLServer => "Language-specific collations",
359 }
360 }
361}