Skip to main content

fraiseql_db/
traits.rs

1//! Database adapter trait definitions.
2//!
3//! The main [`DatabaseAdapter`] trait lives in this file. Supporting types
4//! (`RelayPageResult`, `DatabaseCapabilities`, enums, type aliases) are in
5//! the `adapter_types` submodule.
6
7mod adapter_types;
8mod mutations;
9mod relay;
10
11use std::sync::Arc;
12
13pub use adapter_types::*;
14use async_trait::async_trait;
15use fraiseql_error::{FraiseQLError, Result};
16pub use mutations::SupportsMutations;
17pub use relay::RelayDatabaseAdapter;
18
19use crate::{
20    types::{
21        DatabaseType, JsonbValue, PoolMetrics,
22        sql_hints::{OrderByClause, SqlProjectionHint},
23    },
24    where_clause::WhereClause,
25};
26
27/// Database adapter for executing queries against views.
28///
29/// This trait abstracts over different database backends (PostgreSQL, MySQL, SQLite, SQL Server).
30/// All implementations must support:
31/// - Executing parameterized WHERE queries against views
32/// - Returning JSONB data from the `data` column
33/// - Connection pooling and health checks
34/// - Row-level security (RLS) WHERE clauses
35///
36/// # Architecture
37///
38/// The adapter is the runtime interface to the database. It receives:
39/// - View/table name (e.g., "v_user", "tf_sales")
40/// - Parameterized WHERE clauses (AST form, not strings)
41/// - Projection hints (for performance optimization)
42/// - Pagination parameters (LIMIT/OFFSET)
43///
44/// And returns:
45/// - JSONB rows from the `data` column (most operations)
46/// - Arbitrary rows as HashMap (for aggregation queries)
47/// - Mutation results from stored procedures
48///
49/// # Implementing a New Adapter
50///
51/// To add support for a new database (e.g., Oracle, Snowflake):
52///
53/// 1. **Create a new module** in `src/db/your_database/`
54/// 2. **Implement the trait**:
55///
56///    ```rust,ignore
57///    pub struct YourDatabaseAdapter { /* fields */ }
58///
59///    #[async_trait]
60///    impl DatabaseAdapter for YourDatabaseAdapter {
61///        async fn execute_where_query(&self, ...) -> Result<Vec<JsonbValue>> {
62///            // 1. Build parameterized SQL from WhereClause AST
63///            // 2. Execute with bound parameters (NO string concatenation)
64///            // 3. Return JSONB from data column
65///        }
66///        // Implement other required methods...
67///    }
68///    ```
69/// 3. **Add feature flag** to `Cargo.toml` (e.g., `feature = "your-database"`)
70/// 4. **Copy structure from PostgreSQL adapter** — see `src/db/postgres/adapter.rs`
71/// 5. **Add tests** in `tests/integration/your_database_test.rs`
72///
73/// # Security Requirements
74///
75/// All implementations MUST:
76/// - **Never concatenate user input into SQL strings**
77/// - **Always use parameterized queries** with bind parameters
78/// - **Validate parameter types** before binding
79/// - **Preserve RLS WHERE clauses** (never filter them out)
80/// - **Return errors, not silently fail** (e.g., connection loss)
81///
82/// # Connection Management
83///
84/// - Use a connection pool (recommended: 20 connections default)
85/// - Implement `health_check()` for ping-based monitoring
86/// - Provide `pool_metrics()` for observability
87/// - Handle stale connections gracefully
88///
89/// # Performance Characteristics
90///
91/// Expected throughput when properly implemented:
92/// - **Simple queries** (single table, no WHERE): 250+ Kelem/s
93/// - **Complex queries** (JOINs, multiple conditions): 50+ Kelem/s
94/// - **Mutations** (stored procedures): 1-10 RPS (depends on procedure)
95/// - **Relay pagination** (keyset cursors): 15-30ms latency
96///
97/// # Example: PostgreSQL Implementation
98///
99/// ```rust,ignore
100/// use sqlx::postgres::PgPool;
101/// use async_trait::async_trait;
102///
103/// pub struct PostgresAdapter {
104///     pool: PgPool,
105/// }
106///
107/// #[async_trait]
108/// impl DatabaseAdapter for PostgresAdapter {
109///     async fn execute_where_query(
110///         &self,
111///         view: &str,
112///         where_clause: Option<&WhereClause>,
113///         limit: Option<u32>,
114///         offset: Option<u32>,
115///     ) -> Result<Vec<JsonbValue>> {
116///         // 1. Build SQL: SELECT data FROM {view} WHERE {where_clause} LIMIT {limit}
117///         let mut sql = format!(r#"SELECT data FROM "{}""#, view);
118///
119///         // 2. Add WHERE clause (converts AST to parameterized SQL)
120///         let params = if let Some(where_clause) = where_clause {
121///             sql.push_str(" WHERE ");
122///             let (where_sql, params) = build_where_sql(where_clause)?;
123///             sql.push_str(&where_sql);
124///             params
125///         } else {
126///             vec![]
127///         };
128///
129///         // 3. Add LIMIT and OFFSET
130///         if let Some(limit) = limit {
131///             sql.push_str(" LIMIT ");
132///             sql.push_str(&limit.to_string());
133///         }
134///         if let Some(offset) = offset {
135///             sql.push_str(" OFFSET ");
136///             sql.push_str(&offset.to_string());
137///         }
138///
139///         // 4. Execute with bound parameters (NO string interpolation)
140///         let rows: Vec<(serde_json::Value,)> = sqlx::query_as(&sql)
141///             .bind(&params[0])
142///             .bind(&params[1])
143///             // ... bind all parameters
144///             .fetch_all(&self.pool)
145///             .await?;
146///
147///         // 5. Extract JSONB and return
148///         Ok(rows.into_iter().map(|(data,)| data).collect())
149///     }
150///
151///     // Implement other required methods...
152/// }
153/// ```
154///
155/// # Example: Basic Usage
156///
157/// ```rust,no_run
158/// use fraiseql_db::{DatabaseAdapter, WhereClause, WhereOperator};
159/// use serde_json::json;
160///
161/// # async fn example(adapter: impl DatabaseAdapter) -> Result<(), Box<dyn std::error::Error>> {
162/// // Build WHERE clause (AST, not string)
163/// let where_clause = WhereClause::Field {
164///     path: vec!["email".to_string()],
165///     operator: WhereOperator::Icontains,
166///     value: json!("example.com"),
167/// };
168///
169/// // Execute query with parameters
170/// let results = adapter
171///     .execute_where_query("v_user", Some(&where_clause), Some(10), None, None)
172///     .await?;
173///
174/// println!("Found {} users matching filter", results.len());
175/// # Ok(())
176/// # }
177/// ```
178///
179/// # See Also
180///
181/// - `WhereClause` — AST for parameterized WHERE clauses
182/// - `RelayDatabaseAdapter` — Optional trait for keyset pagination
183/// - `DatabaseCapabilities` — Feature detection for the adapter
184/// - [Performance Guide](https://docs.fraiseql.rs/performance/database-adapters.md)
185// POLICY: `#[async_trait]` placement for `DatabaseAdapter`
186//
187// `DatabaseAdapter` is used both generically (`Server<A: DatabaseAdapter>` in axum
188// handlers, zero overhead via static dispatch) and dynamically (`Arc<dyn
189// DatabaseAdapter + Send + Sync>` in federation, heap-boxed future per call).
190//
191// `#[async_trait]` is required on:
192// - The trait definition (generates `Pin<Box<dyn Future + Send>>` return types)
193// - Every `impl DatabaseAdapter for ConcreteType` block (generates the boxing)
194// NOT required on callers (they see `Pin<Box<dyn Future + Send>>` from macro output).
195//
196// Why not native `async fn in trait` (Rust 1.75+)?
197// Native dyn async trait does NOT propagate `+ Send` on generated futures. Tokio
198// requires futures spawned with `tokio::spawn` to be `Send`. Until Return Type
199// Notation (RFC 3425, tracking: github.com/rust-lang/rust/issues/109417) stabilises,
200// `async_trait` is the only ergonomic path to `dyn DatabaseAdapter + Send + Sync`.
201// Re-evaluate when Rust 1.90+ ships or when RTN is stabilised.
202//
203// MIGRATION TRACKING: async-trait → native async fn in trait
204//
205// Current status: BLOCKED on RFC 3425 (Return Type Notation)
206// See: https://github.com/rust-lang/rfcs/pull/3425
207//      https://github.com/rust-lang/rust/issues/109417
208//
209// Migration is safe when ALL of the following are true:
210// 1. RTN with `+ Send` bounds is stable on rustc (e.g. `fn foo() -> impl Future + Send`)
211// 2. FraiseQL MSRV is updated to that stabilising version
212// 3. tokio::spawn() works with native dyn async trait objects (futures must be Send)
213//
214// Scope when criteria are met: 68 files (grep -rn "#\[async_trait\]" crates/)
215// Effort: Medium (mostly mechanical — remove macro from impls, adjust trait defs)
216// dynosaur was evaluated and rejected: does not propagate + Send (incompatible with Tokio)
217#[async_trait]
218pub trait DatabaseAdapter: Send + Sync {
219    /// Execute a WHERE query against a view and return JSONB rows.
220    ///
221    /// # Arguments
222    ///
223    /// * `view` - View name (e.g., "v_user", "v_post")
224    /// * `where_clause` - Optional WHERE clause AST
225    /// * `limit` - Optional row limit (for pagination)
226    /// * `offset` - Optional row offset (for pagination)
227    /// * `security_context` - Optional security context for RLS and caching decisions
228    ///
229    /// # Returns
230    ///
231    /// Vec of JSONB values from the `data` column.
232    ///
233    /// # Errors
234    ///
235    /// Returns `FraiseQLError::Database` on query execution failure.
236    /// Returns `FraiseQLError::ConnectionPool` if connection pool is exhausted.
237    ///
238    /// # Example
239    ///
240    /// ```rust,no_run
241    /// # use fraiseql_db::DatabaseAdapter;
242    /// # async fn example(adapter: impl DatabaseAdapter) -> Result<(), Box<dyn std::error::Error>> {
243    /// // Simple query without WHERE clause
244    /// let all_users = adapter
245    ///     .execute_where_query("v_user", None, Some(10), Some(0), None)
246    ///     .await?;
247    /// # Ok(())
248    /// # }
249    /// ```
250    async fn execute_where_query(
251        &self,
252        view: &str,
253        where_clause: Option<&WhereClause>,
254        limit: Option<u32>,
255        offset: Option<u32>,
256        order_by: Option<&[OrderByClause]>,
257    ) -> Result<Vec<JsonbValue>>;
258
259    /// Execute a WHERE query with SQL field projection optimization.
260    ///
261    /// Projects only the requested fields at the database level, reducing network payload
262    /// and JSON deserialization overhead by **40-55%** based on production measurements.
263    ///
264    /// This is the primary query execution method for optimized GraphQL queries.
265    /// It automatically selects only the fields requested in the GraphQL query, avoiding
266    /// unnecessary network transfer and deserialization of unused fields.
267    ///
268    /// # Automatic Projection
269    ///
270    /// In most cases, you don't call this directly. The `Executor` automatically:
271    /// 1. Determines which fields the GraphQL query requests
272    /// 2. Generates a `SqlProjectionHint` using database-specific SQL
273    /// 3. Calls this method with the projection hint
274    ///
275    /// # Arguments
276    ///
277    /// * `view` - View name (e.g., "v_user", "v_post")
278    /// * `projection` - Optional SQL projection hint with field list
279    ///   - `Some(hint)`: Use projection to select only requested fields
280    ///   - `None`: Falls back to standard query (full JSONB column)
281    /// * `where_clause` - Optional WHERE clause AST for filtering
282    /// * `limit` - Optional row limit (for pagination)
283    ///
284    /// # Returns
285    ///
286    /// Vec of JSONB values, either:
287    /// - Full objects (when projection is None)
288    /// - Projected objects with only requested fields (when projection is Some)
289    ///
290    /// # Errors
291    ///
292    /// Returns `FraiseQLError::Database` on query execution failure, including:
293    /// - Connection pool exhaustion
294    /// - SQL execution errors
295    /// - Type mismatches
296    ///
297    /// # Performance Characteristics
298    ///
299    /// When projection is provided (recommended):
300    /// - **Latency**: 40-55% reduction vs full object fetch
301    /// - **Network**: 40-55% smaller payload (proportional to unused fields)
302    /// - **Throughput**: Maintains 250+ Kelem/s (elements per second)
303    /// - **Memory**: Proportional to projected fields only
304    ///
305    /// Improvement scales with:
306    /// - Percentage of unused fields (more unused = more improvement)
307    /// - Size of result set (larger sets benefit more)
308    /// - Network latency (network-bound queries benefit most)
309    ///
310    /// When projection is None:
311    /// - Behavior identical to `execute_where_query()`
312    /// - Returns full JSONB column
313    /// - Used for compatibility/debugging
314    ///
315    /// # Database Support
316    ///
317    /// | Database | Status | Implementation |
318    /// |----------|--------|-----------------|
319    /// | PostgreSQL | ✅ Optimized | `jsonb_build_object()` |
320    /// | MySQL | ⏳ Fallback | Server-side filtering (planned) |
321    /// | SQLite | ⏳ Fallback | Server-side filtering (planned) |
322    /// | SQL Server | ⏳ Fallback | Server-side filtering (planned) |
323    ///
324    /// # Example: Direct Usage (Advanced)
325    ///
326    /// ```no_run
327    /// // Requires: running PostgreSQL database and a DatabaseAdapter implementation.
328    /// use fraiseql_db::types::SqlProjectionHint;
329    /// use fraiseql_db::traits::DatabaseAdapter;
330    /// use fraiseql_db::DatabaseType;
331    ///
332    /// # async fn example(adapter: &impl DatabaseAdapter) -> Result<(), Box<dyn std::error::Error>> {
333    /// let projection = SqlProjectionHint::new(
334    ///     DatabaseType::PostgreSQL,
335    ///     "jsonb_build_object(\
336    ///         'id', data->>'id', \
337    ///         'name', data->>'name', \
338    ///         'email', data->>'email'\
339    ///     )".to_string(),
340    ///     75,
341    /// );
342    ///
343    /// let results = adapter
344    ///     .execute_with_projection("v_user", Some(&projection), None, Some(100), None, None)
345    ///     .await?;
346    ///
347    /// // results only contain id, name, email fields
348    /// // 75% smaller than fetching all fields
349    /// # Ok(())
350    /// # }
351    /// ```
352    ///
353    /// # Example: Fallback (No Projection)
354    ///
355    /// ```no_run
356    /// // Requires: running PostgreSQL database and a DatabaseAdapter implementation.
357    /// # use fraiseql_db::traits::DatabaseAdapter;
358    /// # async fn example(adapter: &impl DatabaseAdapter) -> Result<(), Box<dyn std::error::Error>> {
359    /// // For debugging or when projection not available
360    /// let results = adapter
361    ///     .execute_with_projection("v_user", None, None, Some(100), None, None)
362    ///     .await?;
363    ///
364    /// // Equivalent to execute_where_query() - returns full objects
365    /// # Ok(())
366    /// # }
367    /// ```
368    ///
369    /// # See Also
370    ///
371    /// - `execute_where_query()` - Standard query without projection
372    /// - `SqlProjectionHint` - Structure defining field projection
373    /// - [Projection Optimization Guide](https://docs.fraiseql.rs/performance/projection-optimization.md)
374    async fn execute_with_projection(
375        &self,
376        view: &str,
377        projection: Option<&SqlProjectionHint>,
378        where_clause: Option<&WhereClause>,
379        limit: Option<u32>,
380        offset: Option<u32>,
381        order_by: Option<&[OrderByClause]>,
382    ) -> Result<Vec<JsonbValue>>;
383
384    /// Like `execute_where_query` but returns the result wrapped in an `Arc`.
385    ///
386    /// The default implementation wraps the result of `execute_where_query` in a
387    /// fresh `Arc`. `CachedDatabaseAdapter` overrides this to return the cached `Arc`
388    /// directly — eliminating the full `Vec<JsonbValue>` clone that the non-`Arc`
389    /// path requires on every cache hit.
390    ///
391    /// Callers on the hot query path should prefer this variant and borrow from the
392    /// `Arc` via `&**arc` rather than taking ownership.
393    ///
394    /// # Errors
395    ///
396    /// Same errors as `execute_where_query`.
397    async fn execute_where_query_arc(
398        &self,
399        view: &str,
400        where_clause: Option<&WhereClause>,
401        limit: Option<u32>,
402        offset: Option<u32>,
403        order_by: Option<&[OrderByClause]>,
404    ) -> Result<Arc<Vec<JsonbValue>>> {
405        self.execute_where_query(view, where_clause, limit, offset, order_by)
406            .await
407            .map(Arc::new)
408    }
409
410    /// Like `execute_with_projection` but returns the result wrapped in an `Arc`.
411    ///
412    /// The default implementation wraps the result of `execute_with_projection` in a
413    /// fresh `Arc`. `CachedDatabaseAdapter` overrides this to return the cached `Arc`
414    /// directly — eliminating the full `Vec<JsonbValue>` clone that the non-`Arc`
415    /// path requires on every cache hit.
416    ///
417    /// Parameters are passed in a `ProjectionRequest` struct (F043) so adapters
418    /// and callers cannot misorder them.
419    ///
420    /// # Errors
421    ///
422    /// Same errors as `execute_with_projection`.
423    async fn execute_with_projection_arc(
424        &self,
425        request: &ProjectionRequest<'_>,
426    ) -> Result<Arc<Vec<JsonbValue>>> {
427        self.execute_with_projection(
428            request.view,
429            request.projection,
430            request.where_clause,
431            request.limit,
432            request.offset,
433            request.order_by,
434        )
435        .await
436        .map(Arc::new)
437    }
438
439    /// Get database type (for logging/metrics).
440    ///
441    /// Used to identify which database backend is in use.
442    fn database_type(&self) -> DatabaseType;
443
444    /// Health check - verify database connectivity.
445    ///
446    /// Executes a simple query (e.g., `SELECT 1`) to verify the database is reachable.
447    ///
448    /// # Errors
449    ///
450    /// Returns `FraiseQLError::Database` if health check fails.
451    async fn health_check(&self) -> Result<()>;
452
453    /// Get connection pool metrics.
454    ///
455    /// Returns current statistics about the connection pool:
456    /// - Total connections
457    /// - Idle connections
458    /// - Active connections
459    /// - Waiting requests
460    fn pool_metrics(&self) -> PoolMetrics;
461
462    /// Execute raw SQL query and return rows as JSON objects.
463    ///
464    /// Used for aggregation queries where we need full row data, not just JSONB column.
465    ///
466    /// # Security Warning
467    ///
468    /// This method executes arbitrary SQL. **NEVER** pass untrusted input directly to this method.
469    /// Always:
470    /// - Use parameterized queries with bound parameters
471    /// - Validate and sanitize SQL templates before execution
472    /// - Only execute SQL generated by the FraiseQL compiler
473    /// - Log SQL execution for audit trails
474    ///
475    /// # Arguments
476    ///
477    /// * `sql` - Raw SQL query to execute (must be safe/trusted)
478    ///
479    /// # Returns
480    ///
481    /// Vec of rows, where each row is a HashMap of column name to JSON value.
482    ///
483    /// # Errors
484    ///
485    /// Returns `FraiseQLError::Database` on query execution failure.
486    ///
487    /// # Example
488    ///
489    /// ```rust,no_run
490    /// # use fraiseql_db::DatabaseAdapter;
491    /// # async fn example(adapter: impl DatabaseAdapter) -> Result<(), Box<dyn std::error::Error>> {
492    /// // Safe: SQL generated by FraiseQL compiler
493    /// let sql = "SELECT category, SUM(revenue) as total FROM tf_sales GROUP BY category";
494    /// let rows = adapter.execute_raw_query(sql).await?;
495    /// for row in rows {
496    ///     println!("Category: {}, Total: {}", row["category"], row["total"]);
497    /// }
498    /// # Ok(())
499    /// # }
500    /// ```
501    async fn execute_raw_query(
502        &self,
503        sql: &str,
504    ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>>;
505
506    /// Execute a row-shaped query against a view, returning typed column values.
507    ///
508    /// Used by the gRPC transport for protobuf encoding of query results.
509    /// The default implementation delegates to `execute_raw_query` and converts
510    /// JSON results to `ColumnValue` vectors.
511    ///
512    /// # Errors
513    ///
514    /// Returns `FraiseQLError::Database` if the adapter returns an error.
515    async fn execute_row_query(
516        &self,
517        view_name: &str,
518        columns: &[crate::types::ColumnSpec],
519        where_sql: Option<&str>,
520        order_by: Option<&str>,
521        limit: Option<u32>,
522        offset: Option<u32>,
523    ) -> Result<Vec<Vec<crate::types::ColumnValue>>> {
524        use crate::types::ColumnValue;
525
526        let mut sql = format!("SELECT * FROM \"{view_name}\"");
527        if let Some(w) = where_sql {
528            sql.push_str(" WHERE ");
529            sql.push_str(w);
530        }
531        if let Some(ob) = order_by {
532            sql.push_str(" ORDER BY ");
533            sql.push_str(ob);
534        }
535        if let Some(l) = limit {
536            use std::fmt::Write;
537            let _ = write!(sql, " LIMIT {l}");
538        }
539        if let Some(o) = offset {
540            use std::fmt::Write;
541            let _ = write!(sql, " OFFSET {o}");
542        }
543
544        let results = self.execute_raw_query(&sql).await?;
545
546        Ok(results
547            .iter()
548            .map(|row| {
549                columns
550                    .iter()
551                    .map(|col| {
552                        row.get(&col.name).map_or(ColumnValue::Null, |v| match v {
553                            serde_json::Value::Null => ColumnValue::Null,
554                            serde_json::Value::Bool(b) => ColumnValue::Boolean(*b),
555                            serde_json::Value::Number(n) => {
556                                if let Some(i) = n.as_i64() {
557                                    ColumnValue::Int64(i)
558                                } else if let Some(f) = n.as_f64() {
559                                    ColumnValue::Float64(f)
560                                } else {
561                                    ColumnValue::Text(n.to_string())
562                                }
563                            },
564                            serde_json::Value::String(s) => ColumnValue::Text(s.clone()),
565                            other => ColumnValue::Json(other.to_string()),
566                        })
567                    })
568                    .collect()
569            })
570            .collect())
571    }
572
573    /// Execute a parameterized aggregate SQL query (GROUP BY / HAVING / window).
574    ///
575    /// `sql` contains `$N` (PostgreSQL), `?` (MySQL / SQLite), or `@P1` (SQL Server)
576    /// placeholders for string and array values; numeric and NULL values may be inlined.
577    /// `params` are the corresponding values in placeholder order.
578    ///
579    /// Unlike `execute_raw_query`, this method accepts bind parameters so that
580    /// user-supplied filter values never appear as string literals in the SQL text,
581    /// eliminating the injection risk that `escape_sql_string` mitigated previously.
582    ///
583    /// # Arguments
584    ///
585    /// * `sql` - SQL with placeholders generated by
586    ///   `AggregationSqlGenerator::generate_parameterized`
587    /// * `params` - Bind parameters in placeholder order
588    ///
589    /// # Returns
590    ///
591    /// Vec of rows, where each row is a `HashMap` of column name to JSON value.
592    ///
593    /// # Errors
594    ///
595    /// Returns `FraiseQLError::Database` on execution failure.
596    /// Returns `FraiseQLError::Database` on adapters that do not support raw SQL
597    /// (e.g., `FraiseWireAdapter`).
598    async fn execute_parameterized_aggregate(
599        &self,
600        sql: &str,
601        params: &[serde_json::Value],
602    ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>>;
603
604    /// Execute a database function call and return all columns as rows.
605    ///
606    /// Builds `SELECT * FROM {function_name}($1, $2, ...)` with one positional placeholder per
607    /// argument, executes it with the provided JSON values, and returns each result row as a
608    /// `HashMap<column_name, json_value>`.
609    ///
610    /// Used by the mutation execution pathway to call stored procedures that return the
611    /// `app.mutation_response` composite type
612    /// `(status, message, entity_id, entity_type, entity jsonb, updated_fields text[],
613    ///   cascade jsonb, metadata jsonb)`.
614    ///
615    /// # Arguments
616    ///
617    /// * `function_name` - Fully-qualified function name (e.g. `fn_create_machine`)
618    /// * `args` - Positional JSON arguments passed as `$1, $2, …` bind parameters
619    ///
620    /// # Errors
621    ///
622    /// Returns `FraiseQLError::Database` on query execution failure.
623    /// Returns `FraiseQLError::Unsupported` on adapters that do not support mutations
624    /// (default implementation — see [`SupportsMutations`]).
625    async fn execute_function_call(
626        &self,
627        function_name: &str,
628        _args: &[serde_json::Value],
629    ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
630        Err(FraiseQLError::Unsupported {
631            message: format!(
632                "Mutations via function calls are not supported by this adapter. \
633                 Function '{function_name}' cannot be executed. \
634                 Use PostgreSQL, MySQL, or SQL Server for mutation support."
635            ),
636        })
637    }
638
639    /// Returns `true` if this adapter supports GraphQL mutation operations.
640    ///
641    /// **This is the authoritative mutation gate.** The executor checks this method
642    /// before dispatching any mutation. Adapters that return `false` will cause
643    /// mutations to fail with a clear `FraiseQLError::Validation` diagnostic instead
644    /// of silently calling the unsupported `execute_function_call` default.
645    ///
646    /// Override to return `false` for read-only adapters (e.g., `SqliteAdapter`,
647    /// `FraiseWireAdapter`). The compile-time [`SupportsMutations`] marker trait
648    /// complements this runtime check — see its documentation for the distinction.
649    ///
650    /// # Default
651    ///
652    /// Returns `true`. All adapters are assumed mutation-capable unless they override
653    /// this method.
654    fn supports_mutations(&self) -> bool {
655        true
656    }
657
658    /// Bump fact table version counters after a successful mutation.
659    ///
660    /// Called by the executor when a mutation definition declares
661    /// `invalidates_fact_tables`. For each listed table the version counter is
662    /// incremented so that subsequent aggregation queries miss the cache and
663    /// re-fetch fresh data.
664    ///
665    /// The default implementation is a **no-op**: adapters that are not cache-
666    /// aware (e.g. `PostgresAdapter`, `SqliteAdapter`) simply return `Ok(())`.
667    /// `CachedDatabaseAdapter` overrides this to call `bump_tf_version($1)` for
668    /// every `FactTableVersionStrategy::VersionTable` table and update the
669    /// in-process version cache.
670    ///
671    /// # Arguments
672    ///
673    /// * `tables` - Fact table names declared by the mutation (validated SQL identifiers; originate
674    ///   from `MutationDefinition.invalidates_fact_tables`)
675    ///
676    /// # Errors
677    ///
678    /// Returns `FraiseQLError::Database` if the version-bump SQL function fails.
679    async fn bump_fact_table_versions(&self, _tables: &[String]) -> Result<()> {
680        Ok(())
681    }
682
683    /// Invalidate cached query results for the specified views.
684    ///
685    /// Called by the executor after a mutation succeeds, so that stale cache
686    /// entries reading from modified views are evicted. The default
687    /// implementation is a no-op; `CachedDatabaseAdapter` overrides this.
688    ///
689    /// View names are passed as `&[ViewName]` so the wrapper's `Arc<str>`
690    /// backing is preserved across the call. Callers that hold a `String`
691    /// can convert in place with `ViewName::from(...)`.
692    ///
693    /// # Returns
694    ///
695    /// The number of cache entries evicted.
696    async fn invalidate_views(&self, _views: &[crate::ViewName]) -> Result<u64> {
697        Ok(0)
698    }
699
700    /// Evict cache entries that contain the given entity UUID.
701    ///
702    /// Called by the executor after a successful UPDATE or DELETE mutation when
703    /// the `mutation_response` includes an `entity_id`. Only cache entries whose
704    /// entity-ID index contains the given UUID are removed; unrelated entries
705    /// remain warm.
706    ///
707    /// The default implementation is a no-op. `CachedDatabaseAdapter` overrides
708    /// this to perform the selective eviction.
709    ///
710    /// # Returns
711    ///
712    /// The number of cache entries evicted.
713    async fn invalidate_by_entity(&self, _entity_type: &str, _entity_id: &str) -> Result<u64> {
714        Ok(0)
715    }
716
717    /// Evict only list (multi-row) cache entries for the given views.
718    ///
719    /// Called by the executor after a successful CREATE mutation. Unlike
720    /// `invalidate_views()`, this preserves single-entity point-lookup entries
721    /// that are unaffected by the newly created entity.
722    ///
723    /// The default implementation delegates to `invalidate_views()` (safe
724    /// fallback for adapters without a `list_index`).  `CachedDatabaseAdapter`
725    /// overrides this to use the dedicated `list_index` for precise eviction.
726    ///
727    /// # Returns
728    ///
729    /// The number of cache entries evicted.
730    async fn invalidate_list_queries(&self, views: &[crate::ViewName]) -> Result<u64> {
731        self.invalidate_views(views).await
732    }
733
734    /// Get database capabilities.
735    ///
736    /// Returns information about what features this database supports,
737    /// including collation strategies and limitations.
738    ///
739    /// # Returns
740    ///
741    /// `DatabaseCapabilities` describing supported features.
742    fn capabilities(&self) -> DatabaseCapabilities {
743        DatabaseCapabilities::from_database_type(self.database_type())
744    }
745
746    /// Run the database's `EXPLAIN` on a SQL statement without executing it.
747    ///
748    /// Returns a JSON representation of the query plan. The format is
749    /// database-specific (e.g. PostgreSQL returns JSON, SQLite returns rows).
750    ///
751    /// The default implementation returns `Unsupported`.
752    async fn explain_query(
753        &self,
754        _sql: &str,
755        _params: &[serde_json::Value],
756    ) -> Result<serde_json::Value> {
757        Err(fraiseql_error::FraiseQLError::Unsupported {
758            message: "EXPLAIN not available for this database adapter".to_string(),
759        })
760    }
761
762    /// Run `EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)` against a view with the
763    /// same parameterized WHERE clause that `execute_where_query` would use.
764    ///
765    /// Unlike `explain_query`, this method uses **real bound parameters** and
766    /// **actually executes the query** (ANALYZE mode), so the plan reflects
767    /// PostgreSQL's runtime statistics for the given filter values.
768    ///
769    /// Only PostgreSQL supports this; other adapters return
770    /// `FraiseQLError::Unsupported` by default.
771    ///
772    /// # Arguments
773    ///
774    /// * `view` - View name (e.g., "v_user")
775    /// * `where_clause` - Optional filter (same as `execute_where_query`)
776    /// * `limit` - Optional row limit
777    /// * `offset` - Optional row offset
778    ///
779    /// # Errors
780    ///
781    /// Returns `FraiseQLError::Database` on execution failure.
782    /// Returns `FraiseQLError::Unsupported` for non-PostgreSQL adapters.
783    async fn explain_where_query(
784        &self,
785        _view: &str,
786        _where_clause: Option<&WhereClause>,
787        _limit: Option<u32>,
788        _offset: Option<u32>,
789    ) -> Result<serde_json::Value> {
790        Err(fraiseql_error::FraiseQLError::Unsupported {
791            message: "EXPLAIN ANALYZE is not available for this database adapter. \
792                      Only PostgreSQL supports explain_where_query."
793                .to_string(),
794        })
795    }
796
797    /// Returns the mutation strategy used by this adapter.
798    ///
799    /// The default is `FunctionCall` (stored procedures). Adapters that generate
800    /// direct SQL (e.g., SQLite) override this to return `DirectSql`.
801    fn mutation_strategy(&self) -> MutationStrategy {
802        MutationStrategy::FunctionCall
803    }
804
805    /// Set transaction-scoped session variables before query/mutation execution.
806    ///
807    /// Called at the start of each mutation request when `SessionVariablesConfig`
808    /// is populated.  Each `(name, value)` pair is applied via
809    /// `SELECT set_config($1, $2, true)` (transaction-local, auto-reset on
810    /// commit/rollback).
811    ///
812    /// SQL functions and views can then read these settings via
813    /// `current_setting('app.tenant_id', true)`.
814    ///
815    /// # Arguments
816    ///
817    /// * `variables` - Slice of `(setting_name, value)` pairs to inject. Names must be safe
818    ///   PostgreSQL setting names (e.g. `"app.tenant_id"`).
819    ///
820    /// # Default
821    ///
822    /// No-op.  Only `PostgresAdapter` overrides this with `set_config()` calls.
823    /// MySQL, SQLite, and SQL Server adapters inherit the no-op default.
824    ///
825    /// # Errors
826    ///
827    /// Returns `FraiseQLError::Database` if the underlying `set_config()` call fails.
828    async fn set_session_variables(&self, _variables: &[(&str, &str)]) -> Result<()> {
829        Ok(())
830    }
831
832    /// Execute a direct SQL mutation (INSERT/UPDATE/DELETE) and return the
833    /// mutation response rows as JSON objects.
834    ///
835    /// Only adapters using `MutationStrategy::DirectSql` need to override this.
836    /// The default implementation returns `Unsupported`.
837    ///
838    /// # Errors
839    ///
840    /// Returns `FraiseQLError::Unsupported` by default.
841    /// Returns `FraiseQLError::Database` on SQL execution failure.
842    /// Returns `FraiseQLError::Validation` on invalid mutation parameters.
843    async fn execute_direct_mutation(
844        &self,
845        _ctx: &DirectMutationContext<'_>,
846    ) -> Result<Vec<serde_json::Value>> {
847        Err(FraiseQLError::Unsupported {
848            message: "Direct SQL mutations are not supported by this adapter. \
849                      Use execute_function_call for stored-procedure mutations."
850                .to_string(),
851        })
852    }
853
854    /// Retrieve query performance statistics from the database.
855    ///
856    /// Returns the top-N queries ordered by total execution time (descending).
857    /// The exact data source depends on the backend:
858    /// - PostgreSQL: `pg_stat_statements` (requires extension)
859    /// - MySQL: `performance_schema.events_statements_summary_by_digest`
860    /// - SQL Server: `sys.dm_exec_query_stats`
861    /// - SQLite / Wire: empty (no stats available)
862    ///
863    /// # Arguments
864    ///
865    /// * `limit` - Maximum number of entries to return.
866    ///
867    /// # Errors
868    ///
869    /// Returns `FraiseQLError::Database` if the stats query fails.
870    async fn query_stats(&self, _limit: u32) -> Result<Vec<crate::types::QueryStatEntry>> {
871        Ok(vec![])
872    }
873
874    /// Retrieve statistics for a single query by its ID.
875    ///
876    /// The default implementation fetches up to 1000 entries via
877    /// [`query_stats`](Self::query_stats) and filters client-side.
878    /// Backends with efficient single-query lookup (PostgreSQL, SQL Server)
879    /// should override with a `WHERE` clause.
880    ///
881    /// # Errors
882    ///
883    /// Returns `FraiseQLError::Database` if the underlying query fails.
884    async fn query_stats_by_id(&self, id: &str) -> Result<Option<crate::types::QueryStatEntry>> {
885        let stats = self.query_stats(1000).await?;
886        Ok(stats.into_iter().find(|e| e.query_id == id))
887    }
888
889    /// Reset query performance statistics.
890    ///
891    /// Only PostgreSQL supports this (via `pg_stat_statements_reset()`).
892    /// All other adapters return `Unsupported`.
893    ///
894    /// # Errors
895    ///
896    /// Returns `FraiseQLError::Unsupported` for adapters that cannot reset stats.
897    /// Returns `FraiseQLError::Database` if the reset command fails.
898    async fn reset_query_stats(&self) -> Result<()> {
899        Err(FraiseQLError::Unsupported {
900            message: "Query stats reset is not supported by this database adapter".to_string(),
901        })
902    }
903
904    /// Notify the adapter that the schema has changed.
905    ///
906    /// Called during hot-reload after the new schema has been validated.
907    /// Adapters that maintain schema-dependent state (e.g. cache keyed by schema
908    /// version) should clear or rebuild that state here.
909    ///
910    /// The default implementation is a no-op.
911    fn on_schema_reload(&self) {}
912}
913
914#[cfg(test)]
915mod tests;