Skip to main content

fraiseql_db/
traits.rs

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