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(¶ms[0])
142/// .bind(¶ms[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;