Skip to main content

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}