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 /// Connection-affine variant of
605 /// [`execute_parameterized_aggregate`](Self::execute_parameterized_aggregate).
606 ///
607 /// Applies `session_vars` transaction-locally on the same connection that
608 /// runs the aggregate, so aggregate views backed by `current_setting()` RLS
609 /// observe the configured values (fixes #329 for the aggregate path). See
610 /// [`execute_function_call_with_session`](Self::execute_function_call_with_session)
611 /// for the non-PostgreSQL default behaviour.
612 ///
613 /// # Errors
614 ///
615 /// Same errors as [`execute_parameterized_aggregate`](Self::execute_parameterized_aggregate);
616 /// additionally returns `FraiseQLError::Database` if `set_config` fails.
617 async fn execute_parameterized_aggregate_with_session(
618 &self,
619 sql: &str,
620 params: &[serde_json::Value],
621 _session_vars: &[(&str, &str)],
622 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
623 self.execute_parameterized_aggregate(sql, params).await
624 }
625
626 /// Execute a database function call and return all columns as rows.
627 ///
628 /// Builds `SELECT * FROM {function_name}($1, $2, ...)` with one positional placeholder per
629 /// argument, executes it with the provided JSON values, and returns each result row as a
630 /// `HashMap<column_name, json_value>`.
631 ///
632 /// Used by the mutation execution pathway to call stored procedures that return the
633 /// `app.mutation_response` composite type
634 /// `(status, message, entity_id, entity_type, entity jsonb, updated_fields text[],
635 /// cascade jsonb, metadata jsonb)`.
636 ///
637 /// # Arguments
638 ///
639 /// * `function_name` - Fully-qualified function name (e.g. `fn_create_machine`)
640 /// * `args` - Positional JSON arguments passed as `$1, $2, …` bind parameters
641 ///
642 /// # Errors
643 ///
644 /// Returns `FraiseQLError::Database` on query execution failure.
645 /// Returns `FraiseQLError::Unsupported` on adapters that do not support mutations
646 /// (default implementation — see [`SupportsMutations`]).
647 async fn execute_function_call(
648 &self,
649 function_name: &str,
650 _args: &[serde_json::Value],
651 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
652 Err(FraiseQLError::Unsupported {
653 message: format!(
654 "Mutations via function calls are not supported by this adapter. \
655 Function '{function_name}' cannot be executed. \
656 Use PostgreSQL, MySQL, or SQL Server for mutation support."
657 ),
658 })
659 }
660
661 /// Returns `true` if this adapter supports GraphQL mutation operations.
662 ///
663 /// **This is the authoritative mutation gate.** The executor checks this method
664 /// before dispatching any mutation. Adapters that return `false` will cause
665 /// mutations to fail with a clear `FraiseQLError::Validation` diagnostic instead
666 /// of silently calling the unsupported `execute_function_call` default.
667 ///
668 /// Override to return `false` for read-only adapters (e.g., `SqliteAdapter`,
669 /// `FraiseWireAdapter`). The compile-time [`SupportsMutations`] marker trait
670 /// complements this runtime check — see its documentation for the distinction.
671 ///
672 /// # Default
673 ///
674 /// Returns `true`. All adapters are assumed mutation-capable unless they override
675 /// this method.
676 fn supports_mutations(&self) -> bool {
677 true
678 }
679
680 /// Bump fact table version counters after a successful mutation.
681 ///
682 /// Called by the executor when a mutation definition declares
683 /// `invalidates_fact_tables`. For each listed table the version counter is
684 /// incremented so that subsequent aggregation queries miss the cache and
685 /// re-fetch fresh data.
686 ///
687 /// The default implementation is a **no-op**: adapters that are not cache-
688 /// aware (e.g. `PostgresAdapter`, `SqliteAdapter`) simply return `Ok(())`.
689 /// `CachedDatabaseAdapter` overrides this to call `bump_tf_version($1)` for
690 /// every `FactTableVersionStrategy::VersionTable` table and update the
691 /// in-process version cache.
692 ///
693 /// # Arguments
694 ///
695 /// * `tables` - Fact table names declared by the mutation (validated SQL identifiers; originate
696 /// from `MutationDefinition.invalidates_fact_tables`)
697 ///
698 /// # Errors
699 ///
700 /// Returns `FraiseQLError::Database` if the version-bump SQL function fails.
701 async fn bump_fact_table_versions(&self, _tables: &[String]) -> Result<()> {
702 Ok(())
703 }
704
705 /// Invalidate cached query results for the specified views.
706 ///
707 /// Called by the executor after a mutation succeeds, so that stale cache
708 /// entries reading from modified views are evicted. The default
709 /// implementation is a no-op; `CachedDatabaseAdapter` overrides this.
710 ///
711 /// View names are passed as `&[ViewName]` so the wrapper's `Arc<str>`
712 /// backing is preserved across the call. Callers that hold a `String`
713 /// can convert in place with `ViewName::from(...)`.
714 ///
715 /// # Returns
716 ///
717 /// The number of cache entries evicted.
718 async fn invalidate_views(&self, _views: &[crate::ViewName]) -> Result<u64> {
719 Ok(0)
720 }
721
722 /// Evict cache entries that contain the given entity UUID.
723 ///
724 /// Called by the executor after a successful UPDATE or DELETE mutation when
725 /// the `mutation_response` includes an `entity_id`. Only cache entries whose
726 /// entity-ID index contains the given UUID are removed; unrelated entries
727 /// remain warm.
728 ///
729 /// The default implementation is a no-op. `CachedDatabaseAdapter` overrides
730 /// this to perform the selective eviction.
731 ///
732 /// # Returns
733 ///
734 /// The number of cache entries evicted.
735 async fn invalidate_by_entity(&self, _entity_type: &str, _entity_id: &str) -> Result<u64> {
736 Ok(0)
737 }
738
739 /// Evict only list (multi-row) cache entries for the given views.
740 ///
741 /// Called by the executor after a successful CREATE mutation. Unlike
742 /// `invalidate_views()`, this preserves single-entity point-lookup entries
743 /// that are unaffected by the newly created entity.
744 ///
745 /// The default implementation delegates to `invalidate_views()` (safe
746 /// fallback for adapters without a `list_index`). `CachedDatabaseAdapter`
747 /// overrides this to use the dedicated `list_index` for precise eviction.
748 ///
749 /// # Returns
750 ///
751 /// The number of cache entries evicted.
752 async fn invalidate_list_queries(&self, views: &[crate::ViewName]) -> Result<u64> {
753 self.invalidate_views(views).await
754 }
755
756 /// Get database capabilities.
757 ///
758 /// Returns information about what features this database supports,
759 /// including collation strategies and limitations.
760 ///
761 /// # Returns
762 ///
763 /// `DatabaseCapabilities` describing supported features.
764 fn capabilities(&self) -> DatabaseCapabilities {
765 DatabaseCapabilities::from_database_type(self.database_type())
766 }
767
768 /// Run the database's `EXPLAIN` on a SQL statement without executing it.
769 ///
770 /// Returns a JSON representation of the query plan. The format is
771 /// database-specific (e.g. PostgreSQL returns JSON, SQLite returns rows).
772 ///
773 /// The default implementation returns `Unsupported`.
774 async fn explain_query(
775 &self,
776 _sql: &str,
777 _params: &[serde_json::Value],
778 ) -> Result<serde_json::Value> {
779 Err(fraiseql_error::FraiseQLError::Unsupported {
780 message: "EXPLAIN not available for this database adapter".to_string(),
781 })
782 }
783
784 /// Run `EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)` against a view with the
785 /// same parameterized WHERE clause that `execute_where_query` would use.
786 ///
787 /// Unlike `explain_query`, this method uses **real bound parameters** and
788 /// **actually executes the query** (ANALYZE mode), so the plan reflects
789 /// PostgreSQL's runtime statistics for the given filter values.
790 ///
791 /// Only PostgreSQL supports this; other adapters return
792 /// `FraiseQLError::Unsupported` by default.
793 ///
794 /// # Arguments
795 ///
796 /// * `view` - View name (e.g., "v_user")
797 /// * `where_clause` - Optional filter (same as `execute_where_query`)
798 /// * `limit` - Optional row limit
799 /// * `offset` - Optional row offset
800 ///
801 /// # Errors
802 ///
803 /// Returns `FraiseQLError::Database` on execution failure.
804 /// Returns `FraiseQLError::Unsupported` for non-PostgreSQL adapters.
805 async fn explain_where_query(
806 &self,
807 _view: &str,
808 _where_clause: Option<&WhereClause>,
809 _limit: Option<u32>,
810 _offset: Option<u32>,
811 ) -> Result<serde_json::Value> {
812 Err(fraiseql_error::FraiseQLError::Unsupported {
813 message: "EXPLAIN ANALYZE is not available for this database adapter. \
814 Only PostgreSQL supports explain_where_query."
815 .to_string(),
816 })
817 }
818
819 /// Returns the mutation strategy used by this adapter.
820 ///
821 /// The default is `FunctionCall` (stored procedures). Adapters that generate
822 /// direct SQL (e.g., SQLite) override this to return `DirectSql`.
823 fn mutation_strategy(&self) -> MutationStrategy {
824 MutationStrategy::FunctionCall
825 }
826
827 /// Execute a database function call after pinning session variables on the
828 /// **same connection** within the **same transaction** as the call.
829 ///
830 /// This is the connection-affine variant of
831 /// [`execute_function_call`](Self::execute_function_call): the `set_config(..., true)`
832 /// calls and the `SELECT * FROM fn(...)` call share one pooled connection inside one
833 /// transaction, so transaction-local GUCs are visible to the function body (fixes #329).
834 ///
835 /// Adapters that do not support session variables (MySQL, SQLite, SQL
836 /// Server, mocks) inherit the default implementation, which silently drops
837 /// `session_vars` and delegates to [`execute_function_call`](Self::execute_function_call) —
838 /// safe, because those backends never applied session variables in the first
839 /// place.
840 ///
841 /// # Arguments
842 ///
843 /// * `function_name` - Fully-qualified function name
844 /// * `args` - Positional JSON arguments passed as `$1, $2, …`
845 /// * `session_vars` - `(setting_name, value)` pairs applied with `SELECT set_config(name,
846 /// value, true)` before the function call. Pass `&[]` when no session variables are
847 /// configured.
848 ///
849 /// # Errors
850 ///
851 /// Same as [`execute_function_call`](Self::execute_function_call); additionally returns
852 /// `FraiseQLError::Database` if `set_config` fails on any pair.
853 async fn execute_function_call_with_session(
854 &self,
855 function_name: &str,
856 args: &[serde_json::Value],
857 _session_vars: &[(&str, &str)],
858 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
859 // Default: ignore session_vars and delegate. Safe for non-PostgreSQL
860 // adapters, which never applied session variables in the first place.
861 self.execute_function_call(function_name, args).await
862 }
863
864 /// Connection-affine variant of [`execute_where_query_arc`](Self::execute_where_query_arc).
865 ///
866 /// Applies `session_vars` transaction-locally on the same connection that
867 /// runs the read, so PostgreSQL Row-Level-Security policies backed by
868 /// `current_setting()` see the configured values (fixes #329). See
869 /// [`execute_function_call_with_session`](Self::execute_function_call_with_session) for the
870 /// rationale and the non-PostgreSQL default behaviour.
871 ///
872 /// # Errors
873 ///
874 /// Same errors as [`execute_where_query_arc`](Self::execute_where_query_arc); additionally
875 /// returns `FraiseQLError::Database` if `set_config` fails on any pair.
876 async fn execute_where_query_arc_with_session(
877 &self,
878 view: &str,
879 where_clause: Option<&WhereClause>,
880 limit: Option<u32>,
881 offset: Option<u32>,
882 order_by: Option<&[OrderByClause]>,
883 _session_vars: &[(&str, &str)],
884 ) -> Result<Arc<Vec<JsonbValue>>> {
885 self.execute_where_query_arc(view, where_clause, limit, offset, order_by).await
886 }
887
888 /// Connection-affine variant of
889 /// [`execute_with_projection_arc`](Self::execute_with_projection_arc).
890 ///
891 /// See [`execute_where_query_arc_with_session`](Self::execute_where_query_arc_with_session) for
892 /// the rationale.
893 ///
894 /// # Errors
895 ///
896 /// Same errors as [`execute_with_projection_arc`](Self::execute_with_projection_arc);
897 /// additionally returns `FraiseQLError::Database` if `set_config` fails on any pair.
898 async fn execute_with_projection_arc_with_session(
899 &self,
900 request: &ProjectionRequest<'_>,
901 _session_vars: &[(&str, &str)],
902 ) -> Result<Arc<Vec<JsonbValue>>> {
903 self.execute_with_projection_arc(request).await
904 }
905
906 /// Execute a direct SQL mutation (INSERT/UPDATE/DELETE) and return the
907 /// mutation response rows as JSON objects.
908 ///
909 /// Only adapters using `MutationStrategy::DirectSql` need to override this.
910 /// The default implementation returns `Unsupported`.
911 ///
912 /// # Errors
913 ///
914 /// Returns `FraiseQLError::Unsupported` by default.
915 /// Returns `FraiseQLError::Database` on SQL execution failure.
916 /// Returns `FraiseQLError::Validation` on invalid mutation parameters.
917 async fn execute_direct_mutation(
918 &self,
919 _ctx: &DirectMutationContext<'_>,
920 ) -> Result<Vec<serde_json::Value>> {
921 Err(FraiseQLError::Unsupported {
922 message: "Direct SQL mutations are not supported by this adapter. \
923 Use execute_function_call for stored-procedure mutations."
924 .to_string(),
925 })
926 }
927
928 /// Retrieve query performance statistics from the database.
929 ///
930 /// Returns the top-N queries ordered by total execution time (descending).
931 /// The exact data source depends on the backend:
932 /// - PostgreSQL: `pg_stat_statements` (requires extension)
933 /// - MySQL: `performance_schema.events_statements_summary_by_digest`
934 /// - SQL Server: `sys.dm_exec_query_stats`
935 /// - SQLite / Wire: empty (no stats available)
936 ///
937 /// # Arguments
938 ///
939 /// * `limit` - Maximum number of entries to return.
940 ///
941 /// # Errors
942 ///
943 /// Returns `FraiseQLError::Database` if the stats query fails.
944 async fn query_stats(&self, _limit: u32) -> Result<Vec<crate::types::QueryStatEntry>> {
945 Ok(vec![])
946 }
947
948 /// Retrieve statistics for a single query by its ID.
949 ///
950 /// The default implementation fetches up to 1000 entries via
951 /// [`query_stats`](Self::query_stats) and filters client-side.
952 /// Backends with efficient single-query lookup (PostgreSQL, SQL Server)
953 /// should override with a `WHERE` clause.
954 ///
955 /// # Errors
956 ///
957 /// Returns `FraiseQLError::Database` if the underlying query fails.
958 async fn query_stats_by_id(&self, id: &str) -> Result<Option<crate::types::QueryStatEntry>> {
959 let stats = self.query_stats(1000).await?;
960 Ok(stats.into_iter().find(|e| e.query_id == id))
961 }
962
963 /// Reset query performance statistics.
964 ///
965 /// Only PostgreSQL supports this (via `pg_stat_statements_reset()`).
966 /// All other adapters return `Unsupported`.
967 ///
968 /// # Errors
969 ///
970 /// Returns `FraiseQLError::Unsupported` for adapters that cannot reset stats.
971 /// Returns `FraiseQLError::Database` if the reset command fails.
972 async fn reset_query_stats(&self) -> Result<()> {
973 Err(FraiseQLError::Unsupported {
974 message: "Query stats reset is not supported by this database adapter".to_string(),
975 })
976 }
977
978 /// Notify the adapter that the schema has changed.
979 ///
980 /// Called during hot-reload after the new schema has been validated.
981 /// Adapters that maintain schema-dependent state (e.g. cache keyed by schema
982 /// version) should clear or rebuild that state here.
983 ///
984 /// The default implementation is a no-op.
985 fn on_schema_reload(&self) {}
986}
987
988#[cfg(test)]
989mod tests;