Skip to main content

fraiseql_db/
traits.rs

1//! Database adapter trait definitions.
2//!
3//! The main [`DatabaseAdapter`] trait lives in this file. Supporting types
4//! (`RelayPageResult`, enums, type aliases) are in
5//! the `adapter_types` submodule.
6
7mod adapter_types;
8mod composed_read;
9mod mutations;
10mod relay;
11
12use std::sync::Arc;
13
14pub use adapter_types::*;
15use async_trait::async_trait;
16pub use composed_read::{
17    COMPOSED_DOCUMENT_KEY, COMPOSED_EMBEDS_KEY, ComposedEmbed, ComposedKeyset, ComposedLevel,
18    EmbedShape, EmbedSource, LevelKeys, composed_read_unsupported,
19};
20use fraiseql_error::{FraiseQLError, Result};
21pub use mutations::{WriteMode, WriteRequest, Writer};
22pub use relay::RelayDatabaseAdapter;
23
24use crate::{
25    types::{
26        DatabaseType, JsonbValue, PoolMetrics, ReadRouting,
27        sql_hints::{OrderByClause, SqlProjectionHint},
28    },
29    where_clause::WhereClause,
30};
31
32/// Adjudicates the rows a mutation function returned, from **inside** the
33/// transaction that produced them and before it commits.
34///
35/// `Ok(())` commits the transaction; `Err(e)` rolls it back and `e` reaches the
36/// caller. See [`Writer::execute_write`] for why a write needs a decision seam this late
37/// (#1353).
38///
39/// Synchronous by construction: the only decision taken here today is the field
40/// authorizer's, whose `authorize_field` is itself synchronous, and holding an open
41/// transaction across an arbitrary `await` would pin a pooled connection for as long
42/// as app-supplied policy code chose to take.
43pub type MutationRowGate<'a> = &'a (
44        dyn Fn(&[std::collections::HashMap<String, serde_json::Value>]) -> Result<()> + Send + Sync
45    );
46
47/// The framework-owned change-log row the mutation executor writes in-txn.
48///
49/// Carries only the fields the adapter cannot derive from the
50/// `app.mutation_response` row it already holds: the DML verb and a NOT-NULL
51/// `object_type` fallback. The changed-entity identity + payload (`object_id`,
52/// `object_data`, `updated_fields`, `cascade`) are read from the function's own
53/// returned row inside the same transaction (see [`Writer::execute_write`]).
54///
55/// This is the Change Spine transactional-outbox contract. Beyond the
56/// `object_type`/`modification_type` + changed-entity columns, it stamps the
57/// envelope: `tenant_id` (carried here, from `SecurityContext`),
58/// `trace_id` (the W3C trace id of the originating request), `schema_version`
59/// (the compiled schema's content hash — a per-deployment constant),
60/// `trace_context` (the full W3C trace context as JSON), `actor_type` /
61/// `acting_for` (the request's actor classification and, for a delegated agent,
62/// the underlying human — #390), `commit_time` (`clock_timestamp()` at INSERT),
63/// and `seq` (the table's `SEQUENCE` default).
64#[derive(Debug, Clone, Copy)]
65pub struct ChangeLogWrite<'a> {
66    /// NOT-NULL fallback for `object_type` when the row's `entity_type` is NULL.
67    /// Sourced from `MutationDefinition.return_type` (always present).
68    pub object_type:       &'a str,
69    /// The DML verb written to `modification_type` (e.g. `"INSERT"`,
70    /// `"UPDATE"`, `"DELETE"`, `"CUSTOM"`), from `MutationOperation`.
71    pub modification_type: &'a str,
72    /// The tenant partition stamp written to the `tenant_id UUID` column — the
73    /// Trinity public-facing identifier, read from `SecurityContext.tenant_id`
74    /// at write time and **never** reconstructed from connection / RLS state
75    /// (RLS is PG-only; out-of-session spine consumers bypass it, so the row
76    /// must carry tenant identity explicitly). `None` (→ SQL NULL) for an
77    /// unauthenticated request, a request with no tenant, or a tenant
78    /// identifier that is not a UUID.
79    pub tenant_id:         Option<uuid::Uuid>,
80    /// The W3C trace id of the originating request, written to the `trace_id`
81    /// column so an outbox row links back to its distributed trace (the #392
82    /// perf tooling surfaces it as the investigation handle). Read from the
83    /// request's `traceparent` header at write time; `None` (→ SQL NULL) for a
84    /// request without a trace context — e.g. an unauthenticated mutation, which
85    /// carries no `SecurityContext` to stamp.
86    pub trace_id:          Option<&'a str>,
87    /// The compiled schema's version written to the `schema_version` column so an
88    /// outbox row records which deployment produced it — the replay /
89    /// zero-downtime correctness handle for #378 (reject a row replayed under a
90    /// different schema). A per-deployment constant derived from the compiled
91    /// schema (`CompiledSchema::content_hash()`), **not** from the request, so it
92    /// changes on any schema change. `None` (→ SQL NULL) for producers with no
93    /// compiled schema in scope — cooperative external producers (ETL) and the
94    /// non-PostgreSQL no-op path.
95    pub schema_version:    Option<&'a str>,
96    /// The originating request's **full W3C trace context** as a JSON object
97    /// (`{version, trace_id, parent_id, trace_flags, tracestate?}`), written to the
98    /// `trace_context` JSONB column so a row carries enough to re-propagate /
99    /// reconstruct the distributed trace — not just the scalar `trace_id`. Carried
100    /// here as pre-serialized JSON **text** (the adapter binds it to the JSONB
101    /// column). Built from the request's `traceparent` / `tracestate` headers at
102    /// write time; `None` (→ SQL NULL) for a request without a valid trace context,
103    /// consistent with `trace_id`.
104    pub trace_context:     Option<&'a str>,
105    /// The request's actor classification written to the `actor_type` column (the
106    /// `snake_case` `ActorType` token: `"human_user"`, `"service_account"`,
107    /// `"ai_agent"`, `"system_job"`), from `SecurityContext.actor_type()` at write
108    /// time (#390). `None` (→ SQL NULL) for a request with no `SecurityContext` to
109    /// stamp (an unauthenticated mutation), or a cooperative external producer.
110    pub actor_type:        Option<&'a str>,
111    /// For a delegated agent request, the **underlying human** the agent acts for
112    /// — the public-facing UUID, written to the `acting_for UUID` column from
113    /// `SecurityContext.acting_for()` (#390). Mirrors `tenant_id`'s UUID shape so
114    /// it is stamped without a DB lookup. `None` (→ SQL NULL) for a non-delegated
115    /// request, an unauthenticated mutation, or a subject that is not UUID-shaped.
116    pub acting_for:        Option<uuid::Uuid>,
117    /// The ingress transport the originating request declared (#376), e.g.
118    /// `"mcp"` — merged into the row's `extra_metadata` JSONB as
119    /// `{"transport": …}` so MCP-originated (and, later, other
120    /// transport-tagged) writes are queryable
121    /// (`extra_metadata->>'transport' = 'mcp'`). From
122    /// `SecurityContext.transport()` at write time; `None` omits the key —
123    /// for the HTTP GraphQL path (which does not stamp one today), an
124    /// unauthenticated mutation, or a cooperative producer.
125    pub transport:         Option<&'a str>,
126    /// Whether this outbox write also records the changed entity's **pre-image**
127    /// (before-state) into the `object_data_before JSONB` column, sourced from the
128    /// function's own `entity_before` (the after-image comes from `entity`). Set
129    /// from `MutationDefinition.changelog_pre_image`; `false` (the default) leaves
130    /// `object_data_before` out of the INSERT entirely (NULL), byte-for-byte
131    /// today's behavior. The changed-entity payload itself is read from the
132    /// returned row inside the outbox CTE, so this flag only selects which SQL
133    /// form the adapter emits.
134    pub pre_image:         bool,
135}
136
137impl<'a> ChangeLogWrite<'a> {
138    /// Build a change-log write descriptor with no envelope stamps (`tenant_id`,
139    /// `trace_id`, `schema_version`, `trace_context`, `actor_type` and
140    /// `acting_for` NULL). Chain [`with_tenant_id`](Self::with_tenant_id) /
141    /// [`with_trace_id`](Self::with_trace_id) /
142    /// [`with_schema_version`](Self::with_schema_version) /
143    /// [`with_trace_context`](Self::with_trace_context) /
144    /// [`with_actor_type`](Self::with_actor_type) /
145    /// [`with_acting_for`](Self::with_acting_for) to stamp them.
146    #[must_use]
147    pub const fn new(object_type: &'a str, modification_type: &'a str) -> Self {
148        Self {
149            object_type,
150            modification_type,
151            tenant_id: None,
152            trace_id: None,
153            schema_version: None,
154            trace_context: None,
155            actor_type: None,
156            acting_for: None,
157            transport: None,
158            pre_image: false,
159        }
160    }
161
162    /// Stamp the tenant partition id (the Trinity public-facing UUID) onto the
163    /// outbox row. `None` leaves `tenant_id` NULL — for system / unauthenticated
164    /// rows, or a tenant identifier that is not UUID-shaped.
165    #[must_use]
166    pub const fn with_tenant_id(mut self, tenant_id: Option<uuid::Uuid>) -> Self {
167        self.tenant_id = tenant_id;
168        self
169    }
170
171    /// Stamp the originating request's W3C trace id onto the outbox row. `None`
172    /// leaves `trace_id` NULL — for a request with no trace context.
173    #[must_use]
174    pub const fn with_trace_id(mut self, trace_id: Option<&'a str>) -> Self {
175        self.trace_id = trace_id;
176        self
177    }
178
179    /// Stamp the compiled schema's version (its content hash) onto the outbox
180    /// row. `None` leaves `schema_version` NULL — for producers with no compiled
181    /// schema in scope (cooperative external producers, the non-PostgreSQL no-op
182    /// path).
183    #[must_use]
184    pub const fn with_schema_version(mut self, schema_version: Option<&'a str>) -> Self {
185        self.schema_version = schema_version;
186        self
187    }
188
189    /// Stamp the originating request's full W3C trace context (pre-serialized JSON
190    /// text) onto the outbox row's `trace_context` JSONB column. `None` leaves it
191    /// NULL — for a request with no valid trace context, or a non-PostgreSQL
192    /// no-op / cooperative producer.
193    #[must_use]
194    pub const fn with_trace_context(mut self, trace_context: Option<&'a str>) -> Self {
195        self.trace_context = trace_context;
196        self
197    }
198
199    /// Stamp the request's actor classification (the `snake_case` `ActorType`
200    /// token) onto the outbox row's `actor_type` column (#390). `None` leaves it
201    /// NULL — for an unauthenticated mutation or a cooperative producer.
202    #[must_use]
203    pub const fn with_actor_type(mut self, actor_type: Option<&'a str>) -> Self {
204        self.actor_type = actor_type;
205        self
206    }
207
208    /// Stamp the delegated user's UUID (the human a delegated agent acts for) onto
209    /// the outbox row's `acting_for` column (#390). `None` leaves it NULL — for a
210    /// non-delegated request, an unauthenticated mutation, or a non-UUID subject.
211    #[must_use]
212    pub const fn with_acting_for(mut self, acting_for: Option<uuid::Uuid>) -> Self {
213        self.acting_for = acting_for;
214        self
215    }
216
217    /// Stamp the ingress transport (e.g. `"mcp"`) onto the outbox row's
218    /// `extra_metadata.transport` key (#376). `None` omits the key — for a
219    /// transport that does not declare itself, an unauthenticated mutation, or
220    /// a cooperative producer.
221    #[must_use]
222    pub const fn with_transport(mut self, transport: Option<&'a str>) -> Self {
223        self.transport = transport;
224        self
225    }
226
227    /// Opt this outbox write into recording the changed entity's pre-image into
228    /// the `object_data_before` column (from the function's `entity_before`). When
229    /// `false` (the default), `object_data_before` is omitted from the INSERT
230    /// entirely, byte-for-byte today's behavior. Set from
231    /// `MutationDefinition.changelog_pre_image`.
232    #[must_use]
233    pub const fn with_pre_image(mut self, pre_image: bool) -> Self {
234        self.pre_image = pre_image;
235        self
236    }
237}
238
239/// Database adapter for executing queries against views.
240///
241/// This trait abstracts over different database backends (PostgreSQL, MySQL, SQLite, SQL Server).
242/// All implementations must support:
243/// - Executing parameterized WHERE queries against views
244/// - Returning JSONB data from the `data` column
245/// - Connection pooling and health checks
246/// - Row-level security (RLS) WHERE clauses
247///
248/// # Architecture
249///
250/// The adapter is the runtime interface to the database. It receives:
251/// - View/table name (e.g., "v_user", "tf_sales")
252/// - Parameterized WHERE clauses (AST form, not strings)
253/// - Projection hints (for performance optimization)
254/// - Pagination parameters (LIMIT/OFFSET)
255///
256/// And returns:
257/// - JSONB rows from the `data` column (most operations)
258/// - Arbitrary rows as HashMap (for aggregation queries)
259/// - Mutation results from stored procedures
260///
261/// # Implementing a New Adapter
262///
263/// To add support for a new database (e.g., Oracle, Snowflake):
264///
265/// 1. **Create a new module** in `src/db/your_database/`
266/// 2. **Implement the trait**:
267///
268///    ```rust,ignore
269///    pub struct YourDatabaseAdapter { /* fields */ }
270///
271///    #[async_trait]
272///    impl DatabaseAdapter for YourDatabaseAdapter {
273///        async fn execute_where_query(&self, ...) -> Result<Vec<JsonbValue>> {
274///            // 1. Build parameterized SQL from WhereClause AST
275///            // 2. Execute with bound parameters (NO string concatenation)
276///            // 3. Return JSONB from data column
277///        }
278///        // Implement other required methods...
279///    }
280///    ```
281/// 3. **Add feature flag** to `Cargo.toml` (e.g., `feature = "your-database"`)
282/// 4. **Copy structure from PostgreSQL adapter** — see `src/db/postgres/adapter.rs`
283/// 5. **Add tests** in `tests/integration/your_database_test.rs`
284///
285/// # Security Requirements
286///
287/// All implementations MUST:
288/// - **Never concatenate user input into SQL strings**
289/// - **Always use parameterized queries** with bind parameters
290/// - **Validate parameter types** before binding
291/// - **Preserve RLS WHERE clauses** (never filter them out)
292/// - **Return errors, not silently fail** (e.g., connection loss)
293///
294/// # Connection Management
295///
296/// - Use a connection pool (recommended: 20 connections default)
297/// - Implement `health_check()` for ping-based monitoring
298/// - Provide `pool_metrics()` for observability
299/// - Handle stale connections gracefully
300///
301/// # Performance Characteristics
302///
303/// Expected throughput when properly implemented:
304/// - **Simple queries** (single table, no WHERE): 250+ Kelem/s
305/// - **Complex queries** (JOINs, multiple conditions): 50+ Kelem/s
306/// - **Mutations** (stored procedures): 1-10 RPS (depends on procedure)
307/// - **Relay pagination** (keyset cursors): 15-30ms latency
308///
309/// # Example: PostgreSQL Implementation
310///
311/// ```rust,ignore
312/// use sqlx::postgres::PgPool;
313/// use async_trait::async_trait;
314///
315/// pub struct PostgresAdapter {
316///     pool: PgPool,
317/// }
318///
319/// #[async_trait]
320/// impl DatabaseAdapter for PostgresAdapter {
321///     async fn execute_where_query(
322///         &self,
323///         view: &str,
324///         where_clause: Option<&WhereClause>,
325///         limit: Option<u32>,
326///         offset: Option<u32>,
327///     ) -> Result<Vec<JsonbValue>> {
328///         // 1. Build SQL: SELECT data FROM {view} WHERE {where_clause} LIMIT {limit}
329///         let mut sql = format!(r#"SELECT data FROM "{}""#, view);
330///
331///         // 2. Add WHERE clause (converts AST to parameterized SQL)
332///         let params = if let Some(where_clause) = where_clause {
333///             sql.push_str(" WHERE ");
334///             let (where_sql, params) = build_where_sql(where_clause)?;
335///             sql.push_str(&where_sql);
336///             params
337///         } else {
338///             vec![]
339///         };
340///
341///         // 3. Add LIMIT and OFFSET
342///         if let Some(limit) = limit {
343///             sql.push_str(" LIMIT ");
344///             sql.push_str(&limit.to_string());
345///         }
346///         if let Some(offset) = offset {
347///             sql.push_str(" OFFSET ");
348///             sql.push_str(&offset.to_string());
349///         }
350///
351///         // 4. Execute with bound parameters (NO string interpolation)
352///         let rows: Vec<(serde_json::Value,)> = sqlx::query_as(&sql)
353///             .bind(&params[0])
354///             .bind(&params[1])
355///             // ... bind all parameters
356///             .fetch_all(&self.pool)
357///             .await?;
358///
359///         // 5. Extract JSONB and return
360///         Ok(rows.into_iter().map(|(data,)| data).collect())
361///     }
362///
363///     // Implement other required methods...
364/// }
365/// ```
366///
367/// # Example: Basic Usage
368///
369/// ```rust,no_run
370/// use fraiseql_db::{DatabaseAdapter, WhereClause, WhereOperator};
371/// use serde_json::json;
372///
373/// # async fn example(adapter: impl DatabaseAdapter) -> Result<(), Box<dyn std::error::Error>> {
374/// // Build WHERE clause (AST, not string)
375/// let where_clause = WhereClause::Field {
376///     path: vec!["email".to_string()],
377///     operator: WhereOperator::Icontains,
378///     value: json!("example.com"),
379/// };
380///
381/// // Execute query with parameters
382/// let results = adapter
383///     .execute_where_query("v_user", Some(&where_clause), Some(10), None, None)
384///     .await?;
385///
386/// println!("Found {} users matching filter", results.len());
387/// # Ok(())
388/// # }
389/// ```
390///
391/// # See Also
392///
393/// - `WhereClause` — AST for parameterized WHERE clauses
394/// - `RelayDatabaseAdapter` — Optional trait for keyset pagination
395/// - [Performance Guide](https://docs.fraiseql.rs/performance/database-adapters.md)
396// POLICY: `#[async_trait]` placement for `DatabaseAdapter`
397//
398// `DatabaseAdapter` is used both generically (`Server<A: DatabaseAdapter>` in axum
399// handlers, zero overhead via static dispatch) and dynamically (`Arc<dyn
400// DatabaseAdapter + Send + Sync>` in federation, heap-boxed future per call).
401//
402// `#[async_trait]` is required on:
403// - The trait definition (generates `Pin<Box<dyn Future + Send>>` return types)
404// - Every `impl DatabaseAdapter for ConcreteType` block (generates the boxing)
405// NOT required on callers (they see `Pin<Box<dyn Future + Send>>` from macro output).
406//
407// Why not native `async fn in trait` (Rust 1.75+)?
408// Native dyn async trait does NOT propagate `+ Send` on generated futures. Tokio
409// requires futures spawned with `tokio::spawn` to be `Send`. Until Return Type
410// Notation (RFC 3425, tracking: github.com/rust-lang/rust/issues/109417) stabilises,
411// `async_trait` is the only ergonomic path to `dyn DatabaseAdapter + Send + Sync`.
412// Re-evaluate when Rust 1.90+ ships or when RTN is stabilised.
413//
414// MIGRATION TRACKING: async-trait → native async fn in trait
415//
416// Current status: BLOCKED on RFC 3425 (Return Type Notation)
417// See: https://github.com/rust-lang/rfcs/pull/3425
418//      https://github.com/rust-lang/rust/issues/109417
419//
420// Migration is safe when ALL of the following are true:
421// 1. RTN with `+ Send` bounds is stable on rustc (e.g. `fn foo() -> impl Future + Send`)
422// 2. FraiseQL MSRV is updated to that stabilising version
423// 3. tokio::spawn() works with native dyn async trait objects (futures must be Send)
424//
425// Scope when criteria are met: 68 files (grep -rn "#\[async_trait\]" crates/)
426// Effort: Medium (mostly mechanical — remove macro from impls, adjust trait defs)
427// dynosaur was evaluated and rejected: does not propagate + Send (incompatible with Tokio)
428#[async_trait]
429pub trait DatabaseAdapter: Send + Sync + 'static {
430    /// Execute a WHERE query against a view and return JSONB rows.
431    ///
432    /// # Arguments
433    ///
434    /// * `view` - View name (e.g., "v_user", "v_post")
435    /// * `where_clause` - Optional WHERE clause AST
436    /// * `limit` - Optional row limit (for pagination)
437    /// * `offset` - Optional row offset (for pagination)
438    /// * `security_context` - Optional security context for RLS and caching decisions
439    ///
440    /// # Returns
441    ///
442    /// Vec of JSONB values from the `data` column.
443    ///
444    /// # Errors
445    ///
446    /// Returns `FraiseQLError::Database` on query execution failure.
447    /// Returns `FraiseQLError::ConnectionPool` if connection pool is exhausted.
448    ///
449    /// # Example
450    ///
451    /// ```rust,no_run
452    /// # use fraiseql_db::DatabaseAdapter;
453    /// # async fn example(adapter: impl DatabaseAdapter) -> Result<(), Box<dyn std::error::Error>> {
454    /// // Simple query without WHERE clause
455    /// let all_users = adapter
456    ///     .execute_where_query("v_user", None, Some(10), Some(0), None)
457    ///     .await?;
458    /// # Ok(())
459    /// # }
460    /// ```
461    async fn execute_where_query(
462        &self,
463        view: &str,
464        where_clause: Option<&WhereClause>,
465        limit: Option<u32>,
466        offset: Option<u32>,
467        order_by: Option<&[OrderByClause]>,
468    ) -> Result<Vec<JsonbValue>>;
469
470    /// Execute a WHERE query with SQL field projection optimization.
471    ///
472    /// Projects only the requested fields at the database level, reducing network payload
473    /// and JSON deserialization overhead by **40-55%** based on production measurements.
474    ///
475    /// This is the primary query execution method for optimized GraphQL queries.
476    /// It automatically selects only the fields requested in the GraphQL query, avoiding
477    /// unnecessary network transfer and deserialization of unused fields.
478    ///
479    /// # Automatic Projection
480    ///
481    /// In most cases, you don't call this directly. The `Executor` automatically:
482    /// 1. Determines which fields the GraphQL query requests
483    /// 2. Generates a `SqlProjectionHint` using database-specific SQL
484    /// 3. Calls this method with the projection hint
485    ///
486    /// # Arguments
487    ///
488    /// * `view` - View name (e.g., "v_user", "v_post")
489    /// * `projection` - Optional SQL projection hint with field list
490    ///   - `Some(hint)`: Use projection to select only requested fields
491    ///   - `None`: Falls back to standard query (full JSONB column)
492    /// * `where_clause` - Optional WHERE clause AST for filtering
493    /// * `limit` - Optional row limit (for pagination)
494    ///
495    /// # Returns
496    ///
497    /// Vec of JSONB values, either:
498    /// - Full objects (when projection is None)
499    /// - Projected objects with only requested fields (when projection is Some)
500    ///
501    /// # Errors
502    ///
503    /// Returns `FraiseQLError::Database` on query execution failure, including:
504    /// - Connection pool exhaustion
505    /// - SQL execution errors
506    /// - Type mismatches
507    ///
508    /// # Performance Characteristics
509    ///
510    /// When projection is provided (recommended):
511    /// - **Latency**: 40-55% reduction vs full object fetch
512    /// - **Network**: 40-55% smaller payload (proportional to unused fields)
513    /// - **Throughput**: Maintains 250+ Kelem/s (elements per second)
514    /// - **Memory**: Proportional to projected fields only
515    ///
516    /// Improvement scales with:
517    /// - Percentage of unused fields (more unused = more improvement)
518    /// - Size of result set (larger sets benefit more)
519    /// - Network latency (network-bound queries benefit most)
520    ///
521    /// When projection is None:
522    /// - Behavior identical to `execute_where_query()`
523    /// - Returns full JSONB column
524    /// - Used for compatibility/debugging
525    ///
526    /// # Database Support
527    ///
528    /// | Database | Status | Implementation |
529    /// |----------|--------|-----------------|
530    /// | PostgreSQL | ✅ Optimized | `jsonb_build_object()` |
531    /// | MySQL | ⏳ Fallback | Server-side filtering (planned) |
532    /// | SQLite | ⏳ Fallback | Server-side filtering (planned) |
533    /// | SQL Server | ⏳ Fallback | Server-side filtering (planned) |
534    ///
535    /// # Example: Direct Usage (Advanced)
536    ///
537    /// ```no_run
538    /// // Requires: running PostgreSQL database and a DatabaseAdapter implementation.
539    /// use fraiseql_db::types::SqlProjectionHint;
540    /// use fraiseql_db::traits::DatabaseAdapter;
541    /// use fraiseql_db::DatabaseType;
542    ///
543    /// # async fn example(adapter: &impl DatabaseAdapter) -> Result<(), Box<dyn std::error::Error>> {
544    /// let projection = SqlProjectionHint::new(
545    ///     DatabaseType::PostgreSQL,
546    ///     "jsonb_build_object(\
547    ///         'id', data->>'id', \
548    ///         'name', data->>'name', \
549    ///         'email', data->>'email'\
550    ///     )".to_string(),
551    ///     75,
552    /// );
553    ///
554    /// let results = adapter
555    ///     .execute_with_projection("v_user", Some(&projection), None, Some(100), None, None)
556    ///     .await?;
557    ///
558    /// // results only contain id, name, email fields
559    /// // 75% smaller than fetching all fields
560    /// # Ok(())
561    /// # }
562    /// ```
563    ///
564    /// # Example: Fallback (No Projection)
565    ///
566    /// ```no_run
567    /// // Requires: running PostgreSQL database and a DatabaseAdapter implementation.
568    /// # use fraiseql_db::traits::DatabaseAdapter;
569    /// # async fn example(adapter: &impl DatabaseAdapter) -> Result<(), Box<dyn std::error::Error>> {
570    /// // For debugging or when projection not available
571    /// let results = adapter
572    ///     .execute_with_projection("v_user", None, None, Some(100), None, None)
573    ///     .await?;
574    ///
575    /// // Equivalent to execute_where_query() - returns full objects
576    /// # Ok(())
577    /// # }
578    /// ```
579    ///
580    /// # See Also
581    ///
582    /// - `execute_where_query()` - Standard query without projection
583    /// - `SqlProjectionHint` - Structure defining field projection
584    /// - [Projection Optimization Guide](https://docs.fraiseql.rs/performance/projection-optimization.md)
585    async fn execute_with_projection(
586        &self,
587        view: &str,
588        projection: Option<&SqlProjectionHint>,
589        where_clause: Option<&WhereClause>,
590        limit: Option<u32>,
591        offset: Option<u32>,
592        order_by: Option<&[OrderByClause]>,
593    ) -> Result<Vec<JsonbValue>>;
594
595    /// Like `execute_where_query` but returns the result wrapped in an `Arc`.
596    ///
597    /// The default implementation wraps the result of `execute_where_query` in a
598    /// fresh `Arc`. `CachedDatabaseAdapter` overrides this to return the cached `Arc`
599    /// directly — eliminating the full `Vec<JsonbValue>` clone that the non-`Arc`
600    /// path requires on every cache hit.
601    ///
602    /// Callers on the hot query path should prefer this variant and borrow from the
603    /// `Arc` via `&**arc` rather than taking ownership.
604    ///
605    /// # Errors
606    ///
607    /// Same errors as `execute_where_query`.
608    async fn execute_where_query_arc(
609        &self,
610        view: &str,
611        where_clause: Option<&WhereClause>,
612        limit: Option<u32>,
613        offset: Option<u32>,
614        order_by: Option<&[OrderByClause]>,
615    ) -> Result<Arc<Vec<JsonbValue>>> {
616        self.execute_where_query(view, where_clause, limit, offset, order_by)
617            .await
618            .map(Arc::new)
619    }
620
621    /// Like `execute_with_projection` but returns the result wrapped in an `Arc`.
622    ///
623    /// The default implementation wraps the result of `execute_with_projection` in a
624    /// fresh `Arc`. `CachedDatabaseAdapter` overrides this to return the cached `Arc`
625    /// directly — eliminating the full `Vec<JsonbValue>` clone that the non-`Arc`
626    /// path requires on every cache hit.
627    ///
628    /// Parameters are passed in a `ProjectionRequest` struct (F043) so adapters
629    /// and callers cannot misorder them.
630    ///
631    /// # Errors
632    ///
633    /// Same errors as `execute_with_projection`.
634    async fn execute_with_projection_arc(
635        &self,
636        request: &ProjectionRequest<'_>,
637    ) -> Result<Arc<Vec<JsonbValue>>> {
638        self.execute_with_projection(
639            request.view,
640            request.projection,
641            request.where_clause,
642            request.limit,
643            request.offset,
644            request.order_by,
645        )
646        .await
647        .map(Arc::new)
648    }
649
650    /// Get database type (for logging/metrics).
651    ///
652    /// Used to identify which database backend is in use.
653    fn database_type(&self) -> DatabaseType;
654
655    /// Whether reads may be served by a hot standby — read replicas are configured (#1390).
656    ///
657    /// A standby cannot read an UNLOGGED or temporary relation, so a server whose reads go
658    /// to replicas checks at boot that none of its sources depends on one. A wrapper
659    /// adapter must forward this to the adapter it wraps: inheriting the default would
660    /// switch that check off.
661    fn serves_reads_from_standbys(&self) -> bool {
662        false
663    }
664
665    /// Health check - verify database connectivity.
666    ///
667    /// Executes a simple query (e.g., `SELECT 1`) to verify the database is reachable.
668    ///
669    /// # Errors
670    ///
671    /// Returns `FraiseQLError::Database` if health check fails.
672    async fn health_check(&self) -> Result<()>;
673
674    /// Get connection pool metrics.
675    ///
676    /// Returns current statistics about the connection pool:
677    /// - Total connections
678    /// - Idle connections
679    /// - Active connections
680    /// - Waiting requests
681    fn pool_metrics(&self) -> PoolMetrics;
682
683    /// Execute raw SQL query and return rows as JSON objects.
684    ///
685    /// Used for aggregation queries where we need full row data, not just JSONB column.
686    ///
687    /// # Security Warning
688    ///
689    /// This method executes arbitrary SQL. **NEVER** pass untrusted input directly to this method.
690    /// Always:
691    /// - Use parameterized queries with bound parameters
692    /// - Validate and sanitize SQL templates before execution
693    /// - Only execute SQL generated by the FraiseQL compiler
694    /// - Log SQL execution for audit trails
695    ///
696    /// # Arguments
697    ///
698    /// * `sql` - Raw SQL query to execute (must be safe/trusted)
699    ///
700    /// # Returns
701    ///
702    /// Vec of rows, where each row is a HashMap of column name to JSON value.
703    ///
704    /// # Errors
705    ///
706    /// Returns `FraiseQLError::Database` on query execution failure.
707    ///
708    /// # Example
709    ///
710    /// ```rust,no_run
711    /// # use fraiseql_db::DatabaseAdapter;
712    /// # async fn example(adapter: impl DatabaseAdapter) -> Result<(), Box<dyn std::error::Error>> {
713    /// // Safe: SQL generated by FraiseQL compiler
714    /// let sql = "SELECT category, SUM(revenue) as total FROM tf_sales GROUP BY category";
715    /// let rows = adapter.execute_raw_query(sql).await?;
716    /// for row in rows {
717    ///     println!("Category: {}, Total: {}", row["category"], row["total"]);
718    /// }
719    /// # Ok(())
720    /// # }
721    /// ```
722    async fn execute_raw_query(
723        &self,
724        sql: &str,
725    ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>>;
726
727    /// Execute a row-shaped query against a view, returning typed column values.
728    ///
729    /// Used by the gRPC transport for protobuf encoding of query results.
730    /// The default implementation delegates to `execute_raw_query` and converts
731    /// JSON results to `ColumnValue` vectors.
732    ///
733    /// # Errors
734    ///
735    /// Returns `FraiseQLError::Database` if the adapter returns an error.
736    async fn execute_row_query(
737        &self,
738        view_name: &str,
739        columns: &[crate::types::ColumnSpec],
740        where_sql: Option<&str>,
741        order_by: Option<&str>,
742        limit: Option<u32>,
743        offset: Option<u32>,
744    ) -> Result<Vec<Vec<crate::types::ColumnValue>>> {
745        let sql = build_row_query_sql(view_name, where_sql, order_by, limit, offset);
746        let results = self.execute_raw_query(&sql).await?;
747
748        Ok(results.iter().map(|row| row_to_column_values(row, columns)).collect())
749    }
750
751    /// Connection-affine variant of [`execute_row_query`](Self::execute_row_query).
752    ///
753    /// See
754    /// [`execute_where_query_arc_with_session`](Self::execute_where_query_arc_with_session)
755    /// for the rationale: an RLS policy backed by `current_setting()` (#329) only sees
756    /// the variables if they are applied transaction-locally on the **same** connection
757    /// as the read.
758    ///
759    /// # Why the row shape needs its own pair
760    ///
761    /// The row-shaped read is the gRPC transport's source. Until #1351 it resolved its
762    /// own predicate and never reached the engine, so it had no session variables to
763    /// pin and none of these methods existed. Routing it through the direct-read
764    /// chokepoint gives it `session_variables` like every other read — and a resolved
765    /// value the read cannot apply is the failure mode the chokepoint exists to
766    /// prevent, so it travels rather than being dropped.
767    ///
768    /// # Errors
769    ///
770    /// Same errors as [`execute_row_query`](Self::execute_row_query); additionally
771    /// returns `FraiseQLError::Database` if `set_config` fails on any pair.
772    async fn execute_row_query_with_session(
773        &self,
774        view_name: &str,
775        columns: &[crate::types::ColumnSpec],
776        where_sql: Option<&str>,
777        order_by: Option<&str>,
778        limit: Option<u32>,
779        offset: Option<u32>,
780        _session_vars: &[(&str, &str)],
781    ) -> Result<Vec<Vec<crate::types::ColumnValue>>> {
782        self.execute_row_query(view_name, columns, where_sql, order_by, limit, offset)
783            .await
784    }
785
786    /// Execute a parameterized aggregate SQL query (GROUP BY / HAVING / window).
787    ///
788    /// `sql` contains `$N` (PostgreSQL), `?` (MySQL / SQLite), or `@P1` (SQL Server)
789    /// placeholders for string and array values; numeric and NULL values may be inlined.
790    /// `params` are the corresponding values in placeholder order.
791    ///
792    /// Unlike `execute_raw_query`, this method accepts bind parameters so that
793    /// user-supplied filter values never appear as string literals in the SQL text,
794    /// eliminating the injection risk that `escape_sql_string` mitigated previously.
795    ///
796    /// # Arguments
797    ///
798    /// * `sql` - SQL with placeholders generated by
799    ///   `AggregationSqlGenerator::generate_parameterized`
800    /// * `params` - Bind parameters in placeholder order
801    ///
802    /// # Returns
803    ///
804    /// Vec of rows, where each row is a `HashMap` of column name to JSON value.
805    ///
806    /// # Errors
807    ///
808    /// Returns `FraiseQLError::Database` on execution failure.
809    /// Returns `FraiseQLError::Database` on adapters that do not support raw SQL
810    /// (e.g., `FraiseWireAdapter`).
811    async fn execute_parameterized_aggregate(
812        &self,
813        sql: &str,
814        params: &[serde_json::Value],
815    ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>>;
816
817    /// Connection-affine variant of
818    /// [`execute_parameterized_aggregate`](Self::execute_parameterized_aggregate).
819    ///
820    /// Applies `session_vars` transaction-locally on the same connection that
821    /// runs the aggregate, so aggregate views backed by `current_setting()` RLS
822    /// observe the configured values (fixes #329 for the aggregate path). The default
823    /// ignores `session_vars`: a backend without transaction-local settings has none to
824    /// apply.
825    ///
826    /// # Errors
827    ///
828    /// Same errors as [`execute_parameterized_aggregate`](Self::execute_parameterized_aggregate);
829    /// additionally returns `FraiseQLError::Database` if `set_config` fails.
830    async fn execute_parameterized_aggregate_with_session(
831        &self,
832        sql: &str,
833        params: &[serde_json::Value],
834        _session_vars: &[(&str, &str)],
835        _routing: ReadRouting,
836    ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
837        self.execute_parameterized_aggregate(sql, params).await
838    }
839
840    /// Whether this adapter implements
841    /// [`execute_composed_with_session`](Self::execute_composed_with_session) — whether a
842    /// read can have related resources composed into it (the REST `?select=` embed).
843    ///
844    /// Asked twice, of one answer: when the REST transport is mounted, so a deployment
845    /// whose adapter cannot serve embeds is told at boot rather than by its first embed;
846    /// and by the engine before a composed read is sent, so the `501` a request gets is
847    /// that same fact rather than a second one.
848    ///
849    /// **Implementing `execute_composed_with_session` obliges you to override this too**,
850    /// and a wrapping adapter must forward both. Defaults to `false`: a capability is not
851    /// something a backend should acquire by omission.
852    fn supports_composed_reads(&self) -> bool {
853        false
854    }
855
856    /// Invalidate cached query results for the specified views.
857    ///
858    /// Called by the executor after a mutation succeeds, so that stale cache
859    /// entries reading from modified views are evicted. The default
860    /// implementation is a no-op; `CachedDatabaseAdapter` overrides this.
861    ///
862    /// View names are passed as `&[ViewName]` so the wrapper's `Arc<str>`
863    /// backing is preserved across the call. Callers that hold a `String`
864    /// can convert in place with `ViewName::from(...)`.
865    ///
866    /// # Returns
867    ///
868    /// The number of cache entries evicted.
869    async fn invalidate_views(&self, _views: &[crate::ViewName]) -> Result<u64> {
870        Ok(0)
871    }
872
873    /// Evict cache entries that contain the given entity UUID.
874    ///
875    /// Called by the executor after a successful UPDATE or DELETE mutation when
876    /// the `mutation_response` includes an `entity_id`. Only cache entries whose
877    /// entity-ID index contains the given UUID are removed; unrelated entries
878    /// remain warm.
879    ///
880    /// The default implementation is a no-op. `CachedDatabaseAdapter` overrides
881    /// this to perform the selective eviction.
882    ///
883    /// # Returns
884    ///
885    /// The number of cache entries evicted.
886    async fn invalidate_by_entity(&self, _entity_type: &str, _entity_id: &str) -> Result<u64> {
887        Ok(0)
888    }
889
890    /// A snapshot of this adapter's query-result cache, or `None` if it has none.
891    ///
892    /// The operator surface for the cache that actually serves GraphQL queries.
893    /// `None` means "this adapter does not cache" — distinct from `Some(stats)` with
894    /// zero entries, which means "the cache is there and currently empty". The admin
895    /// API reported the second as the first for every non-Arrow deployment (#941).
896    ///
897    /// The default implementation returns `None`; `CachedDatabaseAdapter` overrides it.
898    fn result_cache_stats(&self) -> Option<crate::ResultCacheStats> {
899        None
900    }
901
902    /// Drop every entry in this adapter's query-result cache.
903    ///
904    /// Returns the number of entries dropped, or `None` if this adapter has no cache
905    /// — again so the caller can say "there is no such cache" rather than reporting a
906    /// successful clear of nothing.
907    ///
908    /// The default implementation returns `Ok(None)`; `CachedDatabaseAdapter` overrides it.
909    ///
910    /// # Errors
911    ///
912    /// Returns `FraiseQLError` if the underlying cache cannot be cleared.
913    async fn clear_result_cache(&self) -> Result<Option<usize>> {
914        Ok(None)
915    }
916
917    /// Run the database's `EXPLAIN` on a SQL statement without executing it.
918    ///
919    /// Returns a JSON representation of the query plan. The format is
920    /// database-specific (e.g. PostgreSQL returns JSON, SQLite returns rows).
921    ///
922    /// The default implementation returns `Unsupported`.
923    async fn explain_query(
924        &self,
925        _sql: &str,
926        _params: &[serde_json::Value],
927    ) -> Result<serde_json::Value> {
928        Err(fraiseql_error::FraiseQLError::Unsupported {
929            message: "EXPLAIN not available for this database adapter".to_string(),
930        })
931    }
932
933    /// Run `EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)` against a view with the
934    /// same parameterized WHERE clause that `execute_where_query` would use.
935    ///
936    /// Unlike `explain_query`, this method uses **real bound parameters** and
937    /// **actually executes the query** (ANALYZE mode), so the plan reflects
938    /// PostgreSQL's runtime statistics for the given filter values.
939    ///
940    /// Only PostgreSQL supports this; other adapters return
941    /// `FraiseQLError::Unsupported` by default.
942    ///
943    /// # Arguments
944    ///
945    /// * `view` - View name (e.g., "v_user")
946    /// * `where_clause` - Optional filter (same as `execute_where_query`)
947    /// * `limit` - Optional row limit
948    /// * `offset` - Optional row offset
949    ///
950    /// # Errors
951    ///
952    /// Returns `FraiseQLError::Database` on execution failure.
953    /// Returns `FraiseQLError::Unsupported` for non-PostgreSQL adapters.
954    async fn explain_where_query(
955        &self,
956        _view: &str,
957        _where_clause: Option<&WhereClause>,
958        _limit: Option<u32>,
959        _offset: Option<u32>,
960    ) -> Result<serde_json::Value> {
961        Err(fraiseql_error::FraiseQLError::Unsupported {
962            message: "EXPLAIN ANALYZE is not available for this database adapter. \
963                      Only PostgreSQL supports explain_where_query."
964                .to_string(),
965        })
966    }
967
968    /// Connection-affine variant of [`execute_where_query_arc`](Self::execute_where_query_arc).
969    ///
970    /// Applies `session_vars` transaction-locally on the same connection that
971    /// runs the read, so PostgreSQL Row-Level-Security policies backed by
972    /// `current_setting()` see the configured values (fixes #329). The default ignores
973    /// `session_vars`: a backend without transaction-local settings has none to apply.
974    ///
975    /// # Errors
976    ///
977    /// Same errors as [`execute_where_query_arc`](Self::execute_where_query_arc); additionally
978    /// returns `FraiseQLError::Database` if `set_config` fails on any pair.
979    async fn execute_where_query_arc_with_session(
980        &self,
981        view: &str,
982        where_clause: Option<&WhereClause>,
983        limit: Option<u32>,
984        offset: Option<u32>,
985        order_by: Option<&[OrderByClause]>,
986        _session_vars: &[(&str, &str)],
987        _routing: ReadRouting,
988    ) -> Result<Arc<Vec<JsonbValue>>> {
989        self.execute_where_query_arc(view, where_clause, limit, offset, order_by).await
990    }
991
992    /// Count the rows of `view` matching `where_clause`, without materialising them.
993    ///
994    /// Backs both the GraphQL `<name>Count` sibling (#938) and the REST
995    /// `Prefer: count=exact` header, so the two cannot answer the same question
996    /// differently.
997    ///
998    /// `session_vars` are applied transaction-locally on the connection that
999    /// runs the count, exactly as for the read it describes (#329). This is
1000    /// load-bearing rather than symmetric-for-neatness: under RLS, a count that
1001    /// ran without the session variables would report the *unfiltered* total
1002    /// beside a filtered page — a row-count oracle over rows the caller cannot
1003    /// read.
1004    ///
1005    /// # Default implementation
1006    ///
1007    /// The default fetches the matching rows and counts them. That is *correct*
1008    /// — same view, same predicate, same session — but it is O(rows) in memory,
1009    /// so an adapter that can push the count into the database should override
1010    /// it. `PostgresAdapter` does, with `SELECT COUNT(*)`.
1011    ///
1012    /// # Errors
1013    ///
1014    /// Returns `FraiseQLError::Database` if the query fails.
1015    async fn count_where_query(
1016        &self,
1017        view: &str,
1018        where_clause: Option<&WhereClause>,
1019        session_vars: &[(&str, &str)],
1020        routing: ReadRouting,
1021    ) -> Result<u64> {
1022        let rows = self
1023            .execute_where_query_arc_with_session(
1024                view,
1025                where_clause,
1026                None,
1027                None,
1028                None,
1029                session_vars,
1030                routing,
1031            )
1032            .await?;
1033        Ok(rows.len() as u64)
1034    }
1035
1036    /// Connection-affine variant of
1037    /// [`execute_with_projection_arc`](Self::execute_with_projection_arc).
1038    ///
1039    /// See [`execute_where_query_arc_with_session`](Self::execute_where_query_arc_with_session) for
1040    /// the rationale.
1041    ///
1042    /// # Errors
1043    ///
1044    /// Same errors as [`execute_with_projection_arc`](Self::execute_with_projection_arc);
1045    /// additionally returns `FraiseQLError::Database` if `set_config` fails on any pair.
1046    async fn execute_with_projection_arc_with_session(
1047        &self,
1048        request: &ProjectionRequest<'_>,
1049        _session_vars: &[(&str, &str)],
1050        _routing: ReadRouting,
1051    ) -> Result<Arc<Vec<JsonbValue>>> {
1052        self.execute_with_projection_arc(request).await
1053    }
1054
1055    /// The same read as
1056    /// [`execute_with_projection_arc_with_session`](Self::execute_with_projection_arc_with_session),
1057    /// delivered row by row instead of as a materialised `Vec` (#958).
1058    ///
1059    /// Same view, same predicate, same session variables, same routing. The only
1060    /// difference is *when* the rows arrive and how much memory the caller needs
1061    /// to hold them: a streamed read is `O(1)` in the number of rows, so an export
1062    /// of a table larger than memory is possible where the collecting method would
1063    /// have to fail.
1064    ///
1065    /// # Why this exists as its own method
1066    ///
1067    /// A paginated re-execution loop (`LIMIT n OFFSET k`, `k += n`) delivers the
1068    /// same rows in bounded memory and needs no new trait method, and that is what
1069    /// the export surfaces did. It has two properties a stream does not:
1070    ///
1071    /// - it is `O(offset)` per batch, so exporting `N` rows costs `O(N²)` row scans;
1072    /// - each batch is its own snapshot, so a concurrent insert or delete between batches shifts
1073    ///   rows across the page boundary — silently duplicating a row into two batches, or skipping
1074    ///   one entirely.
1075    ///
1076    /// One statement over one portal has neither problem.
1077    ///
1078    /// # Default implementation
1079    ///
1080    /// Collects the read and replays it. That is *correct* — same rows, same order —
1081    /// but it holds the whole result set, so an adapter that can stream should
1082    /// override it. `PostgresAdapter` does.
1083    ///
1084    /// ⚠ A wrapping adapter (caching, instrumentation) that inherits this default
1085    /// converts every streaming caller back into a buffering one **without failing**:
1086    /// the rows are right and the memory bound is gone. A wrapper must forward this
1087    /// method explicitly.
1088    ///
1089    /// # Errors
1090    ///
1091    /// Returns the same errors as
1092    /// [`execute_with_projection_arc_with_session`](Self::execute_with_projection_arc_with_session).
1093    /// Note that an implementation that truly streams can only report the errors it
1094    /// has seen *so far* when this returns: a failure raised after the first row —
1095    /// a lost connection, a runtime cast error on a later row — surfaces as an `Err`
1096    /// item inside the stream, not from this call.
1097    async fn stream_with_projection(
1098        &self,
1099        request: &ProjectionRequest<'_>,
1100        session_vars: &[(&str, &str)],
1101        routing: ReadRouting,
1102    ) -> Result<JsonbRowStream> {
1103        let rows = self
1104            .execute_with_projection_arc_with_session(request, session_vars, routing)
1105            .await?;
1106        // Unwrap when this call owns the only reference (the usual case, since the
1107        // read above just built it) so replaying the buffer costs no deep clone.
1108        let rows = Arc::try_unwrap(rows).unwrap_or_else(|shared| (*shared).clone());
1109        Ok(Box::pin(futures::stream::iter(rows.into_iter().map(Ok))))
1110    }
1111
1112    /// Execute a read that composes its embedded levels into **one** statement.
1113    ///
1114    /// The REST `?select=` embed used to be a parent read plus one sub-read per parent
1115    /// row per level; this is the same answer in one round trip, one snapshot and one
1116    /// pooled connection. See [`ComposedLevel`] for the call shape and for the
1117    /// `{"d": …, "e": …}` rows it returns.
1118    ///
1119    /// Session variables and routing mean what they mean for
1120    /// [`execute_with_projection_arc_with_session`](Self::execute_with_projection_arc_with_session).
1121    ///
1122    /// # Default implementation
1123    ///
1124    /// Refuses. Composing needs correlated `LATERAL` subqueries and JSON aggregation,
1125    /// which an adapter has to render for its own dialect; answering with anything
1126    /// else — the root rows without their embeds, say — would be a partial response
1127    /// indistinguishable from a parent with no related rows. A wrapping adapter must
1128    /// forward this method explicitly.
1129    ///
1130    /// # Errors
1131    ///
1132    /// `FraiseQLError::Unsupported` from the default; otherwise the errors of
1133    /// [`execute_with_projection_arc_with_session`](Self::execute_with_projection_arc_with_session).
1134    async fn execute_composed_with_session(
1135        &self,
1136        read: &ComposedLevel,
1137        _session_vars: &[(&str, &str)],
1138        _routing: ReadRouting,
1139    ) -> Result<Arc<Vec<JsonbValue>>> {
1140        Err(composed_read_unsupported(&read.view))
1141    }
1142
1143    /// The same read as [`execute_row_query`](Self::execute_row_query), delivered
1144    /// row by row (#958).
1145    ///
1146    /// The column-shaped counterpart of
1147    /// [`stream_with_projection`](Self::stream_with_projection); see that method for
1148    /// why streaming and paginated re-execution are not interchangeable. This is the
1149    /// gRPC transport's source, which encodes one protobuf message per row and has
1150    /// no use for a `Vec` of them.
1151    ///
1152    /// # Default implementation
1153    ///
1154    /// Collects and replays, with the same caveat about wrapping adapters.
1155    ///
1156    /// # Errors
1157    ///
1158    /// Returns the same errors as [`execute_row_query`](Self::execute_row_query);
1159    /// see [`stream_with_projection`](Self::stream_with_projection) for which of
1160    /// them can still arrive after this call has returned `Ok`.
1161    async fn stream_row_query(
1162        &self,
1163        view_name: &str,
1164        columns: &[crate::types::ColumnSpec],
1165        where_sql: Option<&str>,
1166        order_by: Option<&str>,
1167        limit: Option<u32>,
1168        offset: Option<u32>,
1169    ) -> Result<ColumnRowStream> {
1170        let rows = self
1171            .execute_row_query(view_name, columns, where_sql, order_by, limit, offset)
1172            .await?;
1173        Ok(Box::pin(futures::stream::iter(rows.into_iter().map(Ok))))
1174    }
1175
1176    /// Connection-affine variant of [`stream_row_query`](Self::stream_row_query).
1177    ///
1178    /// The streaming twin of
1179    /// [`execute_row_query_with_session`](Self::execute_row_query_with_session); see
1180    /// that method for why the row shape carries session variables at all. The
1181    /// streaming arm is where dropping them costs the most, because it is the arm
1182    /// that delivers an unbounded number of rows.
1183    ///
1184    /// # Errors
1185    ///
1186    /// Same errors as [`stream_row_query`](Self::stream_row_query); additionally
1187    /// returns `FraiseQLError::Database` if `set_config` fails on any pair.
1188    async fn stream_row_query_with_session(
1189        &self,
1190        view_name: &str,
1191        columns: &[crate::types::ColumnSpec],
1192        where_sql: Option<&str>,
1193        order_by: Option<&str>,
1194        limit: Option<u32>,
1195        offset: Option<u32>,
1196        _session_vars: &[(&str, &str)],
1197    ) -> Result<ColumnRowStream> {
1198        self.stream_row_query(view_name, columns, where_sql, order_by, limit, offset)
1199            .await
1200    }
1201
1202    /// Retrieve query performance statistics from the database.
1203    ///
1204    /// Returns the top-N queries ordered by total execution time (descending).
1205    /// The exact data source depends on the backend:
1206    /// - PostgreSQL: `pg_stat_statements` (requires extension)
1207    /// - MySQL: `performance_schema.events_statements_summary_by_digest`
1208    /// - SQL Server: `sys.dm_exec_query_stats`
1209    /// - SQLite / Wire: empty (no stats available)
1210    ///
1211    /// # Arguments
1212    ///
1213    /// * `limit` - Maximum number of entries to return.
1214    ///
1215    /// # Errors
1216    ///
1217    /// Returns `FraiseQLError::Database` if the stats query fails.
1218    async fn query_stats(&self, _limit: u32) -> Result<Vec<crate::types::QueryStatEntry>> {
1219        Ok(vec![])
1220    }
1221
1222    /// Retrieve statistics for a single query by its ID.
1223    ///
1224    /// The default implementation fetches up to 1000 entries via
1225    /// [`query_stats`](Self::query_stats) and filters client-side.
1226    /// Backends with efficient single-query lookup (PostgreSQL, SQL Server)
1227    /// should override with a `WHERE` clause.
1228    ///
1229    /// # Errors
1230    ///
1231    /// Returns `FraiseQLError::Database` if the underlying query fails.
1232    async fn query_stats_by_id(&self, id: &str) -> Result<Option<crate::types::QueryStatEntry>> {
1233        let stats = self.query_stats(1000).await?;
1234        Ok(stats.into_iter().find(|e| e.query_id == id))
1235    }
1236
1237    /// Reset query performance statistics.
1238    ///
1239    /// Only PostgreSQL supports this (via `pg_stat_statements_reset()`).
1240    /// All other adapters return `Unsupported`.
1241    ///
1242    /// # Errors
1243    ///
1244    /// Returns `FraiseQLError::Unsupported` for adapters that cannot reset stats.
1245    /// Returns `FraiseQLError::Database` if the reset command fails.
1246    async fn reset_query_stats(&self) -> Result<()> {
1247        Err(FraiseQLError::Unsupported {
1248            message: "Query stats reset is not supported by this database adapter".to_string(),
1249        })
1250    }
1251
1252    /// Notify the adapter that the schema has changed.
1253    ///
1254    /// Called during hot-reload after the new schema has been validated.
1255    /// Adapters that maintain schema-dependent state (e.g. cache keyed by schema
1256    /// version) should clear or rebuild that state here.
1257    ///
1258    /// The default implementation is a no-op.
1259    fn on_schema_reload(&self) {}
1260
1261    /// Run one operator-supplied statement under the bounds in `request` (#962).
1262    ///
1263    /// This is the only method on this trait whose SQL FraiseQL did not generate.
1264    /// It exists for the Studio SQL console, and it is deliberately the *narrowest*
1265    /// possible shape for that: a single statement, in a transaction whose mode,
1266    /// timeout, row budget and session identity are all decided by the caller and
1267    /// enforced by the database. See [`AdminSqlRequest`] for what each bound buys.
1268    ///
1269    /// # Default implementation
1270    ///
1271    /// **Refuses.** An adapter that has not implemented containment does not get
1272    /// to run arbitrary SQL by inheriting a default that "just works" — the
1273    /// convenient default here would be `execute_raw_query`, which has no
1274    /// transaction, no timeout, no row cap and no way to roll back. Every
1275    /// adapter that should serve this endpoint says so by overriding, and the
1276    /// endpoint answers "unsupported" for the rest.
1277    ///
1278    /// A wrapping adapter must forward explicitly for the same reason it forwards
1279    /// [`stream_with_projection`](Self::stream_with_projection): inheriting the
1280    /// default here turns a capable adapter into an incapable one, and the only
1281    /// symptom is a refusal nobody asked for.
1282    ///
1283    /// # Errors
1284    ///
1285    /// Returns `FraiseQLError::Unsupported` by default. Implementations return
1286    /// `FraiseQLError::Database` for anything PostgreSQL rejects — which
1287    /// includes the write refused by a `READ ONLY` transaction (SQLSTATE
1288    /// `25006`) and the statement cancelled by the timeout (`57014`).
1289    async fn execute_admin_sql(&self, _request: &AdminSqlRequest) -> Result<AdminSqlOutcome> {
1290        Err(FraiseQLError::Unsupported {
1291            message: "Operator-supplied SQL execution is not supported by this database adapter."
1292                .to_string(),
1293        })
1294    }
1295}
1296
1297/// The `SELECT` behind [`DatabaseAdapter::execute_row_query`] and its streaming
1298/// sibling.
1299///
1300/// Shared rather than written twice: the two methods answer the same question and
1301/// a transport that got a different row set depending on whether it streamed would
1302/// be the hardest kind of bug to see. `where_sql` and `order_by` are compiler-
1303/// generated SQL fragments, as the method contract requires.
1304#[must_use]
1305pub fn build_row_query_sql(
1306    view_name: &str,
1307    where_sql: Option<&str>,
1308    order_by: Option<&str>,
1309    limit: Option<u32>,
1310    offset: Option<u32>,
1311) -> String {
1312    use std::fmt::Write as _;
1313
1314    let mut sql = format!("SELECT * FROM \"{view_name}\"");
1315    if let Some(w) = where_sql {
1316        sql.push_str(" WHERE ");
1317        sql.push_str(w);
1318    }
1319    if let Some(ob) = order_by {
1320        sql.push_str(" ORDER BY ");
1321        sql.push_str(ob);
1322    }
1323    if let Some(l) = limit {
1324        let _ = write!(sql, " LIMIT {l}");
1325    }
1326    if let Some(o) = offset {
1327        let _ = write!(sql, " OFFSET {o}");
1328    }
1329    sql
1330}
1331
1332/// Decode one JSON-shaped row into the declared column order.
1333///
1334/// A column the row does not carry decodes to [`crate::types::ColumnValue::Null`]
1335/// rather than shortening the row: the transport encodes positionally against
1336/// `columns`.
1337#[must_use]
1338pub fn row_to_column_values<S: std::hash::BuildHasher>(
1339    row: &std::collections::HashMap<String, serde_json::Value, S>,
1340    columns: &[crate::types::ColumnSpec],
1341) -> Vec<crate::types::ColumnValue> {
1342    use crate::types::ColumnValue;
1343
1344    columns
1345        .iter()
1346        .map(|col| {
1347            row.get(&col.name).map_or(ColumnValue::Null, |v| match v {
1348                serde_json::Value::Null => ColumnValue::Null,
1349                serde_json::Value::Bool(b) => ColumnValue::Boolean(*b),
1350                serde_json::Value::Number(n) => {
1351                    if let Some(i) = n.as_i64() {
1352                        ColumnValue::Int64(i)
1353                    } else if let Some(f) = n.as_f64() {
1354                        ColumnValue::Float64(f)
1355                    } else {
1356                        ColumnValue::Text(n.to_string())
1357                    }
1358                },
1359                serde_json::Value::String(s) => ColumnValue::Text(s.clone()),
1360                other => ColumnValue::Json(other.to_string()),
1361            })
1362        })
1363        .collect()
1364}
1365
1366#[cfg(test)]
1367mod tests;