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(¶ms[0])
138/// .bind(¶ms[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 ) -> Result<Vec<JsonbValue>>;
253
254 /// Execute a WHERE query with SQL field projection optimization.
255 ///
256 /// Projects only the requested fields at the database level, reducing network payload
257 /// and JSON deserialization overhead by **40-55%** based on production measurements.
258 ///
259 /// This is the primary query execution method for optimized GraphQL queries.
260 /// It automatically selects only the fields requested in the GraphQL query, avoiding
261 /// unnecessary network transfer and deserialization of unused fields.
262 ///
263 /// # Automatic Projection
264 ///
265 /// In most cases, you don't call this directly. The `Executor` automatically:
266 /// 1. Determines which fields the GraphQL query requests
267 /// 2. Generates a `SqlProjectionHint` using database-specific SQL
268 /// 3. Calls this method with the projection hint
269 ///
270 /// # Arguments
271 ///
272 /// * `view` - View name (e.g., "v_user", "v_post")
273 /// * `projection` - Optional SQL projection hint with field list
274 /// - `Some(hint)`: Use projection to select only requested fields
275 /// - `None`: Falls back to standard query (full JSONB column)
276 /// * `where_clause` - Optional WHERE clause AST for filtering
277 /// * `limit` - Optional row limit (for pagination)
278 ///
279 /// # Returns
280 ///
281 /// Vec of JSONB values, either:
282 /// - Full objects (when projection is None)
283 /// - Projected objects with only requested fields (when projection is Some)
284 ///
285 /// # Errors
286 ///
287 /// Returns `FraiseQLError::Database` on query execution failure, including:
288 /// - Connection pool exhaustion
289 /// - SQL execution errors
290 /// - Type mismatches
291 ///
292 /// # Performance Characteristics
293 ///
294 /// When projection is provided (recommended):
295 /// - **Latency**: 40-55% reduction vs full object fetch
296 /// - **Network**: 40-55% smaller payload (proportional to unused fields)
297 /// - **Throughput**: Maintains 250+ Kelem/s (elements per second)
298 /// - **Memory**: Proportional to projected fields only
299 ///
300 /// Improvement scales with:
301 /// - Percentage of unused fields (more unused = more improvement)
302 /// - Size of result set (larger sets benefit more)
303 /// - Network latency (network-bound queries benefit most)
304 ///
305 /// When projection is None:
306 /// - Behavior identical to `execute_where_query()`
307 /// - Returns full JSONB column
308 /// - Used for compatibility/debugging
309 ///
310 /// # Database Support
311 ///
312 /// | Database | Status | Implementation |
313 /// |----------|--------|-----------------|
314 /// | PostgreSQL | ✅ Optimized | `jsonb_build_object()` |
315 /// | MySQL | ⏳ Fallback | Server-side filtering (planned) |
316 /// | SQLite | ⏳ Fallback | Server-side filtering (planned) |
317 /// | SQL Server | ⏳ Fallback | Server-side filtering (planned) |
318 ///
319 /// # Example: Direct Usage (Advanced)
320 ///
321 /// ```no_run
322 /// // Requires: running PostgreSQL database and a DatabaseAdapter implementation.
323 /// use fraiseql_db::types::SqlProjectionHint;
324 /// use fraiseql_db::traits::DatabaseAdapter;
325 /// use fraiseql_db::DatabaseType;
326 ///
327 /// # async fn example(adapter: &impl DatabaseAdapter) -> Result<(), Box<dyn std::error::Error>> {
328 /// let projection = SqlProjectionHint {
329 /// database: DatabaseType::PostgreSQL,
330 /// projection_template: "jsonb_build_object(\
331 /// 'id', data->>'id', \
332 /// 'name', data->>'name', \
333 /// 'email', data->>'email'\
334 /// )".to_string(),
335 /// estimated_reduction_percent: 75,
336 /// };
337 ///
338 /// let results = adapter
339 /// .execute_with_projection("v_user", Some(&projection), None, Some(100), None, None)
340 /// .await?;
341 ///
342 /// // results only contain id, name, email fields
343 /// // 75% smaller than fetching all fields
344 /// # Ok(())
345 /// # }
346 /// ```
347 ///
348 /// # Example: Fallback (No Projection)
349 ///
350 /// ```no_run
351 /// // Requires: running PostgreSQL database and a DatabaseAdapter implementation.
352 /// # use fraiseql_db::traits::DatabaseAdapter;
353 /// # async fn example(adapter: &impl DatabaseAdapter) -> Result<(), Box<dyn std::error::Error>> {
354 /// // For debugging or when projection not available
355 /// let results = adapter
356 /// .execute_with_projection("v_user", None, None, Some(100), None, None)
357 /// .await?;
358 ///
359 /// // Equivalent to execute_where_query() - returns full objects
360 /// # Ok(())
361 /// # }
362 /// ```
363 ///
364 /// # See Also
365 ///
366 /// - `execute_where_query()` - Standard query without projection
367 /// - `SqlProjectionHint` - Structure defining field projection
368 /// - [Projection Optimization Guide](https://docs.fraiseql.rs/performance/projection-optimization.md)
369 async fn execute_with_projection(
370 &self,
371 view: &str,
372 projection: Option<&SqlProjectionHint>,
373 where_clause: Option<&WhereClause>,
374 limit: Option<u32>,
375 offset: Option<u32>,
376 order_by: Option<&[OrderByClause]>,
377 ) -> Result<Vec<JsonbValue>>;
378
379 /// Like `execute_where_query` but returns the result wrapped in an `Arc`.
380 ///
381 /// The default implementation wraps the result of `execute_where_query` in a
382 /// fresh `Arc`. `CachedDatabaseAdapter` overrides this to return the cached `Arc`
383 /// directly — eliminating the full `Vec<JsonbValue>` clone that the non-`Arc`
384 /// path requires on every cache hit.
385 ///
386 /// Callers on the hot query path should prefer this variant and borrow from the
387 /// `Arc` via `&**arc` rather than taking ownership.
388 ///
389 /// # Errors
390 ///
391 /// Same errors as `execute_where_query`.
392 async fn execute_where_query_arc(
393 &self,
394 view: &str,
395 where_clause: Option<&WhereClause>,
396 limit: Option<u32>,
397 offset: Option<u32>,
398 order_by: Option<&[OrderByClause]>,
399 ) -> Result<Arc<Vec<JsonbValue>>> {
400 self.execute_where_query(view, where_clause, limit, offset, order_by)
401 .await
402 .map(Arc::new)
403 }
404
405 /// Like `execute_with_projection` but returns the result wrapped in an `Arc`.
406 ///
407 /// The default implementation wraps the result of `execute_with_projection` in a
408 /// fresh `Arc`. `CachedDatabaseAdapter` overrides this to return the cached `Arc`
409 /// directly — eliminating the full `Vec<JsonbValue>` clone that the non-`Arc`
410 /// path requires on every cache hit.
411 ///
412 /// # Errors
413 ///
414 /// Same errors as `execute_with_projection`.
415 async fn execute_with_projection_arc(
416 &self,
417 view: &str,
418 projection: Option<&SqlProjectionHint>,
419 where_clause: Option<&WhereClause>,
420 limit: Option<u32>,
421 offset: Option<u32>,
422 order_by: Option<&[OrderByClause]>,
423 ) -> Result<Arc<Vec<JsonbValue>>> {
424 self.execute_with_projection(view, projection, where_clause, limit, offset, order_by)
425 .await
426 .map(Arc::new)
427 }
428
429 /// Get database type (for logging/metrics).
430 ///
431 /// Used to identify which database backend is in use.
432 fn database_type(&self) -> DatabaseType;
433
434 /// Health check - verify database connectivity.
435 ///
436 /// Executes a simple query (e.g., `SELECT 1`) to verify the database is reachable.
437 ///
438 /// # Errors
439 ///
440 /// Returns `FraiseQLError::Database` if health check fails.
441 async fn health_check(&self) -> Result<()>;
442
443 /// Get connection pool metrics.
444 ///
445 /// Returns current statistics about the connection pool:
446 /// - Total connections
447 /// - Idle connections
448 /// - Active connections
449 /// - Waiting requests
450 fn pool_metrics(&self) -> PoolMetrics;
451
452 /// Execute raw SQL query and return rows as JSON objects.
453 ///
454 /// Used for aggregation queries where we need full row data, not just JSONB column.
455 ///
456 /// # Security Warning
457 ///
458 /// This method executes arbitrary SQL. **NEVER** pass untrusted input directly to this method.
459 /// Always:
460 /// - Use parameterized queries with bound parameters
461 /// - Validate and sanitize SQL templates before execution
462 /// - Only execute SQL generated by the FraiseQL compiler
463 /// - Log SQL execution for audit trails
464 ///
465 /// # Arguments
466 ///
467 /// * `sql` - Raw SQL query to execute (must be safe/trusted)
468 ///
469 /// # Returns
470 ///
471 /// Vec of rows, where each row is a HashMap of column name to JSON value.
472 ///
473 /// # Errors
474 ///
475 /// Returns `FraiseQLError::Database` on query execution failure.
476 ///
477 /// # Example
478 ///
479 /// ```rust,no_run
480 /// # use fraiseql_db::DatabaseAdapter;
481 /// # async fn example(adapter: impl DatabaseAdapter) -> Result<(), Box<dyn std::error::Error>> {
482 /// // Safe: SQL generated by FraiseQL compiler
483 /// let sql = "SELECT category, SUM(revenue) as total FROM tf_sales GROUP BY category";
484 /// let rows = adapter.execute_raw_query(sql).await?;
485 /// for row in rows {
486 /// println!("Category: {}, Total: {}", row["category"], row["total"]);
487 /// }
488 /// # Ok(())
489 /// # }
490 /// ```
491 async fn execute_raw_query(
492 &self,
493 sql: &str,
494 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>>;
495
496 /// Execute a row-shaped query against a view, returning typed column values.
497 ///
498 /// Used by the gRPC transport for protobuf encoding of query results.
499 /// The default implementation delegates to `execute_raw_query` and converts
500 /// JSON results to `ColumnValue` vectors.
501 ///
502 /// # Errors
503 ///
504 /// Returns `FraiseQLError::Database` if the adapter returns an error.
505 async fn execute_row_query(
506 &self,
507 view_name: &str,
508 columns: &[crate::types::ColumnSpec],
509 where_sql: Option<&str>,
510 order_by: Option<&str>,
511 limit: Option<u32>,
512 offset: Option<u32>,
513 ) -> Result<Vec<Vec<crate::types::ColumnValue>>> {
514 use crate::types::ColumnValue;
515
516 let mut sql = format!("SELECT * FROM \"{view_name}\"");
517 if let Some(w) = where_sql {
518 sql.push_str(" WHERE ");
519 sql.push_str(w);
520 }
521 if let Some(ob) = order_by {
522 sql.push_str(" ORDER BY ");
523 sql.push_str(ob);
524 }
525 if let Some(l) = limit {
526 use std::fmt::Write;
527 let _ = write!(sql, " LIMIT {l}");
528 }
529 if let Some(o) = offset {
530 use std::fmt::Write;
531 let _ = write!(sql, " OFFSET {o}");
532 }
533
534 let results = self.execute_raw_query(&sql).await?;
535
536 Ok(results
537 .iter()
538 .map(|row| {
539 columns
540 .iter()
541 .map(|col| {
542 row.get(&col.name).map_or(ColumnValue::Null, |v| match v {
543 serde_json::Value::Null => ColumnValue::Null,
544 serde_json::Value::Bool(b) => ColumnValue::Boolean(*b),
545 serde_json::Value::Number(n) => {
546 if let Some(i) = n.as_i64() {
547 ColumnValue::Int64(i)
548 } else if let Some(f) = n.as_f64() {
549 ColumnValue::Float64(f)
550 } else {
551 ColumnValue::Text(n.to_string())
552 }
553 },
554 serde_json::Value::String(s) => ColumnValue::Text(s.clone()),
555 other => ColumnValue::Json(other.to_string()),
556 })
557 })
558 .collect()
559 })
560 .collect())
561 }
562
563 /// Execute a parameterized aggregate SQL query (GROUP BY / HAVING / window).
564 ///
565 /// `sql` contains `$N` (PostgreSQL), `?` (MySQL / SQLite), or `@P1` (SQL Server)
566 /// placeholders for string and array values; numeric and NULL values may be inlined.
567 /// `params` are the corresponding values in placeholder order.
568 ///
569 /// Unlike `execute_raw_query`, this method accepts bind parameters so that
570 /// user-supplied filter values never appear as string literals in the SQL text,
571 /// eliminating the injection risk that `escape_sql_string` mitigated previously.
572 ///
573 /// # Arguments
574 ///
575 /// * `sql` - SQL with placeholders generated by
576 /// `AggregationSqlGenerator::generate_parameterized`
577 /// * `params` - Bind parameters in placeholder order
578 ///
579 /// # Returns
580 ///
581 /// Vec of rows, where each row is a `HashMap` of column name to JSON value.
582 ///
583 /// # Errors
584 ///
585 /// Returns `FraiseQLError::Database` on execution failure.
586 /// Returns `FraiseQLError::Database` on adapters that do not support raw SQL
587 /// (e.g., `FraiseWireAdapter`).
588 async fn execute_parameterized_aggregate(
589 &self,
590 sql: &str,
591 params: &[serde_json::Value],
592 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>>;
593
594 /// Execute a database function call and return all columns as rows.
595 ///
596 /// Builds `SELECT * FROM {function_name}($1, $2, ...)` with one positional placeholder per
597 /// argument, executes it with the provided JSON values, and returns each result row as a
598 /// `HashMap<column_name, json_value>`.
599 ///
600 /// Used by the mutation execution pathway to call stored procedures that return the
601 /// `app.mutation_response` composite type
602 /// `(status, message, entity_id, entity_type, entity jsonb, updated_fields text[],
603 /// cascade jsonb, metadata jsonb)`.
604 ///
605 /// # Arguments
606 ///
607 /// * `function_name` - Fully-qualified function name (e.g. `fn_create_machine`)
608 /// * `args` - Positional JSON arguments passed as `$1, $2, …` bind parameters
609 ///
610 /// # Errors
611 ///
612 /// Returns `FraiseQLError::Database` on query execution failure.
613 /// Returns `FraiseQLError::Unsupported` on adapters that do not support mutations
614 /// (default implementation — see [`SupportsMutations`]).
615 async fn execute_function_call(
616 &self,
617 function_name: &str,
618 _args: &[serde_json::Value],
619 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
620 Err(FraiseQLError::Unsupported {
621 message: format!(
622 "Mutations via function calls are not supported by this adapter. \
623 Function '{function_name}' cannot be executed. \
624 Use PostgreSQL, MySQL, or SQL Server for mutation support."
625 ),
626 })
627 }
628
629 /// Returns `true` if this adapter supports GraphQL mutation operations.
630 ///
631 /// **This is the authoritative mutation gate.** The executor checks this method
632 /// before dispatching any mutation. Adapters that return `false` will cause
633 /// mutations to fail with a clear `FraiseQLError::Validation` diagnostic instead
634 /// of silently calling the unsupported `execute_function_call` default.
635 ///
636 /// Override to return `false` for read-only adapters (e.g., `SqliteAdapter`,
637 /// `FraiseWireAdapter`). The compile-time [`SupportsMutations`] marker trait
638 /// complements this runtime check — see its documentation for the distinction.
639 ///
640 /// # Default
641 ///
642 /// Returns `true`. All adapters are assumed mutation-capable unless they override
643 /// this method.
644 fn supports_mutations(&self) -> bool {
645 true
646 }
647
648 /// Bump fact table version counters after a successful mutation.
649 ///
650 /// Called by the executor when a mutation definition declares
651 /// `invalidates_fact_tables`. For each listed table the version counter is
652 /// incremented so that subsequent aggregation queries miss the cache and
653 /// re-fetch fresh data.
654 ///
655 /// The default implementation is a **no-op**: adapters that are not cache-
656 /// aware (e.g. `PostgresAdapter`, `SqliteAdapter`) simply return `Ok(())`.
657 /// `CachedDatabaseAdapter` overrides this to call `bump_tf_version($1)` for
658 /// every `FactTableVersionStrategy::VersionTable` table and update the
659 /// in-process version cache.
660 ///
661 /// # Arguments
662 ///
663 /// * `tables` - Fact table names declared by the mutation (validated SQL identifiers; originate
664 /// from `MutationDefinition.invalidates_fact_tables`)
665 ///
666 /// # Errors
667 ///
668 /// Returns `FraiseQLError::Database` if the version-bump SQL function fails.
669 async fn bump_fact_table_versions(&self, _tables: &[String]) -> Result<()> {
670 Ok(())
671 }
672
673 /// Invalidate cached query results for the specified views.
674 ///
675 /// Called by the executor after a mutation succeeds, so that stale cache
676 /// entries reading from modified views are evicted. The default
677 /// implementation is a no-op; `CachedDatabaseAdapter` overrides this.
678 ///
679 /// # Returns
680 ///
681 /// The number of cache entries evicted.
682 async fn invalidate_views(&self, _views: &[String]) -> Result<u64> {
683 Ok(0)
684 }
685
686 /// Evict cache entries that contain the given entity UUID.
687 ///
688 /// Called by the executor after a successful UPDATE or DELETE mutation when
689 /// the `mutation_response` includes an `entity_id`. Only cache entries whose
690 /// entity-ID index contains the given UUID are removed; unrelated entries
691 /// remain warm.
692 ///
693 /// The default implementation is a no-op. `CachedDatabaseAdapter` overrides
694 /// this to perform the selective eviction.
695 ///
696 /// # Returns
697 ///
698 /// The number of cache entries evicted.
699 async fn invalidate_by_entity(&self, _entity_type: &str, _entity_id: &str) -> Result<u64> {
700 Ok(0)
701 }
702
703 /// Evict only list (multi-row) cache entries for the given views.
704 ///
705 /// Called by the executor after a successful CREATE mutation. Unlike
706 /// `invalidate_views()`, this preserves single-entity point-lookup entries
707 /// that are unaffected by the newly created entity.
708 ///
709 /// The default implementation delegates to `invalidate_views()` (safe
710 /// fallback for adapters without a `list_index`). `CachedDatabaseAdapter`
711 /// overrides this to use the dedicated `list_index` for precise eviction.
712 ///
713 /// # Returns
714 ///
715 /// The number of cache entries evicted.
716 async fn invalidate_list_queries(&self, views: &[String]) -> Result<u64> {
717 self.invalidate_views(views).await
718 }
719
720 /// Get database capabilities.
721 ///
722 /// Returns information about what features this database supports,
723 /// including collation strategies and limitations.
724 ///
725 /// # Returns
726 ///
727 /// `DatabaseCapabilities` describing supported features.
728 fn capabilities(&self) -> DatabaseCapabilities {
729 DatabaseCapabilities::from_database_type(self.database_type())
730 }
731
732 /// Run the database's `EXPLAIN` on a SQL statement without executing it.
733 ///
734 /// Returns a JSON representation of the query plan. The format is
735 /// database-specific (e.g. PostgreSQL returns JSON, SQLite returns rows).
736 ///
737 /// The default implementation returns `Unsupported`.
738 async fn explain_query(
739 &self,
740 _sql: &str,
741 _params: &[serde_json::Value],
742 ) -> Result<serde_json::Value> {
743 Err(fraiseql_error::FraiseQLError::Unsupported {
744 message: "EXPLAIN not available for this database adapter".to_string(),
745 })
746 }
747
748 /// Run `EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)` against a view with the
749 /// same parameterized WHERE clause that `execute_where_query` would use.
750 ///
751 /// Unlike `explain_query`, this method uses **real bound parameters** and
752 /// **actually executes the query** (ANALYZE mode), so the plan reflects
753 /// PostgreSQL's runtime statistics for the given filter values.
754 ///
755 /// Only PostgreSQL supports this; other adapters return
756 /// `FraiseQLError::Unsupported` by default.
757 ///
758 /// # Arguments
759 ///
760 /// * `view` - View name (e.g., "v_user")
761 /// * `where_clause` - Optional filter (same as `execute_where_query`)
762 /// * `limit` - Optional row limit
763 /// * `offset` - Optional row offset
764 ///
765 /// # Errors
766 ///
767 /// Returns `FraiseQLError::Database` on execution failure.
768 /// Returns `FraiseQLError::Unsupported` for non-PostgreSQL adapters.
769 async fn explain_where_query(
770 &self,
771 _view: &str,
772 _where_clause: Option<&WhereClause>,
773 _limit: Option<u32>,
774 _offset: Option<u32>,
775 ) -> Result<serde_json::Value> {
776 Err(fraiseql_error::FraiseQLError::Unsupported {
777 message: "EXPLAIN ANALYZE is not available for this database adapter. \
778 Only PostgreSQL supports explain_where_query."
779 .to_string(),
780 })
781 }
782
783 /// Returns the mutation strategy used by this adapter.
784 ///
785 /// The default is `FunctionCall` (stored procedures). Adapters that generate
786 /// direct SQL (e.g., SQLite) override this to return `DirectSql`.
787 fn mutation_strategy(&self) -> MutationStrategy {
788 MutationStrategy::FunctionCall
789 }
790
791 /// Execute a direct SQL mutation (INSERT/UPDATE/DELETE) and return the
792 /// mutation response rows as JSON objects.
793 ///
794 /// Only adapters using `MutationStrategy::DirectSql` need to override this.
795 /// The default implementation returns `Unsupported`.
796 ///
797 /// # Errors
798 ///
799 /// Returns `FraiseQLError::Unsupported` by default.
800 /// Returns `FraiseQLError::Database` on SQL execution failure.
801 /// Returns `FraiseQLError::Validation` on invalid mutation parameters.
802 async fn execute_direct_mutation(
803 &self,
804 _ctx: &DirectMutationContext<'_>,
805 ) -> Result<Vec<serde_json::Value>> {
806 Err(FraiseQLError::Unsupported {
807 message: "Direct SQL mutations are not supported by this adapter. \
808 Use execute_function_call for stored-procedure mutations."
809 .to_string(),
810 })
811 }
812}
813
814/// Database capabilities and feature support.
815///
816/// Describes what features a database backend supports, allowing the runtime
817/// to adapt behavior based on database limitations.
818#[derive(Debug, Clone, Copy)]
819pub struct DatabaseCapabilities {
820 /// Database type.
821 pub database_type: DatabaseType,
822
823 /// Supports locale-specific collations.
824 pub supports_locale_collation: bool,
825
826 /// Requires custom collation registration.
827 pub requires_custom_collation: bool,
828
829 /// Recommended collation provider.
830 pub recommended_collation: Option<&'static str>,
831}
832
833impl DatabaseCapabilities {
834 /// Create capabilities from database type.
835 #[must_use]
836 pub const fn from_database_type(db_type: DatabaseType) -> Self {
837 match db_type {
838 DatabaseType::PostgreSQL => Self {
839 database_type: db_type,
840 supports_locale_collation: true,
841 requires_custom_collation: false,
842 recommended_collation: Some("icu"),
843 },
844 DatabaseType::MySQL => Self {
845 database_type: db_type,
846 supports_locale_collation: false,
847 requires_custom_collation: false,
848 recommended_collation: Some("utf8mb4_unicode_ci"),
849 },
850 DatabaseType::SQLite => Self {
851 database_type: db_type,
852 supports_locale_collation: false,
853 requires_custom_collation: true,
854 recommended_collation: Some("NOCASE"),
855 },
856 DatabaseType::SQLServer => Self {
857 database_type: db_type,
858 supports_locale_collation: true,
859 requires_custom_collation: false,
860 recommended_collation: Some("Latin1_General_100_CI_AI_SC_UTF8"),
861 },
862 }
863 }
864
865 /// Get collation strategy description.
866 #[must_use]
867 pub const fn collation_strategy(&self) -> &'static str {
868 match self.database_type {
869 DatabaseType::PostgreSQL => "ICU collations (locale-specific)",
870 DatabaseType::MySQL => "UTF8MB4 collations (general)",
871 DatabaseType::SQLite => "NOCASE (limited)",
872 DatabaseType::SQLServer => "Language-specific collations",
873 }
874 }
875}
876
877/// Strategy used by an adapter for executing mutations.
878///
879/// Adapters that use stored database functions (PostgreSQL, MySQL, SQL Server) use
880/// `FunctionCall`. Adapters that generate INSERT/UPDATE/DELETE SQL directly (SQLite)
881/// use `DirectSql`.
882#[derive(Debug, Clone, Copy, PartialEq, Eq)]
883#[non_exhaustive]
884pub enum MutationStrategy {
885 /// Mutations execute via stored database functions (`SELECT * FROM fn_create_user($1, $2)`).
886 FunctionCall,
887 /// Mutations execute via direct SQL (`INSERT INTO ... RETURNING *`).
888 DirectSql,
889}
890
891/// The kind of direct mutation operation.
892#[derive(Debug, Clone, Copy, PartialEq, Eq)]
893#[non_exhaustive]
894pub enum DirectMutationOp {
895 /// `INSERT INTO ... RETURNING *`
896 Insert,
897 /// `UPDATE ... SET ... WHERE pk = ? RETURNING *`
898 Update,
899 /// `DELETE FROM ... WHERE pk = ? RETURNING *`
900 Delete,
901}
902
903/// Context for a direct SQL mutation (used by `DirectSql` strategy adapters).
904///
905/// All field references are borrowed from the caller to avoid allocation.
906#[derive(Debug)]
907pub struct DirectMutationContext<'a> {
908 /// The mutation operation to perform.
909 pub operation: DirectMutationOp,
910 /// Target table name (e.g., `"users"`).
911 pub table: &'a str,
912 /// Client-supplied column names (in bind order).
913 pub columns: &'a [String],
914 /// All bind values: client values first, then injected values.
915 pub values: &'a [serde_json::Value],
916 /// Server-injected column names (e.g., RLS tenant columns), appended after client columns.
917 pub inject_columns: &'a [String],
918 /// GraphQL return type name (e.g., `"User"`), used in the mutation response envelope.
919 pub return_type: &'a str,
920}
921
922/// A typed cursor value for keyset (relay) pagination.
923///
924/// The cursor type is determined at compile time by `QueryDefinition::relay_cursor_type`
925/// and used at runtime to choose the correct SQL comparison and cursor
926/// encoding/decoding path.
927#[derive(Debug, Clone, PartialEq, Eq)]
928#[non_exhaustive]
929pub enum CursorValue {
930 /// BIGINT primary key cursor (default, backward-compatible).
931 Int64(i64),
932 /// UUID cursor — bound as text and cast to `uuid` in SQL.
933 Uuid(String),
934}
935
936/// Database adapter supertrait for adapters that implement Relay cursor pagination.
937///
938/// Only adapters that genuinely support keyset pagination need to implement this trait.
939/// Non-implementing adapters carry no relay code at all — no stubs, no flags.
940///
941/// # Implementors
942///
943/// - `PostgresAdapter` — full keyset pagination
944/// - `MySqlAdapter` — keyset pagination with `?` params
945/// - `CachedDatabaseAdapter<A>` — delegates to inner `A`
946///
947/// # Usage
948///
949/// Construct an `Executor` with `Executor::new_with_relay` to enable relay
950/// query execution. The bound `A: RelayDatabaseAdapter` is enforced at that call site.
951pub trait RelayDatabaseAdapter: DatabaseAdapter {
952 /// Execute keyset (cursor-based) pagination against a JSONB view.
953 ///
954 /// # Arguments
955 ///
956 /// * `view` — SQL view name (will be quoted before use)
957 /// * `cursor_column` — column used as the pagination key (e.g. `pk_user`, `id`)
958 /// * `after` — forward cursor: return rows where `cursor_column > after`
959 /// * `before` — backward cursor: return rows where `cursor_column < before`
960 /// * `limit` — row fetch count (pass `page_size + 1` to detect `hasNextPage`)
961 /// * `forward` — `true` → ASC order; `false` → DESC (re-sorted ASC via subquery)
962 /// * `where_clause` — optional user-supplied filter applied after the cursor condition
963 /// * `order_by` — optional custom sort; cursor column appended as tiebreaker
964 /// * `include_total_count` — when `true`, compute the matching row count before LIMIT
965 ///
966 /// # Errors
967 ///
968 /// Returns `FraiseQLError::Database` on SQL execution failure.
969 fn execute_relay_page<'a>(
970 &'a self,
971 view: &'a str,
972 cursor_column: &'a str,
973 after: Option<CursorValue>,
974 before: Option<CursorValue>,
975 limit: u32,
976 forward: bool,
977 where_clause: Option<&'a WhereClause>,
978 order_by: Option<&'a [OrderByClause]>,
979 include_total_count: bool,
980 ) -> impl Future<Output = Result<RelayPageResult>> + Send + 'a;
981}
982
983/// Marker trait for database adapters that support write operations via stored functions.
984///
985/// Adapters that implement this trait signal that they can execute GraphQL mutations by
986/// calling stored database functions (e.g. `fn_create_user`, `fn_update_order`).
987///
988/// Marker trait for database adapters that support stored-procedure mutations.
989///
990/// # Role: documentation, generic bound, and compile-time enforcement
991///
992/// This trait serves three purposes:
993/// 1. **Documentation**: it makes write-capable adapters self-describing at the type level.
994/// 2. **Generic bounds**: code that only accepts write-capable adapters can constrain on `A:
995/// SupportsMutations` (e.g., `CachedDatabaseAdapter<A: SupportsMutations>`).
996/// 3. **Compile-time enforcement**: `Executor<A>::execute_mutation()` is only available when `A:
997/// SupportsMutations`. Attempting to call it with `SqliteAdapter` produces a compiler error
998/// (`error[E0277]: SqliteAdapter does not implement SupportsMutations`).
999///
1000/// The `execute()` method (which accepts raw GraphQL strings) still performs a runtime
1001/// `supports_mutations()` check because it cannot know the operation type at compile time.
1002/// For direct mutation dispatch, prefer `execute_mutation()` to get compile-time safety.
1003///
1004/// # Which adapters implement this?
1005///
1006/// | Adapter | Implements |
1007/// |---------|-----------|
1008/// | `PostgresAdapter` | ✅ Yes |
1009/// | `MySqlAdapter` | ✅ Yes |
1010/// | `SqlServerAdapter` | ✅ Yes |
1011/// | `SqliteAdapter` | ❌ No — SQLite does not support stored-function mutations |
1012/// | `FraiseWireAdapter` | ❌ No — read-only wire protocol |
1013/// | `CachedDatabaseAdapter<A>` | ✅ When `A: SupportsMutations` |
1014pub trait SupportsMutations: DatabaseAdapter {}
1015
1016/// Type alias for boxed dynamic database adapters.
1017///
1018/// Used to store database adapters without generic type parameters in collections
1019/// or struct fields. The adapter type is determined at runtime.
1020///
1021/// # Example
1022///
1023/// ```ignore
1024/// let adapter: BoxDatabaseAdapter = Box::new(postgres_adapter);
1025/// ```
1026pub type BoxDatabaseAdapter = Box<dyn DatabaseAdapter>;
1027
1028/// Type alias for arc-wrapped dynamic database adapters.
1029///
1030/// Used for thread-safe, reference-counted storage of adapters in shared state.
1031///
1032/// # Example
1033///
1034/// ```ignore
1035/// let adapter: ArcDatabaseAdapter = Arc::new(postgres_adapter);
1036/// ```
1037pub type ArcDatabaseAdapter = std::sync::Arc<dyn DatabaseAdapter>;
1038
1039#[cfg(test)]
1040mod tests {
1041 #[allow(clippy::unwrap_used)] // Reason: test code
1042 #[test]
1043 fn database_adapter_is_send_sync() {
1044 // Static assertion: `dyn DatabaseAdapter` must be `Send + Sync`.
1045 // This test exists to catch accidental removal of `Send + Sync` bounds.
1046 // It only needs to compile — no runtime assertion required.
1047 fn assert_send_sync<T: Send + Sync + ?Sized>() {}
1048 assert_send_sync::<dyn super::DatabaseAdapter>();
1049 }
1050}