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/// The framework-owned change-log row the mutation executor writes in-txn.
28///
29/// Carries only the fields the adapter cannot derive from the
30/// `app.mutation_response` row it already holds: the DML verb and a NOT-NULL
31/// `object_type` fallback. The changed-entity identity + payload (`object_id`,
32/// `object_data`, `updated_fields`, `cascade`) are read from the function's own
33/// returned row inside the same transaction (see
34/// [`DatabaseAdapter::execute_function_call_with_changelog`]).
35///
36/// This is the Change Spine transactional-outbox contract. Beyond the
37/// `object_type`/`modification_type` + changed-entity columns, it stamps the
38/// envelope: `tenant_id` (carried here, from `SecurityContext`),
39/// `trace_id` (the W3C trace id of the originating request), `schema_version`
40/// (the compiled schema's content hash — a per-deployment constant),
41/// `trace_context` (the full W3C trace context as JSON), `actor_type` /
42/// `acting_for` (the request's actor classification and, for a delegated agent,
43/// the underlying human — #390), `commit_time` (`clock_timestamp()` at INSERT),
44/// and `seq` (the table's `SEQUENCE` default).
45#[derive(Debug, Clone, Copy)]
46pub struct ChangeLogWrite<'a> {
47 /// NOT-NULL fallback for `object_type` when the row's `entity_type` is NULL.
48 /// Sourced from `MutationDefinition.return_type` (always present).
49 pub object_type: &'a str,
50 /// The DML verb written to `modification_type` (e.g. `"INSERT"`,
51 /// `"UPDATE"`, `"DELETE"`, `"CUSTOM"`), from `MutationOperation`.
52 pub modification_type: &'a str,
53 /// The tenant partition stamp written to the `tenant_id UUID` column — the
54 /// Trinity public-facing identifier, read from `SecurityContext.tenant_id`
55 /// at write time and **never** reconstructed from connection / RLS state
56 /// (RLS is PG-only; out-of-session spine consumers bypass it, so the row
57 /// must carry tenant identity explicitly). `None` (→ SQL NULL) for an
58 /// unauthenticated request, a request with no tenant, or a tenant
59 /// identifier that is not a UUID.
60 pub tenant_id: Option<uuid::Uuid>,
61 /// The W3C trace id of the originating request, written to the `trace_id`
62 /// column so an outbox row links back to its distributed trace (the #392
63 /// perf tooling surfaces it as the investigation handle). Read from the
64 /// request's `traceparent` header at write time; `None` (→ SQL NULL) for a
65 /// request without a trace context — e.g. an unauthenticated mutation, which
66 /// carries no `SecurityContext` to stamp.
67 pub trace_id: Option<&'a str>,
68 /// The compiled schema's version written to the `schema_version` column so an
69 /// outbox row records which deployment produced it — the replay /
70 /// zero-downtime correctness handle for #378 (reject a row replayed under a
71 /// different schema). A per-deployment constant derived from the compiled
72 /// schema (`CompiledSchema::content_hash()`), **not** from the request, so it
73 /// changes on any schema change. `None` (→ SQL NULL) for producers with no
74 /// compiled schema in scope — cooperative external producers (ETL) and the
75 /// non-PostgreSQL no-op path.
76 pub schema_version: Option<&'a str>,
77 /// The originating request's **full W3C trace context** as a JSON object
78 /// (`{version, trace_id, parent_id, trace_flags, tracestate?}`), written to the
79 /// `trace_context` JSONB column so a row carries enough to re-propagate /
80 /// reconstruct the distributed trace — not just the scalar `trace_id`. Carried
81 /// here as pre-serialized JSON **text** (the adapter binds it to the JSONB
82 /// column). Built from the request's `traceparent` / `tracestate` headers at
83 /// write time; `None` (→ SQL NULL) for a request without a valid trace context,
84 /// consistent with `trace_id`.
85 pub trace_context: Option<&'a str>,
86 /// The request's actor classification written to the `actor_type` column (the
87 /// `snake_case` `ActorType` token: `"human_user"`, `"service_account"`,
88 /// `"ai_agent"`, `"system_job"`), from `SecurityContext.actor_type()` at write
89 /// time (#390). `None` (→ SQL NULL) for a request with no `SecurityContext` to
90 /// stamp (an unauthenticated mutation), or a cooperative external producer.
91 pub actor_type: Option<&'a str>,
92 /// For a delegated agent request, the **underlying human** the agent acts for
93 /// — the public-facing UUID, written to the `acting_for UUID` column from
94 /// `SecurityContext.acting_for()` (#390). Mirrors `tenant_id`'s UUID shape so
95 /// it is stamped without a DB lookup. `None` (→ SQL NULL) for a non-delegated
96 /// request, an unauthenticated mutation, or a subject that is not UUID-shaped.
97 pub acting_for: Option<uuid::Uuid>,
98 /// Whether this outbox write also records the changed entity's **pre-image**
99 /// (before-state) into the `object_data_before JSONB` column, sourced from the
100 /// function's own `entity_before` (the after-image comes from `entity`). Set
101 /// from `MutationDefinition.changelog_pre_image`; `false` (the default) leaves
102 /// `object_data_before` out of the INSERT entirely (NULL), byte-for-byte
103 /// today's behavior. The changed-entity payload itself is read from the
104 /// returned row inside the outbox CTE, so this flag only selects which SQL
105 /// form the adapter emits.
106 pub pre_image: bool,
107}
108
109impl<'a> ChangeLogWrite<'a> {
110 /// Build a change-log write descriptor with no envelope stamps (`tenant_id`,
111 /// `trace_id`, `schema_version`, `trace_context`, `actor_type` and
112 /// `acting_for` NULL). Chain [`with_tenant_id`](Self::with_tenant_id) /
113 /// [`with_trace_id`](Self::with_trace_id) /
114 /// [`with_schema_version`](Self::with_schema_version) /
115 /// [`with_trace_context`](Self::with_trace_context) /
116 /// [`with_actor_type`](Self::with_actor_type) /
117 /// [`with_acting_for`](Self::with_acting_for) to stamp them.
118 #[must_use]
119 pub const fn new(object_type: &'a str, modification_type: &'a str) -> Self {
120 Self {
121 object_type,
122 modification_type,
123 tenant_id: None,
124 trace_id: None,
125 schema_version: None,
126 trace_context: None,
127 actor_type: None,
128 acting_for: None,
129 pre_image: false,
130 }
131 }
132
133 /// Stamp the tenant partition id (the Trinity public-facing UUID) onto the
134 /// outbox row. `None` leaves `tenant_id` NULL — for system / unauthenticated
135 /// rows, or a tenant identifier that is not UUID-shaped.
136 #[must_use]
137 pub const fn with_tenant_id(mut self, tenant_id: Option<uuid::Uuid>) -> Self {
138 self.tenant_id = tenant_id;
139 self
140 }
141
142 /// Stamp the originating request's W3C trace id onto the outbox row. `None`
143 /// leaves `trace_id` NULL — for a request with no trace context.
144 #[must_use]
145 pub const fn with_trace_id(mut self, trace_id: Option<&'a str>) -> Self {
146 self.trace_id = trace_id;
147 self
148 }
149
150 /// Stamp the compiled schema's version (its content hash) onto the outbox
151 /// row. `None` leaves `schema_version` NULL — for producers with no compiled
152 /// schema in scope (cooperative external producers, the non-PostgreSQL no-op
153 /// path).
154 #[must_use]
155 pub const fn with_schema_version(mut self, schema_version: Option<&'a str>) -> Self {
156 self.schema_version = schema_version;
157 self
158 }
159
160 /// Stamp the originating request's full W3C trace context (pre-serialized JSON
161 /// text) onto the outbox row's `trace_context` JSONB column. `None` leaves it
162 /// NULL — for a request with no valid trace context, or a non-PostgreSQL
163 /// no-op / cooperative producer.
164 #[must_use]
165 pub const fn with_trace_context(mut self, trace_context: Option<&'a str>) -> Self {
166 self.trace_context = trace_context;
167 self
168 }
169
170 /// Stamp the request's actor classification (the `snake_case` `ActorType`
171 /// token) onto the outbox row's `actor_type` column (#390). `None` leaves it
172 /// NULL — for an unauthenticated mutation or a cooperative producer.
173 #[must_use]
174 pub const fn with_actor_type(mut self, actor_type: Option<&'a str>) -> Self {
175 self.actor_type = actor_type;
176 self
177 }
178
179 /// Stamp the delegated user's UUID (the human a delegated agent acts for) onto
180 /// the outbox row's `acting_for` column (#390). `None` leaves it NULL — for a
181 /// non-delegated request, an unauthenticated mutation, or a non-UUID subject.
182 #[must_use]
183 pub const fn with_acting_for(mut self, acting_for: Option<uuid::Uuid>) -> Self {
184 self.acting_for = acting_for;
185 self
186 }
187
188 /// Opt this outbox write into recording the changed entity's pre-image into
189 /// the `object_data_before` column (from the function's `entity_before`). When
190 /// `false` (the default), `object_data_before` is omitted from the INSERT
191 /// entirely, byte-for-byte today's behavior. Set from
192 /// `MutationDefinition.changelog_pre_image`.
193 #[must_use]
194 pub const fn with_pre_image(mut self, pre_image: bool) -> Self {
195 self.pre_image = pre_image;
196 self
197 }
198}
199
200/// Database adapter for executing queries against views.
201///
202/// This trait abstracts over different database backends (PostgreSQL, MySQL, SQLite, SQL Server).
203/// All implementations must support:
204/// - Executing parameterized WHERE queries against views
205/// - Returning JSONB data from the `data` column
206/// - Connection pooling and health checks
207/// - Row-level security (RLS) WHERE clauses
208///
209/// # Architecture
210///
211/// The adapter is the runtime interface to the database. It receives:
212/// - View/table name (e.g., "v_user", "tf_sales")
213/// - Parameterized WHERE clauses (AST form, not strings)
214/// - Projection hints (for performance optimization)
215/// - Pagination parameters (LIMIT/OFFSET)
216///
217/// And returns:
218/// - JSONB rows from the `data` column (most operations)
219/// - Arbitrary rows as HashMap (for aggregation queries)
220/// - Mutation results from stored procedures
221///
222/// # Implementing a New Adapter
223///
224/// To add support for a new database (e.g., Oracle, Snowflake):
225///
226/// 1. **Create a new module** in `src/db/your_database/`
227/// 2. **Implement the trait**:
228///
229/// ```rust,ignore
230/// pub struct YourDatabaseAdapter { /* fields */ }
231///
232/// #[async_trait]
233/// impl DatabaseAdapter for YourDatabaseAdapter {
234/// async fn execute_where_query(&self, ...) -> Result<Vec<JsonbValue>> {
235/// // 1. Build parameterized SQL from WhereClause AST
236/// // 2. Execute with bound parameters (NO string concatenation)
237/// // 3. Return JSONB from data column
238/// }
239/// // Implement other required methods...
240/// }
241/// ```
242/// 3. **Add feature flag** to `Cargo.toml` (e.g., `feature = "your-database"`)
243/// 4. **Copy structure from PostgreSQL adapter** — see `src/db/postgres/adapter.rs`
244/// 5. **Add tests** in `tests/integration/your_database_test.rs`
245///
246/// # Security Requirements
247///
248/// All implementations MUST:
249/// - **Never concatenate user input into SQL strings**
250/// - **Always use parameterized queries** with bind parameters
251/// - **Validate parameter types** before binding
252/// - **Preserve RLS WHERE clauses** (never filter them out)
253/// - **Return errors, not silently fail** (e.g., connection loss)
254///
255/// # Connection Management
256///
257/// - Use a connection pool (recommended: 20 connections default)
258/// - Implement `health_check()` for ping-based monitoring
259/// - Provide `pool_metrics()` for observability
260/// - Handle stale connections gracefully
261///
262/// # Performance Characteristics
263///
264/// Expected throughput when properly implemented:
265/// - **Simple queries** (single table, no WHERE): 250+ Kelem/s
266/// - **Complex queries** (JOINs, multiple conditions): 50+ Kelem/s
267/// - **Mutations** (stored procedures): 1-10 RPS (depends on procedure)
268/// - **Relay pagination** (keyset cursors): 15-30ms latency
269///
270/// # Example: PostgreSQL Implementation
271///
272/// ```rust,ignore
273/// use sqlx::postgres::PgPool;
274/// use async_trait::async_trait;
275///
276/// pub struct PostgresAdapter {
277/// pool: PgPool,
278/// }
279///
280/// #[async_trait]
281/// impl DatabaseAdapter for PostgresAdapter {
282/// async fn execute_where_query(
283/// &self,
284/// view: &str,
285/// where_clause: Option<&WhereClause>,
286/// limit: Option<u32>,
287/// offset: Option<u32>,
288/// ) -> Result<Vec<JsonbValue>> {
289/// // 1. Build SQL: SELECT data FROM {view} WHERE {where_clause} LIMIT {limit}
290/// let mut sql = format!(r#"SELECT data FROM "{}""#, view);
291///
292/// // 2. Add WHERE clause (converts AST to parameterized SQL)
293/// let params = if let Some(where_clause) = where_clause {
294/// sql.push_str(" WHERE ");
295/// let (where_sql, params) = build_where_sql(where_clause)?;
296/// sql.push_str(&where_sql);
297/// params
298/// } else {
299/// vec![]
300/// };
301///
302/// // 3. Add LIMIT and OFFSET
303/// if let Some(limit) = limit {
304/// sql.push_str(" LIMIT ");
305/// sql.push_str(&limit.to_string());
306/// }
307/// if let Some(offset) = offset {
308/// sql.push_str(" OFFSET ");
309/// sql.push_str(&offset.to_string());
310/// }
311///
312/// // 4. Execute with bound parameters (NO string interpolation)
313/// let rows: Vec<(serde_json::Value,)> = sqlx::query_as(&sql)
314/// .bind(¶ms[0])
315/// .bind(¶ms[1])
316/// // ... bind all parameters
317/// .fetch_all(&self.pool)
318/// .await?;
319///
320/// // 5. Extract JSONB and return
321/// Ok(rows.into_iter().map(|(data,)| data).collect())
322/// }
323///
324/// // Implement other required methods...
325/// }
326/// ```
327///
328/// # Example: Basic Usage
329///
330/// ```rust,no_run
331/// use fraiseql_db::{DatabaseAdapter, WhereClause, WhereOperator};
332/// use serde_json::json;
333///
334/// # async fn example(adapter: impl DatabaseAdapter) -> Result<(), Box<dyn std::error::Error>> {
335/// // Build WHERE clause (AST, not string)
336/// let where_clause = WhereClause::Field {
337/// path: vec!["email".to_string()],
338/// operator: WhereOperator::Icontains,
339/// value: json!("example.com"),
340/// };
341///
342/// // Execute query with parameters
343/// let results = adapter
344/// .execute_where_query("v_user", Some(&where_clause), Some(10), None, None)
345/// .await?;
346///
347/// println!("Found {} users matching filter", results.len());
348/// # Ok(())
349/// # }
350/// ```
351///
352/// # See Also
353///
354/// - `WhereClause` — AST for parameterized WHERE clauses
355/// - `RelayDatabaseAdapter` — Optional trait for keyset pagination
356/// - `DatabaseCapabilities` — Feature detection for the adapter
357/// - [Performance Guide](https://docs.fraiseql.rs/performance/database-adapters.md)
358// POLICY: `#[async_trait]` placement for `DatabaseAdapter`
359//
360// `DatabaseAdapter` is used both generically (`Server<A: DatabaseAdapter>` in axum
361// handlers, zero overhead via static dispatch) and dynamically (`Arc<dyn
362// DatabaseAdapter + Send + Sync>` in federation, heap-boxed future per call).
363//
364// `#[async_trait]` is required on:
365// - The trait definition (generates `Pin<Box<dyn Future + Send>>` return types)
366// - Every `impl DatabaseAdapter for ConcreteType` block (generates the boxing)
367// NOT required on callers (they see `Pin<Box<dyn Future + Send>>` from macro output).
368//
369// Why not native `async fn in trait` (Rust 1.75+)?
370// Native dyn async trait does NOT propagate `+ Send` on generated futures. Tokio
371// requires futures spawned with `tokio::spawn` to be `Send`. Until Return Type
372// Notation (RFC 3425, tracking: github.com/rust-lang/rust/issues/109417) stabilises,
373// `async_trait` is the only ergonomic path to `dyn DatabaseAdapter + Send + Sync`.
374// Re-evaluate when Rust 1.90+ ships or when RTN is stabilised.
375//
376// MIGRATION TRACKING: async-trait → native async fn in trait
377//
378// Current status: BLOCKED on RFC 3425 (Return Type Notation)
379// See: https://github.com/rust-lang/rfcs/pull/3425
380// https://github.com/rust-lang/rust/issues/109417
381//
382// Migration is safe when ALL of the following are true:
383// 1. RTN with `+ Send` bounds is stable on rustc (e.g. `fn foo() -> impl Future + Send`)
384// 2. FraiseQL MSRV is updated to that stabilising version
385// 3. tokio::spawn() works with native dyn async trait objects (futures must be Send)
386//
387// Scope when criteria are met: 68 files (grep -rn "#\[async_trait\]" crates/)
388// Effort: Medium (mostly mechanical — remove macro from impls, adjust trait defs)
389// dynosaur was evaluated and rejected: does not propagate + Send (incompatible with Tokio)
390#[async_trait]
391pub trait DatabaseAdapter: Send + Sync {
392 /// Execute a WHERE query against a view and return JSONB rows.
393 ///
394 /// # Arguments
395 ///
396 /// * `view` - View name (e.g., "v_user", "v_post")
397 /// * `where_clause` - Optional WHERE clause AST
398 /// * `limit` - Optional row limit (for pagination)
399 /// * `offset` - Optional row offset (for pagination)
400 /// * `security_context` - Optional security context for RLS and caching decisions
401 ///
402 /// # Returns
403 ///
404 /// Vec of JSONB values from the `data` column.
405 ///
406 /// # Errors
407 ///
408 /// Returns `FraiseQLError::Database` on query execution failure.
409 /// Returns `FraiseQLError::ConnectionPool` if connection pool is exhausted.
410 ///
411 /// # Example
412 ///
413 /// ```rust,no_run
414 /// # use fraiseql_db::DatabaseAdapter;
415 /// # async fn example(adapter: impl DatabaseAdapter) -> Result<(), Box<dyn std::error::Error>> {
416 /// // Simple query without WHERE clause
417 /// let all_users = adapter
418 /// .execute_where_query("v_user", None, Some(10), Some(0), None)
419 /// .await?;
420 /// # Ok(())
421 /// # }
422 /// ```
423 async fn execute_where_query(
424 &self,
425 view: &str,
426 where_clause: Option<&WhereClause>,
427 limit: Option<u32>,
428 offset: Option<u32>,
429 order_by: Option<&[OrderByClause]>,
430 ) -> Result<Vec<JsonbValue>>;
431
432 /// Execute a WHERE query with SQL field projection optimization.
433 ///
434 /// Projects only the requested fields at the database level, reducing network payload
435 /// and JSON deserialization overhead by **40-55%** based on production measurements.
436 ///
437 /// This is the primary query execution method for optimized GraphQL queries.
438 /// It automatically selects only the fields requested in the GraphQL query, avoiding
439 /// unnecessary network transfer and deserialization of unused fields.
440 ///
441 /// # Automatic Projection
442 ///
443 /// In most cases, you don't call this directly. The `Executor` automatically:
444 /// 1. Determines which fields the GraphQL query requests
445 /// 2. Generates a `SqlProjectionHint` using database-specific SQL
446 /// 3. Calls this method with the projection hint
447 ///
448 /// # Arguments
449 ///
450 /// * `view` - View name (e.g., "v_user", "v_post")
451 /// * `projection` - Optional SQL projection hint with field list
452 /// - `Some(hint)`: Use projection to select only requested fields
453 /// - `None`: Falls back to standard query (full JSONB column)
454 /// * `where_clause` - Optional WHERE clause AST for filtering
455 /// * `limit` - Optional row limit (for pagination)
456 ///
457 /// # Returns
458 ///
459 /// Vec of JSONB values, either:
460 /// - Full objects (when projection is None)
461 /// - Projected objects with only requested fields (when projection is Some)
462 ///
463 /// # Errors
464 ///
465 /// Returns `FraiseQLError::Database` on query execution failure, including:
466 /// - Connection pool exhaustion
467 /// - SQL execution errors
468 /// - Type mismatches
469 ///
470 /// # Performance Characteristics
471 ///
472 /// When projection is provided (recommended):
473 /// - **Latency**: 40-55% reduction vs full object fetch
474 /// - **Network**: 40-55% smaller payload (proportional to unused fields)
475 /// - **Throughput**: Maintains 250+ Kelem/s (elements per second)
476 /// - **Memory**: Proportional to projected fields only
477 ///
478 /// Improvement scales with:
479 /// - Percentage of unused fields (more unused = more improvement)
480 /// - Size of result set (larger sets benefit more)
481 /// - Network latency (network-bound queries benefit most)
482 ///
483 /// When projection is None:
484 /// - Behavior identical to `execute_where_query()`
485 /// - Returns full JSONB column
486 /// - Used for compatibility/debugging
487 ///
488 /// # Database Support
489 ///
490 /// | Database | Status | Implementation |
491 /// |----------|--------|-----------------|
492 /// | PostgreSQL | ✅ Optimized | `jsonb_build_object()` |
493 /// | MySQL | ⏳ Fallback | Server-side filtering (planned) |
494 /// | SQLite | ⏳ Fallback | Server-side filtering (planned) |
495 /// | SQL Server | ⏳ Fallback | Server-side filtering (planned) |
496 ///
497 /// # Example: Direct Usage (Advanced)
498 ///
499 /// ```no_run
500 /// // Requires: running PostgreSQL database and a DatabaseAdapter implementation.
501 /// use fraiseql_db::types::SqlProjectionHint;
502 /// use fraiseql_db::traits::DatabaseAdapter;
503 /// use fraiseql_db::DatabaseType;
504 ///
505 /// # async fn example(adapter: &impl DatabaseAdapter) -> Result<(), Box<dyn std::error::Error>> {
506 /// let projection = SqlProjectionHint::new(
507 /// DatabaseType::PostgreSQL,
508 /// "jsonb_build_object(\
509 /// 'id', data->>'id', \
510 /// 'name', data->>'name', \
511 /// 'email', data->>'email'\
512 /// )".to_string(),
513 /// 75,
514 /// );
515 ///
516 /// let results = adapter
517 /// .execute_with_projection("v_user", Some(&projection), None, Some(100), None, None)
518 /// .await?;
519 ///
520 /// // results only contain id, name, email fields
521 /// // 75% smaller than fetching all fields
522 /// # Ok(())
523 /// # }
524 /// ```
525 ///
526 /// # Example: Fallback (No Projection)
527 ///
528 /// ```no_run
529 /// // Requires: running PostgreSQL database and a DatabaseAdapter implementation.
530 /// # use fraiseql_db::traits::DatabaseAdapter;
531 /// # async fn example(adapter: &impl DatabaseAdapter) -> Result<(), Box<dyn std::error::Error>> {
532 /// // For debugging or when projection not available
533 /// let results = adapter
534 /// .execute_with_projection("v_user", None, None, Some(100), None, None)
535 /// .await?;
536 ///
537 /// // Equivalent to execute_where_query() - returns full objects
538 /// # Ok(())
539 /// # }
540 /// ```
541 ///
542 /// # See Also
543 ///
544 /// - `execute_where_query()` - Standard query without projection
545 /// - `SqlProjectionHint` - Structure defining field projection
546 /// - [Projection Optimization Guide](https://docs.fraiseql.rs/performance/projection-optimization.md)
547 async fn execute_with_projection(
548 &self,
549 view: &str,
550 projection: Option<&SqlProjectionHint>,
551 where_clause: Option<&WhereClause>,
552 limit: Option<u32>,
553 offset: Option<u32>,
554 order_by: Option<&[OrderByClause]>,
555 ) -> Result<Vec<JsonbValue>>;
556
557 /// Like `execute_where_query` but returns the result wrapped in an `Arc`.
558 ///
559 /// The default implementation wraps the result of `execute_where_query` in a
560 /// fresh `Arc`. `CachedDatabaseAdapter` overrides this to return the cached `Arc`
561 /// directly — eliminating the full `Vec<JsonbValue>` clone that the non-`Arc`
562 /// path requires on every cache hit.
563 ///
564 /// Callers on the hot query path should prefer this variant and borrow from the
565 /// `Arc` via `&**arc` rather than taking ownership.
566 ///
567 /// # Errors
568 ///
569 /// Same errors as `execute_where_query`.
570 async fn execute_where_query_arc(
571 &self,
572 view: &str,
573 where_clause: Option<&WhereClause>,
574 limit: Option<u32>,
575 offset: Option<u32>,
576 order_by: Option<&[OrderByClause]>,
577 ) -> Result<Arc<Vec<JsonbValue>>> {
578 self.execute_where_query(view, where_clause, limit, offset, order_by)
579 .await
580 .map(Arc::new)
581 }
582
583 /// Like `execute_with_projection` but returns the result wrapped in an `Arc`.
584 ///
585 /// The default implementation wraps the result of `execute_with_projection` in a
586 /// fresh `Arc`. `CachedDatabaseAdapter` overrides this to return the cached `Arc`
587 /// directly — eliminating the full `Vec<JsonbValue>` clone that the non-`Arc`
588 /// path requires on every cache hit.
589 ///
590 /// Parameters are passed in a `ProjectionRequest` struct (F043) so adapters
591 /// and callers cannot misorder them.
592 ///
593 /// # Errors
594 ///
595 /// Same errors as `execute_with_projection`.
596 async fn execute_with_projection_arc(
597 &self,
598 request: &ProjectionRequest<'_>,
599 ) -> Result<Arc<Vec<JsonbValue>>> {
600 self.execute_with_projection(
601 request.view,
602 request.projection,
603 request.where_clause,
604 request.limit,
605 request.offset,
606 request.order_by,
607 )
608 .await
609 .map(Arc::new)
610 }
611
612 /// Get database type (for logging/metrics).
613 ///
614 /// Used to identify which database backend is in use.
615 fn database_type(&self) -> DatabaseType;
616
617 /// Health check - verify database connectivity.
618 ///
619 /// Executes a simple query (e.g., `SELECT 1`) to verify the database is reachable.
620 ///
621 /// # Errors
622 ///
623 /// Returns `FraiseQLError::Database` if health check fails.
624 async fn health_check(&self) -> Result<()>;
625
626 /// Get connection pool metrics.
627 ///
628 /// Returns current statistics about the connection pool:
629 /// - Total connections
630 /// - Idle connections
631 /// - Active connections
632 /// - Waiting requests
633 fn pool_metrics(&self) -> PoolMetrics;
634
635 /// Execute raw SQL query and return rows as JSON objects.
636 ///
637 /// Used for aggregation queries where we need full row data, not just JSONB column.
638 ///
639 /// # Security Warning
640 ///
641 /// This method executes arbitrary SQL. **NEVER** pass untrusted input directly to this method.
642 /// Always:
643 /// - Use parameterized queries with bound parameters
644 /// - Validate and sanitize SQL templates before execution
645 /// - Only execute SQL generated by the FraiseQL compiler
646 /// - Log SQL execution for audit trails
647 ///
648 /// # Arguments
649 ///
650 /// * `sql` - Raw SQL query to execute (must be safe/trusted)
651 ///
652 /// # Returns
653 ///
654 /// Vec of rows, where each row is a HashMap of column name to JSON value.
655 ///
656 /// # Errors
657 ///
658 /// Returns `FraiseQLError::Database` on query execution failure.
659 ///
660 /// # Example
661 ///
662 /// ```rust,no_run
663 /// # use fraiseql_db::DatabaseAdapter;
664 /// # async fn example(adapter: impl DatabaseAdapter) -> Result<(), Box<dyn std::error::Error>> {
665 /// // Safe: SQL generated by FraiseQL compiler
666 /// let sql = "SELECT category, SUM(revenue) as total FROM tf_sales GROUP BY category";
667 /// let rows = adapter.execute_raw_query(sql).await?;
668 /// for row in rows {
669 /// println!("Category: {}, Total: {}", row["category"], row["total"]);
670 /// }
671 /// # Ok(())
672 /// # }
673 /// ```
674 async fn execute_raw_query(
675 &self,
676 sql: &str,
677 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>>;
678
679 /// Execute a row-shaped query against a view, returning typed column values.
680 ///
681 /// Used by the gRPC transport for protobuf encoding of query results.
682 /// The default implementation delegates to `execute_raw_query` and converts
683 /// JSON results to `ColumnValue` vectors.
684 ///
685 /// # Errors
686 ///
687 /// Returns `FraiseQLError::Database` if the adapter returns an error.
688 async fn execute_row_query(
689 &self,
690 view_name: &str,
691 columns: &[crate::types::ColumnSpec],
692 where_sql: Option<&str>,
693 order_by: Option<&str>,
694 limit: Option<u32>,
695 offset: Option<u32>,
696 ) -> Result<Vec<Vec<crate::types::ColumnValue>>> {
697 use crate::types::ColumnValue;
698
699 let mut sql = format!("SELECT * FROM \"{view_name}\"");
700 if let Some(w) = where_sql {
701 sql.push_str(" WHERE ");
702 sql.push_str(w);
703 }
704 if let Some(ob) = order_by {
705 sql.push_str(" ORDER BY ");
706 sql.push_str(ob);
707 }
708 if let Some(l) = limit {
709 use std::fmt::Write;
710 let _ = write!(sql, " LIMIT {l}");
711 }
712 if let Some(o) = offset {
713 use std::fmt::Write;
714 let _ = write!(sql, " OFFSET {o}");
715 }
716
717 let results = self.execute_raw_query(&sql).await?;
718
719 Ok(results
720 .iter()
721 .map(|row| {
722 columns
723 .iter()
724 .map(|col| {
725 row.get(&col.name).map_or(ColumnValue::Null, |v| match v {
726 serde_json::Value::Null => ColumnValue::Null,
727 serde_json::Value::Bool(b) => ColumnValue::Boolean(*b),
728 serde_json::Value::Number(n) => {
729 if let Some(i) = n.as_i64() {
730 ColumnValue::Int64(i)
731 } else if let Some(f) = n.as_f64() {
732 ColumnValue::Float64(f)
733 } else {
734 ColumnValue::Text(n.to_string())
735 }
736 },
737 serde_json::Value::String(s) => ColumnValue::Text(s.clone()),
738 other => ColumnValue::Json(other.to_string()),
739 })
740 })
741 .collect()
742 })
743 .collect())
744 }
745
746 /// Execute a parameterized aggregate SQL query (GROUP BY / HAVING / window).
747 ///
748 /// `sql` contains `$N` (PostgreSQL), `?` (MySQL / SQLite), or `@P1` (SQL Server)
749 /// placeholders for string and array values; numeric and NULL values may be inlined.
750 /// `params` are the corresponding values in placeholder order.
751 ///
752 /// Unlike `execute_raw_query`, this method accepts bind parameters so that
753 /// user-supplied filter values never appear as string literals in the SQL text,
754 /// eliminating the injection risk that `escape_sql_string` mitigated previously.
755 ///
756 /// # Arguments
757 ///
758 /// * `sql` - SQL with placeholders generated by
759 /// `AggregationSqlGenerator::generate_parameterized`
760 /// * `params` - Bind parameters in placeholder order
761 ///
762 /// # Returns
763 ///
764 /// Vec of rows, where each row is a `HashMap` of column name to JSON value.
765 ///
766 /// # Errors
767 ///
768 /// Returns `FraiseQLError::Database` on execution failure.
769 /// Returns `FraiseQLError::Database` on adapters that do not support raw SQL
770 /// (e.g., `FraiseWireAdapter`).
771 async fn execute_parameterized_aggregate(
772 &self,
773 sql: &str,
774 params: &[serde_json::Value],
775 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>>;
776
777 /// Connection-affine variant of
778 /// [`execute_parameterized_aggregate`](Self::execute_parameterized_aggregate).
779 ///
780 /// Applies `session_vars` transaction-locally on the same connection that
781 /// runs the aggregate, so aggregate views backed by `current_setting()` RLS
782 /// observe the configured values (fixes #329 for the aggregate path). See
783 /// [`execute_function_call_with_session`](Self::execute_function_call_with_session)
784 /// for the non-PostgreSQL default behaviour.
785 ///
786 /// # Errors
787 ///
788 /// Same errors as [`execute_parameterized_aggregate`](Self::execute_parameterized_aggregate);
789 /// additionally returns `FraiseQLError::Database` if `set_config` fails.
790 async fn execute_parameterized_aggregate_with_session(
791 &self,
792 sql: &str,
793 params: &[serde_json::Value],
794 _session_vars: &[(&str, &str)],
795 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
796 self.execute_parameterized_aggregate(sql, params).await
797 }
798
799 /// Execute a database function call and return all columns as rows.
800 ///
801 /// Builds `SELECT * FROM {function_name}($1, $2, ...)` with one positional placeholder per
802 /// argument, executes it with the provided JSON values, and returns each result row as a
803 /// `HashMap<column_name, json_value>`.
804 ///
805 /// Used by the mutation execution pathway to call stored procedures that return the
806 /// `app.mutation_response` composite type
807 /// `(status, message, entity_id, entity_type, entity jsonb, updated_fields text[],
808 /// cascade jsonb, metadata jsonb)`.
809 ///
810 /// # Arguments
811 ///
812 /// * `function_name` - Fully-qualified function name (e.g. `fn_create_machine`)
813 /// * `args` - Positional JSON arguments passed as `$1, $2, …` bind parameters
814 ///
815 /// # Errors
816 ///
817 /// Returns `FraiseQLError::Database` on query execution failure.
818 /// Returns `FraiseQLError::Unsupported` on adapters that do not support mutations
819 /// (default implementation — see [`SupportsMutations`]).
820 async fn execute_function_call(
821 &self,
822 function_name: &str,
823 _args: &[serde_json::Value],
824 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
825 Err(FraiseQLError::Unsupported {
826 message: format!(
827 "Mutations via function calls are not supported by this adapter. \
828 Function '{function_name}' cannot be executed. \
829 Use PostgreSQL, MySQL, or SQL Server for mutation support."
830 ),
831 })
832 }
833
834 /// Returns `true` if this adapter supports GraphQL mutation operations.
835 ///
836 /// **This is the authoritative mutation gate.** The executor checks this method
837 /// before dispatching any mutation. Adapters that return `false` will cause
838 /// mutations to fail with a clear `FraiseQLError::Validation` diagnostic instead
839 /// of silently calling the unsupported `execute_function_call` default.
840 ///
841 /// Override to return `false` for read-only adapters (e.g., `SqliteAdapter`,
842 /// `FraiseWireAdapter`). The compile-time [`SupportsMutations`] marker trait
843 /// complements this runtime check — see its documentation for the distinction.
844 ///
845 /// # Default
846 ///
847 /// Returns `true`. All adapters are assumed mutation-capable unless they override
848 /// this method.
849 fn supports_mutations(&self) -> bool {
850 true
851 }
852
853 /// Bump fact table version counters after a successful mutation.
854 ///
855 /// Called by the executor when a mutation definition declares
856 /// `invalidates_fact_tables`. For each listed table the version counter is
857 /// incremented so that subsequent aggregation queries miss the cache and
858 /// re-fetch fresh data.
859 ///
860 /// The default implementation is a **no-op**: adapters that are not cache-
861 /// aware (e.g. `PostgresAdapter`, `SqliteAdapter`) simply return `Ok(())`.
862 /// `CachedDatabaseAdapter` overrides this to call `bump_tf_version($1)` for
863 /// every `FactTableVersionStrategy::VersionTable` table and update the
864 /// in-process version cache.
865 ///
866 /// # Arguments
867 ///
868 /// * `tables` - Fact table names declared by the mutation (validated SQL identifiers; originate
869 /// from `MutationDefinition.invalidates_fact_tables`)
870 ///
871 /// # Errors
872 ///
873 /// Returns `FraiseQLError::Database` if the version-bump SQL function fails.
874 async fn bump_fact_table_versions(&self, _tables: &[String]) -> Result<()> {
875 Ok(())
876 }
877
878 /// Invalidate cached query results for the specified views.
879 ///
880 /// Called by the executor after a mutation succeeds, so that stale cache
881 /// entries reading from modified views are evicted. The default
882 /// implementation is a no-op; `CachedDatabaseAdapter` overrides this.
883 ///
884 /// View names are passed as `&[ViewName]` so the wrapper's `Arc<str>`
885 /// backing is preserved across the call. Callers that hold a `String`
886 /// can convert in place with `ViewName::from(...)`.
887 ///
888 /// # Returns
889 ///
890 /// The number of cache entries evicted.
891 async fn invalidate_views(&self, _views: &[crate::ViewName]) -> Result<u64> {
892 Ok(0)
893 }
894
895 /// Evict cache entries that contain the given entity UUID.
896 ///
897 /// Called by the executor after a successful UPDATE or DELETE mutation when
898 /// the `mutation_response` includes an `entity_id`. Only cache entries whose
899 /// entity-ID index contains the given UUID are removed; unrelated entries
900 /// remain warm.
901 ///
902 /// The default implementation is a no-op. `CachedDatabaseAdapter` overrides
903 /// this to perform the selective eviction.
904 ///
905 /// # Returns
906 ///
907 /// The number of cache entries evicted.
908 async fn invalidate_by_entity(&self, _entity_type: &str, _entity_id: &str) -> Result<u64> {
909 Ok(0)
910 }
911
912 /// Evict only list (multi-row) cache entries for the given views.
913 ///
914 /// Called by the executor after a successful CREATE mutation. Unlike
915 /// `invalidate_views()`, this preserves single-entity point-lookup entries
916 /// that are unaffected by the newly created entity.
917 ///
918 /// The default implementation delegates to `invalidate_views()` (safe
919 /// fallback for adapters without a `list_index`). `CachedDatabaseAdapter`
920 /// overrides this to use the dedicated `list_index` for precise eviction.
921 ///
922 /// # Returns
923 ///
924 /// The number of cache entries evicted.
925 async fn invalidate_list_queries(&self, views: &[crate::ViewName]) -> Result<u64> {
926 self.invalidate_views(views).await
927 }
928
929 /// Get database capabilities.
930 ///
931 /// Returns information about what features this database supports,
932 /// including collation strategies and limitations.
933 ///
934 /// # Returns
935 ///
936 /// `DatabaseCapabilities` describing supported features.
937 fn capabilities(&self) -> DatabaseCapabilities {
938 DatabaseCapabilities::from_database_type(self.database_type())
939 }
940
941 /// Run the database's `EXPLAIN` on a SQL statement without executing it.
942 ///
943 /// Returns a JSON representation of the query plan. The format is
944 /// database-specific (e.g. PostgreSQL returns JSON, SQLite returns rows).
945 ///
946 /// The default implementation returns `Unsupported`.
947 async fn explain_query(
948 &self,
949 _sql: &str,
950 _params: &[serde_json::Value],
951 ) -> Result<serde_json::Value> {
952 Err(fraiseql_error::FraiseQLError::Unsupported {
953 message: "EXPLAIN not available for this database adapter".to_string(),
954 })
955 }
956
957 /// Run `EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)` against a view with the
958 /// same parameterized WHERE clause that `execute_where_query` would use.
959 ///
960 /// Unlike `explain_query`, this method uses **real bound parameters** and
961 /// **actually executes the query** (ANALYZE mode), so the plan reflects
962 /// PostgreSQL's runtime statistics for the given filter values.
963 ///
964 /// Only PostgreSQL supports this; other adapters return
965 /// `FraiseQLError::Unsupported` by default.
966 ///
967 /// # Arguments
968 ///
969 /// * `view` - View name (e.g., "v_user")
970 /// * `where_clause` - Optional filter (same as `execute_where_query`)
971 /// * `limit` - Optional row limit
972 /// * `offset` - Optional row offset
973 ///
974 /// # Errors
975 ///
976 /// Returns `FraiseQLError::Database` on execution failure.
977 /// Returns `FraiseQLError::Unsupported` for non-PostgreSQL adapters.
978 async fn explain_where_query(
979 &self,
980 _view: &str,
981 _where_clause: Option<&WhereClause>,
982 _limit: Option<u32>,
983 _offset: Option<u32>,
984 ) -> Result<serde_json::Value> {
985 Err(fraiseql_error::FraiseQLError::Unsupported {
986 message: "EXPLAIN ANALYZE is not available for this database adapter. \
987 Only PostgreSQL supports explain_where_query."
988 .to_string(),
989 })
990 }
991
992 /// Returns the mutation strategy used by this adapter.
993 ///
994 /// The default is `FunctionCall` (stored procedures). Adapters that generate
995 /// direct SQL (e.g., SQLite) override this to return `DirectSql`.
996 fn mutation_strategy(&self) -> MutationStrategy {
997 MutationStrategy::FunctionCall
998 }
999
1000 /// Execute a database function call after pinning session variables on the
1001 /// **same connection** within the **same transaction** as the call.
1002 ///
1003 /// This is the connection-affine variant of
1004 /// [`execute_function_call`](Self::execute_function_call): the `set_config(..., true)`
1005 /// calls and the `SELECT * FROM fn(...)` call share one pooled connection inside one
1006 /// transaction, so transaction-local GUCs are visible to the function body (fixes #329).
1007 ///
1008 /// Adapters that do not support session variables (MySQL, SQLite, SQL
1009 /// Server, mocks) inherit the default implementation, which silently drops
1010 /// `session_vars` and delegates to [`execute_function_call`](Self::execute_function_call) —
1011 /// safe, because those backends never applied session variables in the first
1012 /// place.
1013 ///
1014 /// # Arguments
1015 ///
1016 /// * `function_name` - Fully-qualified function name
1017 /// * `args` - Positional JSON arguments passed as `$1, $2, …`
1018 /// * `session_vars` - `(setting_name, value)` pairs applied with `SELECT set_config(name,
1019 /// value, true)` before the function call. Pass `&[]` when no session variables are
1020 /// configured.
1021 ///
1022 /// # Errors
1023 ///
1024 /// Same as [`execute_function_call`](Self::execute_function_call); additionally returns
1025 /// `FraiseQLError::Database` if `set_config` fails on any pair.
1026 async fn execute_function_call_with_session(
1027 &self,
1028 function_name: &str,
1029 args: &[serde_json::Value],
1030 _session_vars: &[(&str, &str)],
1031 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
1032 // Default: ignore session_vars and delegate. Safe for non-PostgreSQL
1033 // adapters, which never applied session variables in the first place.
1034 self.execute_function_call(function_name, args).await
1035 }
1036
1037 /// Connection-affine variant of
1038 /// [`execute_function_call_with_session`](Self::execute_function_call_with_session)
1039 /// that **also writes one `core.tb_entity_change_log` row in the same
1040 /// transaction** as the mutation function — the Change Spine transactional
1041 /// outbox.
1042 ///
1043 /// When `changelog` is `Some`, the framework owns the change-log write: a
1044 /// single statement runs the function and INSERTs the outbox row atomically
1045 /// on the same connection, so `fraiseql.started_at` (set txn-locally for the
1046 /// `duration_ms` computation) is visible and a crash leaves neither the
1047 /// mutation nor the log row. The changed-entity columns are read from the
1048 /// function's own `app.mutation_response` row; only the DML verb and a
1049 /// NOT-NULL `object_type` fallback are threaded in via [`ChangeLogWrite`].
1050 /// The row is written only for an effective change (`succeeded` AND
1051 /// `state_changed`).
1052 ///
1053 /// When `changelog` is `None`, behaviour is identical to
1054 /// [`execute_function_call_with_session`](Self::execute_function_call_with_session).
1055 ///
1056 /// PostgreSQL, MySQL, and SQL Server each override this with a real in-txn
1057 /// write. PostgreSQL runs one `MATERIALIZED` CTE that calls the function and
1058 /// INSERTs the outbox row atomically; MySQL and SQL Server cannot reference a
1059 /// `CALL`/`EXEC` result set in a following `INSERT … SELECT`, so they open a
1060 /// transaction, parse the `app.mutation_response` row in Rust, and INSERT the
1061 /// outbox row (via [`crate::changelog::build_changelog_insert_sql`]) on the same
1062 /// connection before commit. On those two dialects `duration_ms` / `started_at`
1063 /// are legitimately NULL (no request-scoped DB clock).
1064 ///
1065 /// SQLite (read-only) and mocks inherit the default below, which drops
1066 /// `changelog` and delegates — so those mutations still run, they just write no
1067 /// outbox row.
1068 ///
1069 /// # Errors
1070 ///
1071 /// Same as
1072 /// [`execute_function_call_with_session`](Self::execute_function_call_with_session);
1073 /// additionally returns `FraiseQLError::Database` if the outbox INSERT fails
1074 /// (e.g. the contract migration has not been applied).
1075 async fn execute_function_call_with_changelog(
1076 &self,
1077 function_name: &str,
1078 args: &[serde_json::Value],
1079 session_vars: &[(&str, &str)],
1080 _changelog: Option<&ChangeLogWrite<'_>>,
1081 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
1082 // Default: ignore the change-log write and delegate. SQLite (read-only) and
1083 // mocks keep this no-op; PostgreSQL / MySQL / SQL Server override it.
1084 self.execute_function_call_with_session(function_name, args, session_vars).await
1085 }
1086
1087 /// Validate-bind-without-commit variant of
1088 /// [`execute_function_call`](Self::execute_function_call): run the mutation
1089 /// function inside a transaction that is **rolled back** instead of committed.
1090 ///
1091 /// The function binds and executes — so constraints, triggers, and the
1092 /// `app.mutation_response` shape are all exercised — but no writes persist and
1093 /// no change-log outbox row is emitted. Powers `fraiseql query --dry-run` and
1094 /// the `doctor --runtime` mutation probes (driven by the executor's
1095 /// `RuntimeConfig::dry_run_mutations` flag).
1096 ///
1097 /// Only the PostgreSQL adapter overrides this today; every other adapter keeps
1098 /// the default below, which returns `Unsupported` rather than silently
1099 /// committing. (SQL Server already wraps mutations in `BEGIN TRANSACTION` and
1100 /// could override this with a `ROLLBACK`; SQLite uses `MutationStrategy::DirectSql`
1101 /// and would need its own variant.)
1102 ///
1103 /// # Errors
1104 ///
1105 /// Returns `FraiseQLError::Unsupported` on adapters that do not implement a
1106 /// rollback path. PostgreSQL returns `FraiseQLError::Database` on execution
1107 /// failure (mirroring [`execute_function_call`](Self::execute_function_call)).
1108 async fn execute_function_call_dry_run(
1109 &self,
1110 _function_name: &str,
1111 _args: &[serde_json::Value],
1112 _session_vars: &[(&str, &str)],
1113 ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
1114 Err(FraiseQLError::Unsupported {
1115 message:
1116 "--dry-run mutations (validate-bind-without-commit) are only supported by the \
1117 PostgreSQL adapter."
1118 .to_string(),
1119 })
1120 }
1121
1122 /// Connection-affine variant of [`execute_where_query_arc`](Self::execute_where_query_arc).
1123 ///
1124 /// Applies `session_vars` transaction-locally on the same connection that
1125 /// runs the read, so PostgreSQL Row-Level-Security policies backed by
1126 /// `current_setting()` see the configured values (fixes #329). See
1127 /// [`execute_function_call_with_session`](Self::execute_function_call_with_session) for the
1128 /// rationale and the non-PostgreSQL default behaviour.
1129 ///
1130 /// # Errors
1131 ///
1132 /// Same errors as [`execute_where_query_arc`](Self::execute_where_query_arc); additionally
1133 /// returns `FraiseQLError::Database` if `set_config` fails on any pair.
1134 async fn execute_where_query_arc_with_session(
1135 &self,
1136 view: &str,
1137 where_clause: Option<&WhereClause>,
1138 limit: Option<u32>,
1139 offset: Option<u32>,
1140 order_by: Option<&[OrderByClause]>,
1141 _session_vars: &[(&str, &str)],
1142 ) -> Result<Arc<Vec<JsonbValue>>> {
1143 self.execute_where_query_arc(view, where_clause, limit, offset, order_by).await
1144 }
1145
1146 /// Connection-affine variant of
1147 /// [`execute_with_projection_arc`](Self::execute_with_projection_arc).
1148 ///
1149 /// See [`execute_where_query_arc_with_session`](Self::execute_where_query_arc_with_session) for
1150 /// the rationale.
1151 ///
1152 /// # Errors
1153 ///
1154 /// Same errors as [`execute_with_projection_arc`](Self::execute_with_projection_arc);
1155 /// additionally returns `FraiseQLError::Database` if `set_config` fails on any pair.
1156 async fn execute_with_projection_arc_with_session(
1157 &self,
1158 request: &ProjectionRequest<'_>,
1159 _session_vars: &[(&str, &str)],
1160 ) -> Result<Arc<Vec<JsonbValue>>> {
1161 self.execute_with_projection_arc(request).await
1162 }
1163
1164 /// Execute a direct SQL mutation (INSERT/UPDATE/DELETE) and return the
1165 /// mutation response rows as JSON objects.
1166 ///
1167 /// Only adapters using `MutationStrategy::DirectSql` need to override this.
1168 /// The default implementation returns `Unsupported`.
1169 ///
1170 /// # Errors
1171 ///
1172 /// Returns `FraiseQLError::Unsupported` by default.
1173 /// Returns `FraiseQLError::Database` on SQL execution failure.
1174 /// Returns `FraiseQLError::Validation` on invalid mutation parameters.
1175 async fn execute_direct_mutation(
1176 &self,
1177 _ctx: &DirectMutationContext<'_>,
1178 ) -> Result<Vec<serde_json::Value>> {
1179 Err(FraiseQLError::Unsupported {
1180 message: "Direct SQL mutations are not supported by this adapter. \
1181 Use execute_function_call for stored-procedure mutations."
1182 .to_string(),
1183 })
1184 }
1185
1186 /// Retrieve query performance statistics from the database.
1187 ///
1188 /// Returns the top-N queries ordered by total execution time (descending).
1189 /// The exact data source depends on the backend:
1190 /// - PostgreSQL: `pg_stat_statements` (requires extension)
1191 /// - MySQL: `performance_schema.events_statements_summary_by_digest`
1192 /// - SQL Server: `sys.dm_exec_query_stats`
1193 /// - SQLite / Wire: empty (no stats available)
1194 ///
1195 /// # Arguments
1196 ///
1197 /// * `limit` - Maximum number of entries to return.
1198 ///
1199 /// # Errors
1200 ///
1201 /// Returns `FraiseQLError::Database` if the stats query fails.
1202 async fn query_stats(&self, _limit: u32) -> Result<Vec<crate::types::QueryStatEntry>> {
1203 Ok(vec![])
1204 }
1205
1206 /// Retrieve statistics for a single query by its ID.
1207 ///
1208 /// The default implementation fetches up to 1000 entries via
1209 /// [`query_stats`](Self::query_stats) and filters client-side.
1210 /// Backends with efficient single-query lookup (PostgreSQL, SQL Server)
1211 /// should override with a `WHERE` clause.
1212 ///
1213 /// # Errors
1214 ///
1215 /// Returns `FraiseQLError::Database` if the underlying query fails.
1216 async fn query_stats_by_id(&self, id: &str) -> Result<Option<crate::types::QueryStatEntry>> {
1217 let stats = self.query_stats(1000).await?;
1218 Ok(stats.into_iter().find(|e| e.query_id == id))
1219 }
1220
1221 /// Reset query performance statistics.
1222 ///
1223 /// Only PostgreSQL supports this (via `pg_stat_statements_reset()`).
1224 /// All other adapters return `Unsupported`.
1225 ///
1226 /// # Errors
1227 ///
1228 /// Returns `FraiseQLError::Unsupported` for adapters that cannot reset stats.
1229 /// Returns `FraiseQLError::Database` if the reset command fails.
1230 async fn reset_query_stats(&self) -> Result<()> {
1231 Err(FraiseQLError::Unsupported {
1232 message: "Query stats reset is not supported by this database adapter".to_string(),
1233 })
1234 }
1235
1236 /// Notify the adapter that the schema has changed.
1237 ///
1238 /// Called during hot-reload after the new schema has been validated.
1239 /// Adapters that maintain schema-dependent state (e.g. cache keyed by schema
1240 /// version) should clear or rebuild that state here.
1241 ///
1242 /// The default implementation is a no-op.
1243 fn on_schema_reload(&self) {}
1244}
1245
1246#[cfg(test)]
1247mod tests;